diff --git a/servers/curated-corpus-api/src/admin/resolvers/mutations/Section/createSection.integration.ts b/servers/curated-corpus-api/src/admin/resolvers/mutations/Section/createSection.integration.ts new file mode 100644 index 00000000..f529edbc --- /dev/null +++ b/servers/curated-corpus-api/src/admin/resolvers/mutations/Section/createSection.integration.ts @@ -0,0 +1,200 @@ +import { print } from 'graphql'; +import request from 'supertest'; + +import { ApolloServer } from '@apollo/server'; +import { PrismaClient, Section, SectionItem } from '.prisma/client'; + +import { ActivitySource, CreateSectionApiInput, ScheduledSurfacesEnum } from 'content-common'; + + +import { client } from '../../../../database/client'; +import { ApprovedItem } from '../../../../database/types'; + +import { + clearDb, + createSectionHelper, + createSectionItemHelper, + createApprovedItemHelper, +} from '../../../../test/helpers'; + +import { CREATE_SECTION } from '../sample-mutations.gql'; +import { MozillaAccessGroup } from 'content-common'; +import { startServer } from '../../../../express'; +import { IAdminContext } from '../../../context'; + +describe('mutations: Section (createSection)', () => { + let app: Express.Application; + let db: PrismaClient; + let graphQLUrl: string; + let input: CreateSectionApiInput; + let server: ApolloServer; + let section: Section; + let sectionItem: SectionItem; + let approvedItem: ApprovedItem; + + const headers = { + name: 'Test User', + username: 'test.user@test.com', + groups: `group1,group2,${MozillaAccessGroup.SCHEDULED_SURFACE_CURATOR_FULL}`, + }; + + beforeAll(async () => { + // port 0 tells express to dynamically assign an available port + ({ app, adminServer: server, adminUrl: graphQLUrl } = await startServer(0)); + db = client(); + }); + + afterAll(async () => { + await server.stop(); + await clearDb(db); + await db.$disconnect(); + }); + + beforeEach(async () => { + // we need an ApprovedItem to create a SectionItem + section = await createSectionHelper(db, { + externalId: 'bcg-456', + createSource: ActivitySource.ML, + }); + + approvedItem = await createApprovedItemHelper(db, { + title: '10 Reasons You Should Quit Social Media', + }); + + sectionItem = await createSectionItemHelper(db, { + approvedItemId: approvedItem.id, + sectionId: section.id, + rank: 1 + }); + }); + + afterEach(async () => { + await clearDb(db); + }); + + it('should create a Section if user has full access', async () => { + input = { + externalId: '123-abc', + title: 'Fake Section Title', + scheduledSurfaceGuid: ScheduledSurfacesEnum.NEW_TAB_EN_US, + sort: 1, + createSource: ActivitySource.ML, + active: true + }; + + const result = await request(app) + .post(graphQLUrl) + .set(headers) + .send({ + query: print(CREATE_SECTION), + variables: { data: input }, + }); + + expect(result.body.errors).toBeUndefined(); + expect(result.body.data).not.toBeNull(); + + // Expect all fields to be set correctly + expect(result.body.data?.createSection.externalId).toEqual('123-abc'); + expect(result.body.data?.createSection.title).toEqual('Fake Section Title'); + expect(result.body.data?.createSection.scheduledSurfaceGuid).toEqual('NEW_TAB_EN_US'); + expect(result.body.data?.createSection.sort).toEqual(1); + expect(result.body.data?.createSection.createSource).toEqual('ML'); + expect(result.body.data?.createSection.active).toBeTruthy(); + }); + + it('should create a Section without optional properties', async () => { + // `sort` is the only optional property - omitting below + input = { + externalId: '321-xyz', + title: 'Fake Section Title', + scheduledSurfaceGuid: ScheduledSurfacesEnum.NEW_TAB_EN_US, + createSource: ActivitySource.ML, + active: true + }; + + const result = await request(app) + .post(graphQLUrl) + .set(headers) + .send({ + query: print(CREATE_SECTION), + variables: { data: input }, + }); + + expect(result.body.errors).toBeUndefined(); + expect(result.body.data).not.toBeNull(); + + // sort should be null + expect(result.body.data?.createSection.sort).toBeNull(); + + // Expect all other fields to be set correctly + expect(result.body.data?.createSection.externalId).toEqual('321-xyz'); + expect(result.body.data?.createSection.title).toEqual('Fake Section Title'); + expect(result.body.data?.createSection.scheduledSurfaceGuid).toEqual('NEW_TAB_EN_US'); + expect(result.body.data?.createSection.createSource).toEqual('ML'); + expect(result.body.data?.createSection.active).toBeTruthy(); + }); + + it('should update an existing Section & mark any associated SectionItems in-active', async () => { + input = { + externalId: 'bcg-456', + title: 'Updating Fake Section Title', + scheduledSurfaceGuid: ScheduledSurfacesEnum.NEW_TAB_EN_US, + createSource: ActivitySource.ML, + sort: 2, + active: true + }; + + const result = await request(app) + .post(graphQLUrl) + .set(headers) + .send({ + query: print(CREATE_SECTION), + variables: { data: input }, + }); + + expect(result.body.errors).toBeUndefined(); + expect(result.body.data).not.toBeNull(); + + // Expect all fields to be set correctly + expect(result.body.data?.createSection.externalId).toEqual('bcg-456'); + expect(result.body.data?.createSection.title).toEqual('Updating Fake Section Title'); + expect(result.body.data?.createSection.scheduledSurfaceGuid).toEqual('NEW_TAB_EN_US'); + expect(result.body.data?.createSection.sort).toEqual(2); + expect(result.body.data?.createSection.createSource).toEqual('ML'); + expect(result.body.data?.createSection.active).toBeTruthy(); + + const inactiveSectioinItem = await db.sectionItem.findUnique({ + where: {externalId: sectionItem.externalId} + }); + // Expect associated section item to be in-active now + expect(inactiveSectioinItem.active).toBeFalsy(); + }); + + it('should fail to create a Section if createSource is not ML', async () => { + input = { + externalId: 'bcg-456', + title: 'Updating Fake Section Title', + scheduledSurfaceGuid: ScheduledSurfacesEnum.NEW_TAB_EN_US, + createSource: ActivitySource.MANUAL, + sort: 2, + active: true + }; + + const result = await request(app) + .post(graphQLUrl) + .set(headers) + .send({ + query: print(CREATE_SECTION), + variables: { data: input }, + }); + + // we should have a UserInputError + expect(result.body.errors).not.toBeUndefined(); + expect(result.body.errors?.[0].extensions?.code).toEqual('BAD_USER_INPUT'); + + // check the error message + expect(result.body.errors?.[0].message).toContain( + "Cannot create a Section: createSource must be ML", + ); + }); +}); \ No newline at end of file diff --git a/servers/curated-corpus-api/src/admin/resolvers/mutations/Section/index.ts b/servers/curated-corpus-api/src/admin/resolvers/mutations/Section/index.ts index c6ba128f..fe39d142 100644 --- a/servers/curated-corpus-api/src/admin/resolvers/mutations/Section/index.ts +++ b/servers/curated-corpus-api/src/admin/resolvers/mutations/Section/index.ts @@ -1,4 +1,4 @@ -import { AuthenticationError, NotFoundError, UserInputError } from '@pocket-tools/apollo-utils'; +import { AuthenticationError, UserInputError } from '@pocket-tools/apollo-utils'; import { createSection as dbCreateSection, diff --git a/servers/curated-corpus-api/src/admin/resolvers/mutations/SectionItem/createSectionItem.integration.ts b/servers/curated-corpus-api/src/admin/resolvers/mutations/SectionItem/createSectionItem.integration.ts index 97e9ca87..27f2ef9c 100644 --- a/servers/curated-corpus-api/src/admin/resolvers/mutations/SectionItem/createSectionItem.integration.ts +++ b/servers/curated-corpus-api/src/admin/resolvers/mutations/SectionItem/createSectionItem.integration.ts @@ -4,7 +4,7 @@ import request from 'supertest'; import { ApolloServer } from '@apollo/server'; import { PrismaClient, Section } from '.prisma/client'; -import { ScheduledItemSource, CreateSectionItemApiInput } from 'content-common'; +import { ActivitySource, CreateSectionItemApiInput } from 'content-common'; import { client } from '../../../../database/client'; import { ApprovedItem } from '../../../../database/types'; @@ -49,7 +49,7 @@ describe('mutations: SectionItem (createSectionItem)', () => { beforeEach(async () => { // we need a Section and an ApprovedItem to create a SectionItem section = await createSectionHelper(db, { - createSource: ScheduledItemSource.ML, + createSource: ActivitySource.ML, }); approvedItem = await createApprovedItemHelper(db, { diff --git a/servers/curated-corpus-api/src/database/mutations/Section.integration.ts b/servers/curated-corpus-api/src/database/mutations/Section.integration.ts new file mode 100644 index 00000000..50e8883f --- /dev/null +++ b/servers/curated-corpus-api/src/database/mutations/Section.integration.ts @@ -0,0 +1,78 @@ +import { PrismaClient } from '.prisma/client'; + +import { client } from '../client'; +import { clearDb, createApprovedItemHelper } from '../../test/helpers'; +import { createSection, updateSection } from './Section'; +import { createSectionHelper, createSectionItemHelper } from '../../test/helpers'; +import { ActivitySource, ScheduledSurfacesEnum } from 'content-common'; + +describe('Section', () => { + let db: PrismaClient; + + beforeAll(async () => { + db = client(); + await clearDb(db); + }); + + afterAll(async () => { + await db.$disconnect(); + }); + + beforeEach(async () => { + await clearDb(db); + }); + + describe('createSection', () => { + it('should create a Section', async () => { + const input = { + externalId: 'njh-789', + title: 'Fake Section Title', + scheduledSurfaceGuid: ScheduledSurfacesEnum.NEW_TAB_EN_US, + createSource: ActivitySource.MANUAL, + sort: 2, + active: true + }; + + const result = await createSection(db, input); + + expect(result.externalId).toEqual('njh-789'); + }); + }); + + describe('updateSection', () => { + it('should update a Section & mark any associated SectionItems in-active', async () => { + const approvedItem = await createApprovedItemHelper(db, { + title: 'Fake Item!', + }); + + const section = await createSectionHelper(db, {externalId: 'oiueh-123', title: 'New Title'}); + + const sectionItem = await createSectionItemHelper(db, { + approvedItemId: approvedItem.id, + sectionId: section.id, + rank: 1 + }); + + const input = { + externalId: 'oiueh-123', + title: 'Updating new title', + scheduledSurfaceGuid: ScheduledSurfacesEnum.NEW_TAB_EN_US, + createSource: ActivitySource.MANUAL, + sort: 3, + active: true + }; + + const result = await updateSection(db, input); + + expect(result.externalId).toEqual('oiueh-123'); + expect(result.title).toEqual('Updating new title'); + expect(result.sort).toEqual(3); + + const inactiveSectioinItem = await db.sectionItem.findUnique({ + where: {externalId: sectionItem.externalId} + }); + // Expect associated section item to be in-active now + expect(inactiveSectioinItem.active).toBeFalsy(); + }); + }); +}); diff --git a/servers/curated-corpus-api/src/database/mutations/SectionItem.integration.ts b/servers/curated-corpus-api/src/database/mutations/SectionItem.integration.ts index 347d7705..ee4323bc 100644 --- a/servers/curated-corpus-api/src/database/mutations/SectionItem.integration.ts +++ b/servers/curated-corpus-api/src/database/mutations/SectionItem.integration.ts @@ -6,7 +6,7 @@ import { createSectionItem } from './SectionItem'; import { createApprovedItemHelper } from '../../test/helpers'; import { createSectionHelper } from '../../test/helpers'; -describe('ScheduleReview', () => { +describe('SectionItem', () => { let db: PrismaClient; beforeAll(async () => { diff --git a/servers/curated-corpus-api/src/shared/fragments.gql.ts b/servers/curated-corpus-api/src/shared/fragments.gql.ts index a9acd3f6..3cfb81da 100644 --- a/servers/curated-corpus-api/src/shared/fragments.gql.ts +++ b/servers/curated-corpus-api/src/shared/fragments.gql.ts @@ -66,7 +66,7 @@ export const SectionData = gql` title scheduledSurfaceGuid sort - source + createSource active createdAt updatedAt diff --git a/servers/curated-corpus-api/src/test/helpers/createSectionHelper.integration.ts b/servers/curated-corpus-api/src/test/helpers/createSectionHelper.integration.ts index b7d24210..2bc38b36 100644 --- a/servers/curated-corpus-api/src/test/helpers/createSectionHelper.integration.ts +++ b/servers/curated-corpus-api/src/test/helpers/createSectionHelper.integration.ts @@ -1,6 +1,6 @@ import { PrismaClient, Section } from '.prisma/client'; -import { ScheduledItemSource, ScheduledSurfacesEnum } from 'content-common'; +import { ActivitySource, ScheduledSurfacesEnum } from 'content-common'; import { clearDb } from './clearDb'; import { @@ -30,7 +30,7 @@ describe('createSectionHelper', () => { it('should create a Section with all props supplied', async () => { const data: CreateSectionHelperOptionalInput = { - createSource: ScheduledItemSource.ML, + createSource: ActivitySource.ML, externalId: 'AnExternalIdFromML', scheduledSurfaceGuid: ScheduledSurfacesEnum.NEW_TAB_EN_US, title: 'How to Build Community', diff --git a/servers/curated-corpus-api/src/test/helpers/createSectionHelper.ts b/servers/curated-corpus-api/src/test/helpers/createSectionHelper.ts index ea7241ea..bf3f24a8 100644 --- a/servers/curated-corpus-api/src/test/helpers/createSectionHelper.ts +++ b/servers/curated-corpus-api/src/test/helpers/createSectionHelper.ts @@ -1,11 +1,11 @@ import { Prisma, PrismaClient, Section } from '.prisma/client'; import { faker } from '@faker-js/faker'; -import { ScheduledSurfacesEnum, ScheduledItemSource } from 'content-common'; +import { ScheduledSurfacesEnum, ActivitySource } from 'content-common'; // optional information you can provide when creating an section export interface CreateSectionHelperOptionalInput { - createSource?: ScheduledItemSource; + createSource?: ActivitySource; // externalId can be provided, but if not will be generated by prisma on insert externalId?: string; scheduledSurfaceGuid?: ScheduledSurfacesEnum; @@ -28,8 +28,8 @@ export async function createSectionHelper( ): Promise
{ const createSectionDefaults = { createSource: faker.helpers.arrayElement([ - ScheduledItemSource.MANUAL, - ScheduledItemSource.ML, + ActivitySource.MANUAL, + ActivitySource.ML, ]), scheduledSurfaceGuid: faker.helpers.arrayElement(scheduledSurfaceGuids), title: faker.lorem.sentence(10), diff --git a/servers/curated-corpus-api/src/test/helpers/createSectionItemHelper.integration.ts b/servers/curated-corpus-api/src/test/helpers/createSectionItemHelper.integration.ts new file mode 100644 index 00000000..e6de763b --- /dev/null +++ b/servers/curated-corpus-api/src/test/helpers/createSectionItemHelper.integration.ts @@ -0,0 +1,60 @@ +import { PrismaClient, Section, ApprovedItem, SectionItem } from '.prisma/client'; + +import { ActivitySource } from 'content-common'; + +import { clearDb } from './clearDb'; +import { + createSectionItemHelper, + CreateSectionItemHelperInput, +} from './createSectionItemHelper'; +import { createSectionHelper } from './createSectionHelper' +import { createApprovedItemHelper } from './createApprovedItemHelper'; + +const db = new PrismaClient(); +let section: Section; +let approvedItem: ApprovedItem; + +describe('createSectionItemHelper', () => { + beforeEach(async () => { + await clearDb(db); + section = await createSectionHelper(db, { + externalId: 'bcg-456', + createSource: ActivitySource.ML, + }); + approvedItem = await createApprovedItemHelper(db, { + title: '10 Reasons You Should Quit Social Media', + }); + }); + + afterAll(async () => { + await clearDb(db); + await db.$disconnect(); + }); + + it('should create a SectionItem with no rank supplied', async () => { + const data: CreateSectionItemHelperInput = { + approvedItemId: approvedItem.id, + sectionId: section.id, + }; + + const sectionItem: SectionItem = await createSectionItemHelper(db, data); + + // just make sure a record was created + expect(sectionItem.externalId).not.toBeFalsy(); + }); + + it('should create a SectionItem with all props supplied', async () => { + const data: CreateSectionItemHelperInput = { + approvedItemId: approvedItem.id, + sectionId: section.id, + rank: 1 + }; + + const sectionItem: SectionItem = await createSectionItemHelper(db, data); + + // make sure props specified make it to the db + expect(sectionItem.approvedItemId).toEqual(approvedItem.id); + expect(sectionItem.sectionId).toEqual(section.id); + expect(sectionItem.rank).toEqual(data.rank); + }); +}); diff --git a/servers/curated-corpus-api/src/test/helpers/createSectionItemHelper.ts b/servers/curated-corpus-api/src/test/helpers/createSectionItemHelper.ts new file mode 100644 index 00000000..303e3a3b --- /dev/null +++ b/servers/curated-corpus-api/src/test/helpers/createSectionItemHelper.ts @@ -0,0 +1,22 @@ +import { PrismaClient, SectionItem } from '.prisma/client'; + +export interface CreateSectionItemHelperInput { + approvedItemId: number, + sectionId: number, + rank?: number +} + +/** + * A helper function that creates a sample section item for testing or local development. + * @param prisma + * @param data + */ +export async function createSectionItemHelper( + prisma: PrismaClient, + data: CreateSectionItemHelperInput, +): Promise { + + return await prisma.sectionItem.create({ + data: data, + }); +} \ No newline at end of file diff --git a/servers/curated-corpus-api/src/test/helpers/index.ts b/servers/curated-corpus-api/src/test/helpers/index.ts index 537f3357..74bb25b8 100644 --- a/servers/curated-corpus-api/src/test/helpers/index.ts +++ b/servers/curated-corpus-api/src/test/helpers/index.ts @@ -5,3 +5,4 @@ export { createScheduledItemHelper } from './createScheduledItemHelper'; export { createRejectedCuratedCorpusItemHelper } from './createRejectedCuratedCorpusItemHelper'; export { createScheduleReviewHelper } from './createScheduleReviewHelper'; export { createSectionHelper } from './createSectionHelper'; +export { createSectionItemHelper } from './createSectionItemHelper';