From 2d2c56bbd16c470c73a104a76ed2d46786e6d1c6 Mon Sep 17 00:00:00 2001 From: capJavert Date: Wed, 10 Dec 2025 10:38:53 +0100 Subject: [PATCH] feat: parse opportunity for logged in users --- __tests__/schema/opportunity.ts | 120 +++++++++++++++++++++++++++++--- src/schema/opportunity.ts | 25 +++++-- 2 files changed, 130 insertions(+), 15 deletions(-) diff --git a/__tests__/schema/opportunity.ts b/__tests__/schema/opportunity.ts index 5d5a05c416..f4d24becc5 100644 --- a/__tests__/schema/opportunity.ts +++ b/__tests__/schema/opportunity.ts @@ -5543,22 +5543,122 @@ describe('mutation parseOpportunity', () => { expect(body.errors[0].message).toBe('File type not supported'); }); - it('should not allow authenticated users to parse opportunity', async () => { + it('should parse opportunity for authenticated user', async () => { loggedUser = '1'; - const res = await client.mutate(MUTATION, { - variables: { - payload: { - url: 'https://example.com/opportunity', + trackingId = 'anon1'; + + fileTypeFromBuffer.mockResolvedValue({ + ext: 'pdf', + mime: 'application/pdf', + }); + + const uploadResumeFromBufferSpy = jest.spyOn( + googleCloud, + 'uploadResumeFromBuffer', + ); + + uploadResumeFromBufferSpy.mockResolvedValue( + `https://storage.cloud.google.com/${RESUME_BUCKET_NAME}/file`, + ); + + const deleteFileFromBucketSpy = jest.spyOn( + googleCloud, + 'deleteFileFromBucket', + ); + + deleteFileFromBucketSpy.mockResolvedValue(true); + + // Execute the mutation with a file upload + const res = await authorizeRequest( + request(app.server) + .post('/graphql') + .field( + 'operations', + JSON.stringify({ + query: MUTATION, + variables: { + payload: { + file: null, + }, + }, + }), + ) + .field('map', JSON.stringify({ '0': ['variables.payload.file'] })) + .attach('0', './__tests__/fixture/screen.pdf'), + ).expect(200); + + const body = res.body; + expect(body.errors).toBeFalsy(); + + expect(body.data.parseOpportunity).toMatchObject({ + title: 'Mocked Opportunity Title', + tldr: 'This is a mocked TL;DR of the opportunity.', + keywords: [ + { keyword: 'mock' }, + { keyword: 'opportunity' }, + { keyword: 'test' }, + ], + meta: { + employmentType: EmploymentType.FULL_TIME, + seniorityLevel: SeniorityLevel.SENIOR, + roleType: RoleType.Auto, + salary: { + min: 1000, + max: 2000, + period: SalaryPeriod.MONTHLY, }, }, + content: { + overview: { + content: 'This is the overview of the mocked opportunity.', + html: '

This is the overview of the mocked opportunity.

\n', + }, + responsibilities: { + content: 'These are the responsibilities of the mocked opportunity.', + html: '

These are the responsibilities of the mocked opportunity.

\n', + }, + requirements: { + content: 'These are the requirements of the mocked opportunity.', + html: '

These are the requirements of the mocked opportunity.

\n', + }, + }, + location: [ + { + city: 'San Francisco', + country: 'USA', + subdivision: 'CA', + type: LocationType.REMOTE, + }, + ], + questions: [], + feedbackQuestions: [ + { + title: 'Why did you reject this opportunity?', + placeholder: `E.g., Not interested in the tech stack, location doesn't work for me, compensation too low...`, + }, + ], }); - expect(res.errors).toBeDefined(); - expect(res.errors?.[0].extensions.code).toBe('FORBIDDEN'); - expect(res.errors?.[0].message).toBe( - 'Not available for authenticated users yet', - ); + const opportunity = await con.getRepository(OpportunityJob).findOne({ + where: { + id: body.data.parseOpportunity.id, + }, + }); + + expect(opportunity).toBeDefined(); + expect(opportunity!.state).toBe(OpportunityState.DRAFT); + + const opportunityRecruiter = await con + .getRepository(OpportunityUserRecruiter) + .findOne({ + where: { + opportunityId: body.data.parseOpportunity.id, + userId: loggedUser, + }, + }); + + expect(opportunityRecruiter).toBeDefined(); }); }); diff --git a/src/schema/opportunity.ts b/src/schema/opportunity.ts index 499c06fc24..d1b9b381af 100644 --- a/src/schema/opportunity.ts +++ b/src/schema/opportunity.ts @@ -2077,8 +2077,8 @@ export const resolvers: IResolvers = traceResolvers< ctx: Context, info, ): Promise => { - if (ctx.userId) { - throw new ForbiddenError('Not available for authenticated users yet'); + if (!(ctx.userId || ctx.trackingId)) { + throw new ValidationError('User identifier is required'); } const parseOpportunityPayload = @@ -2175,6 +2175,12 @@ export const resolvers: IResolvers = traceResolvers< const opportunityResult = await ctx.con.transaction( async (entityManager) => { + const flags: Opportunity['flags'] = {}; + + if (!ctx.userId) { + flags.anonUserId = ctx.trackingId; // save tracking id to attribute later + } + const opportunity = await entityManager .getRepository(OpportunityJob) .save( @@ -2182,9 +2188,7 @@ export const resolvers: IResolvers = traceResolvers< ...parsedOpportunity, state: OpportunityState.DRAFT, content: opportunityContent, - flags: { - anonUserId: ctx.trackingId, // save tracking id to attribute later - }, + flags, } as DeepPartial), ); @@ -2200,6 +2204,17 @@ export const resolvers: IResolvers = traceResolvers< })), ); + if (ctx.userId) { + await entityManager + .getRepository(OpportunityUserRecruiter) + .insert( + entityManager.getRepository(OpportunityUserRecruiter).create({ + opportunityId: opportunity.id, + userId: ctx.userId, + }), + ); + } + return opportunity; }, );