From d2be893a5a724de45cf59ec6948fa37b429de320 Mon Sep 17 00:00:00 2001 From: Elliot DeNolf Date: Mon, 18 Jul 2022 09:55:59 -0700 Subject: [PATCH] test: port relationships spec --- test/relationships/config.ts | 214 ++++++++++++++++++++++++++++ test/relationships/int.spec.ts | 182 +++++++++++++++++++++++ test/relationships/payload-types.ts | 83 +++++++++++ 3 files changed, 479 insertions(+) create mode 100644 test/relationships/config.ts create mode 100644 test/relationships/int.spec.ts create mode 100644 test/relationships/payload-types.ts diff --git a/test/relationships/config.ts b/test/relationships/config.ts new file mode 100644 index 0000000000..4474eb133d --- /dev/null +++ b/test/relationships/config.ts @@ -0,0 +1,214 @@ +import type { CollectionConfig } from '../../src/collections/config/types'; +import { devUser } from '../credentials'; +import { buildConfig } from '../buildConfig'; +import type { CustomIdRelation, Post, Relation } from './payload-types'; + +const openAccess = { + create: () => true, + read: () => true, + update: () => true, + delete: () => true, +}; + +const defaultAccess = ({ req: { user } }) => Boolean(user); + +const collectionWithName = (collectionSlug: string): CollectionConfig => { + return { + slug: collectionSlug, + access: openAccess, + fields: [ + { + name: 'name', + type: 'text', + }, + { + name: 'disableRelation', // used filteredRelation + type: 'checkbox', + required: true, + admin: { + position: 'sidebar', + }, + }, + ], + }; +}; + +export const slug = 'posts'; +export const relationSlug = 'relation'; +export const defaultAccessRelSlug = 'strict-access'; +export const chainedRelSlug = 'chained-relation'; +export const customIdSlug = 'custom-id-relation'; +export default buildConfig({ + collections: [ + { + slug, + access: openAccess, + fields: [ + { + name: 'title', + type: 'text', + }, + { + name: 'description', + type: 'text', + }, + { + name: 'number', + type: 'number', + }, + // Relationship + { + name: 'relationField', + type: 'relationship', + relationTo: relationSlug, + }, + // Relationship w/ default access + { + name: 'defaultAccessRelation', + type: 'relationship', + relationTo: defaultAccessRelSlug, + }, + { + name: 'chainedRelation', + type: 'relationship', + relationTo: chainedRelSlug, + }, + { + name: 'maxDepthRelation', + maxDepth: 0, + type: 'relationship', + relationTo: relationSlug, + }, + { + name: 'customIdRelation', + type: 'relationship', + relationTo: customIdSlug, + }, + { + name: 'filteredRelation', + type: 'relationship', + relationTo: relationSlug, + filterOptions: { + disableRelation: { + not_equals: true, + }, + }, + }, + ], + }, + collectionWithName(relationSlug), + { + ...collectionWithName(defaultAccessRelSlug), + access: { + create: defaultAccess, + read: defaultAccess, + update: defaultAccess, + delete: defaultAccess, + }, + }, + { + slug: chainedRelSlug, + access: openAccess, + fields: [ + { + name: 'name', + type: 'text', + }, + { + name: 'relation', + type: 'relationship', + relationTo: chainedRelSlug, + }, + ], + }, + { + slug: customIdSlug, + fields: [ + { + name: 'id', + type: 'text', + }, + { + name: 'name', + type: 'text', + }, + ], + }, + ], + onInit: async (payload) => { + await payload.create({ + collection: 'users', + data: { + email: devUser.email, + password: devUser.password, + }, + }); + + const rel1 = await payload.create({ + collection: relationSlug, + data: { + name: 'name', + }, + }); + + const filteredRelation = await payload.create({ + collection: relationSlug, + data: { + name: 'filtered', + }, + }); + + const defaultAccessRelation = await payload.create({ + collection: defaultAccessRelSlug, + data: { + name: 'name', + }, + }); + + const chained3 = await payload.create({ + collection: chainedRelSlug, + data: { + name: 'chain3', + }, + }); + + const chained2 = await payload.create({ + collection: chainedRelSlug, + data: { + name: 'chain2', + relation: chained3.id, + }, + }); + + const chained = await payload.create({ + collection: chainedRelSlug, + data: { + name: 'chain1', + relation: chained2.id, + }, + }); + + const customIdRelation = await payload.create({ + collection: customIdSlug, + data: { + id: 'custommmm', + name: 'custom-id', + }, + }); + + + // Relationship + await payload.create({ + collection: slug, + data: { + title: 'with relationship', + relationField: rel1.id, + defaultAccessRelation: defaultAccessRelation.id, + chainedRelation: chained.id, + maxDepthRelation: rel1.id, + customIdRelation: customIdRelation.id, + filteredRelation: filteredRelation.id, + }, + }); + }, +}); diff --git a/test/relationships/int.spec.ts b/test/relationships/int.spec.ts new file mode 100644 index 0000000000..5408e40968 --- /dev/null +++ b/test/relationships/int.spec.ts @@ -0,0 +1,182 @@ +import mongoose from 'mongoose'; +import { randomBytes } from 'crypto'; +import { initPayloadTest } from '../helpers/configHelpers'; +import config, { customIdSlug, chainedRelSlug, defaultAccessRelSlug, slug, relationSlug } from './config'; +import payload from '../../src'; +import { RESTClient } from '../helpers/rest'; +import type { ChainedRelation, CustomIdRelation, Post, Relation } from './payload-types'; +import { mapAsync } from '../../src/utilities/mapAsync'; + +let client: RESTClient; + +type EasierChained = { relation: EasierChained, id: string } + +describe('Relationships', () => { + beforeAll(async () => { + const { serverURL } = await initPayloadTest({ __dirname, init: { local: false } }); + client = new RESTClient(config, { serverURL, defaultSlug: slug }); + await client.login(); + }); + + afterAll(async () => { + await mongoose.connection.dropDatabase(); + await mongoose.connection.close(); + await payload.mongoMemoryServer.stop(); + }); + + beforeEach(async () => { + await clearDocs(); + }); + + describe('Querying', () => { + describe('Relationships', () => { + let post: Post; + let relation: Relation; + let filteredRelation: Relation; + let defaultAccessRelation: Relation; + let chained: ChainedRelation; + let chained2: ChainedRelation; + let chained3: ChainedRelation; + let customIdRelation: CustomIdRelation; + let generatedCustomId: string; + const nameToQuery = 'name'; + + beforeEach(async () => { + relation = await payload.create({ + collection: relationSlug, + data: { + name: nameToQuery, + }, + }); + + filteredRelation = await payload.create({ + collection: relationSlug, + data: { + name: nameToQuery, + disableRelation: false, + }, + }); + + defaultAccessRelation = await payload.create({ + collection: defaultAccessRelSlug, + data: { + name: 'default access', + }, + }); + + chained3 = await payload.create({ + collection: chainedRelSlug, + data: { + name: 'chain3', + }, + }); + + chained2 = await payload.create({ + collection: chainedRelSlug, + data: { + name: 'chain2', + relation: chained3.id, + }, + }); + + chained = await payload.create({ + collection: chainedRelSlug, + data: { + name: 'chain1', + relation: chained2.id, + }, + }); + + generatedCustomId = `custom-${randomBytes(32).toString('hex').slice(0, 12)}`; + customIdRelation = await payload.create({ + collection: customIdSlug, + data: { + id: generatedCustomId, + name: 'custom-id', + }, + }); + + post = await createPost({ + relationField: relation.id, + defaultAccessRelation: defaultAccessRelation.id, + chainedRelation: chained.id, + maxDepthRelation: relation.id, + customIdRelation: customIdRelation.id, + filteredRelation: filteredRelation.id, + }); + + await createPost(); // Extra post to allow asserting totalDoc count + }); + + it('should prevent an unauthorized population of strict access', async () => { + const { doc } = await client.findByID({ id: post.id, auth: false }); + expect(doc.defaultAccessRelation).toEqual(defaultAccessRelation.id); + }); + + it('should populate strict access when authorized', async () => { + const { doc } = await client.findByID({ id: post.id }); + expect(doc.defaultAccessRelation).toEqual(defaultAccessRelation); + }); + + it('should use filterOptions to limit relationship options', async () => { + const { doc } = await client.findByID({ id: post.id }); + + expect(doc.filteredRelation).toMatchObject({ id: filteredRelation.id }); + + await client.update({ id: filteredRelation.id, slug: relationSlug, data: { disableRelation: true } }); + + const { doc: docAfterUpdatingRel } = await client.findByID({ id: post.id }); + + // No change to existing relation + expect(docAfterUpdatingRel.filteredRelation).toMatchObject({ id: filteredRelation.id }); + + // Attempt to update post with a now filtered relation + const { status, errors } = await client.update({ id: post.id, data: { filteredRelation: filteredRelation.id } }); + + expect(errors?.[0]).toMatchObject({ name: 'ValidationError', message: expect.any(String), data: expect.anything() }); + expect(status).toEqual(400); + }); + + describe('depth', () => { + it('should populate to depth', async () => { + const { doc } = await client.findByID({ id: post.id, options: { depth: 2 } }); + const depth0 = doc?.chainedRelation as EasierChained; + expect(depth0.id).toEqual(chained.id); + expect(depth0.relation.id).toEqual(chained2.id); + expect(depth0.relation.relation as unknown as string).toEqual(chained3.id); + expect(depth0.relation.relation).toEqual(chained3.id); + }); + + it('should only populate ID if depth 0', async () => { + const { doc } = await client.findByID({ id: post.id, options: { depth: 0 } }); + expect(doc?.chainedRelation).toEqual(chained.id); + }); + + it('should respect maxDepth at field level', async () => { + const { doc } = await client.findByID({ id: post.id, options: { depth: 1 } }); + expect(doc?.maxDepthRelation).toEqual(relation.id); + expect(doc?.maxDepthRelation).not.toHaveProperty('name'); + // should not affect other fields + expect(doc?.relationField).toMatchObject({ id: relation.id, name: relation.name }); + }); + + it('should query a custom id relation', async () => { + const { doc } = await client.findByID({ id: post.id }); + expect(doc?.customIdRelation).toMatchObject({ id: generatedCustomId }); + }); + }); + }); + }); +}); + +async function createPost(overrides?: Partial) { + return payload.create({ collection: slug, data: { title: 'title', ...overrides } }); +} + +async function clearDocs(): Promise { + const allDocs = await payload.find({ collection: slug, limit: 100 }); + const ids = allDocs.docs.map((doc) => doc.id); + await mapAsync(ids, async (id) => { + await payload.delete({ collection: slug, id }); + }); +} diff --git a/test/relationships/payload-types.ts b/test/relationships/payload-types.ts new file mode 100644 index 0000000000..ed7be8f9a2 --- /dev/null +++ b/test/relationships/payload-types.ts @@ -0,0 +1,83 @@ +/* tslint:disable */ +/** + * This file was automatically generated by Payload CMS. + * DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config, + * and re-run `payload generate:types` to regenerate this file. + */ + +export interface Config {} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "posts". + */ +export interface Post { + id: string; + title?: string; + description?: string; + number?: number; + relationField?: string | Relation; + defaultAccessRelation?: string | StrictAccess; + chainedRelation?: string | ChainedRelation; + maxDepthRelation?: string | Relation; + customIdRelation?: string | CustomIdRelation; + filteredRelation?: string | Relation; + createdAt: string; + updatedAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "relation". + */ +export interface Relation { + id: string; + name?: string; + disableRelation: boolean; + createdAt: string; + updatedAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "strict-access". + */ +export interface StrictAccess { + id: string; + name?: string; + disableRelation: boolean; + createdAt: string; + updatedAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "chained-relation". + */ +export interface ChainedRelation { + id: string; + name?: string; + relation?: string | ChainedRelation; + createdAt: string; + updatedAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "custom-id-relation". + */ +export interface CustomIdRelation { + id: string; + name?: string; + createdAt: string; + updatedAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "users". + */ +export interface User { + id: string; + email?: string; + resetPasswordToken?: string; + resetPasswordExpiration?: string; + loginAttempts?: number; + lockUntil?: string; + createdAt: string; + updatedAt: string; +}