Skip to content

Commit 17c49e6

Browse files
authored
feat: parse cv to profile (#3297)
1 parent f8c4192 commit 17c49e6

File tree

11 files changed

+569
-69
lines changed

11 files changed

+569
-69
lines changed

.infra/common.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,10 @@ export const workers: Worker[] = [
437437
topic: 'api.v1.recruiter-rejected-candidate-match',
438438
subscription: 'api.recruiter-rejected-candidate-match-email',
439439
},
440+
{
441+
topic: 'api.v1.candidate-preference-updated',
442+
subscription: 'api.parse-cv-profile',
443+
},
440444
];
441445

442446
export const personalizedDigestWorkers: Worker[] = [

__tests__/helpers.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,16 @@ import {
4646
ScreeningQuestionsResponse,
4747
BrokkrService,
4848
ExtractMarkdownResponse,
49+
ParseCVResponse,
4950
} from '@dailydotdev/schema';
5051
import { createClient, type ClickHouseClient } from '@clickhouse/client';
5152
import * as clickhouseCommon from '../src/common/clickhouse';
5253
import { Message as ProtobufMessage } from '@bufbuild/protobuf';
5354
import { GarmrService } from '../src/integrations/garmr';
55+
import { userExperienceCertificationFixture } from './fixture/profile/certification';
56+
import { userExperienceEducationFixture } from './fixture/profile/education';
57+
import { userExperienceProjectFixture } from './fixture/profile/project';
58+
import { userExperienceWorkFixture } from './fixture/profile/work';
5459

5560
export class MockContext extends Context {
5661
mockSpan: MockProxy<opentelemetry.Span> & opentelemetry.Span;
@@ -443,6 +448,20 @@ export const createMockBrokkrTransport = () =>
443448
content: `# Extracted content for ${request.blobName} in ${request.bucketName}`,
444449
});
445450
},
451+
parseCV: (request) => {
452+
if (request.blobName === 'empty-cv-mock') {
453+
return new ParseCVResponse({});
454+
}
455+
456+
return new ParseCVResponse({
457+
parsedCv: JSON.stringify([
458+
userExperienceCertificationFixture[0],
459+
userExperienceEducationFixture[0],
460+
userExperienceProjectFixture[0],
461+
userExperienceWorkFixture[0],
462+
]),
463+
});
464+
},
446465
});
447466
});
448467

Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
import {
2+
createGarmrMock,
3+
createMockBrokkrTransport,
4+
expectSuccessfulTypedBackground,
5+
saveFixtures,
6+
} from '../../helpers';
7+
import { DataSource } from 'typeorm';
8+
import createOrGetConnection from '../../../src/db';
9+
import { User } from '../../../src/entity/user/User';
10+
import { usersFixture } from '../../fixture/user';
11+
import { parseCVProfileWorker as worker } from '../../../src/workers/opportunity/parseCVProfile';
12+
import { BrokkrService, CandidatePreferenceUpdated } from '@dailydotdev/schema';
13+
import { createClient } from '@connectrpc/connect';
14+
import type { ServiceClient } from '../../../src/types';
15+
import * as brokkrCommon from '../../../src/common/brokkr';
16+
import { UserExperience } from '../../../src/entity/user/experiences/UserExperience';
17+
import { getSecondsTimestamp, updateFlagsStatement } from '../../../src/common';
18+
19+
let con: DataSource;
20+
21+
beforeAll(async () => {
22+
con = await createOrGetConnection();
23+
});
24+
25+
describe('parseCVProfile worker', () => {
26+
beforeEach(async () => {
27+
jest.resetAllMocks();
28+
29+
await saveFixtures(
30+
con,
31+
User,
32+
usersFixture.map((item) => {
33+
return {
34+
...item,
35+
id: `${item.id}-pcpw`,
36+
};
37+
}),
38+
);
39+
40+
const transport = createMockBrokkrTransport();
41+
42+
const serviceClient = {
43+
instance: createClient(BrokkrService, transport),
44+
garmr: createGarmrMock(),
45+
};
46+
47+
jest
48+
.spyOn(brokkrCommon, 'getBrokkrClient')
49+
.mockImplementation((): ServiceClient<typeof BrokkrService> => {
50+
return serviceClient;
51+
});
52+
});
53+
54+
it('should parse CV to profile', async () => {
55+
const userId = '1-pcpw';
56+
57+
const payload = new CandidatePreferenceUpdated({
58+
payload: {
59+
userId,
60+
cv: {
61+
blob: userId,
62+
bucket: 'bucket-test',
63+
lastModified: getSecondsTimestamp(new Date()),
64+
},
65+
},
66+
});
67+
68+
const parseCVSpy = jest.spyOn(
69+
brokkrCommon.getBrokkrClient().instance,
70+
'parseCV',
71+
);
72+
73+
await expectSuccessfulTypedBackground<'api.v1.candidate-preference-updated'>(
74+
worker,
75+
payload,
76+
);
77+
78+
expect(parseCVSpy).toHaveBeenCalledTimes(1);
79+
80+
const experiences = await con.getRepository(UserExperience).find({
81+
where: { userId },
82+
});
83+
84+
expect(experiences).toHaveLength(4);
85+
86+
const user = await con.getRepository(User).findOneBy({ id: userId });
87+
expect(user?.flags.lastCVParseAt).toBeDefined();
88+
});
89+
90+
it('should skip if CV blob or bucket is empty', async () => {
91+
const userId = '1-pcpw';
92+
93+
const payload = new CandidatePreferenceUpdated({
94+
payload: {
95+
userId,
96+
},
97+
});
98+
99+
const parseCVSpy = jest.spyOn(
100+
brokkrCommon.getBrokkrClient().instance,
101+
'parseCV',
102+
);
103+
104+
await expectSuccessfulTypedBackground<'api.v1.candidate-preference-updated'>(
105+
worker,
106+
payload,
107+
);
108+
109+
expect(parseCVSpy).toHaveBeenCalledTimes(0);
110+
111+
const experiences = await con.getRepository(UserExperience).find({
112+
where: { userId },
113+
});
114+
115+
expect(experiences).toHaveLength(0);
116+
});
117+
118+
it('should skip if CV lastModified is empty', async () => {
119+
const userId = '1-pcpw';
120+
121+
const payload = new CandidatePreferenceUpdated({
122+
payload: {
123+
userId,
124+
cv: {
125+
blob: userId,
126+
bucket: 'bucket-test',
127+
},
128+
},
129+
});
130+
131+
const parseCVSpy = jest.spyOn(
132+
brokkrCommon.getBrokkrClient().instance,
133+
'parseCV',
134+
);
135+
136+
await expectSuccessfulTypedBackground<'api.v1.candidate-preference-updated'>(
137+
worker,
138+
payload,
139+
);
140+
141+
expect(parseCVSpy).toHaveBeenCalledTimes(0);
142+
143+
const experiences = await con.getRepository(UserExperience).find({
144+
where: { userId },
145+
});
146+
147+
expect(experiences).toHaveLength(0);
148+
});
149+
150+
it('should skip if userId is empty', async () => {
151+
const userId = '1-pcpw';
152+
153+
const payload = new CandidatePreferenceUpdated({
154+
payload: {
155+
cv: {
156+
blob: userId,
157+
bucket: 'bucket-test',
158+
lastModified: getSecondsTimestamp(new Date()),
159+
},
160+
},
161+
});
162+
163+
const parseCVSpy = jest.spyOn(
164+
brokkrCommon.getBrokkrClient().instance,
165+
'parseCV',
166+
);
167+
168+
await expectSuccessfulTypedBackground<'api.v1.candidate-preference-updated'>(
169+
worker,
170+
payload,
171+
);
172+
173+
expect(parseCVSpy).toHaveBeenCalledTimes(0);
174+
175+
const experiences = await con.getRepository(UserExperience).find({
176+
where: { userId },
177+
});
178+
179+
expect(experiences).toHaveLength(0);
180+
});
181+
182+
it('should skip if user is not found', async () => {
183+
const userId = 'non-existing-user';
184+
185+
const payload = new CandidatePreferenceUpdated({
186+
payload: {
187+
userId,
188+
cv: {
189+
blob: userId,
190+
bucket: 'bucket-test',
191+
lastModified: getSecondsTimestamp(new Date()),
192+
},
193+
},
194+
});
195+
196+
const parseCVSpy = jest.spyOn(
197+
brokkrCommon.getBrokkrClient().instance,
198+
'parseCV',
199+
);
200+
201+
await expectSuccessfulTypedBackground<'api.v1.candidate-preference-updated'>(
202+
worker,
203+
payload,
204+
);
205+
206+
expect(parseCVSpy).toHaveBeenCalledTimes(0);
207+
208+
const experiences = await con.getRepository(UserExperience).find({
209+
where: { userId },
210+
});
211+
212+
expect(experiences).toHaveLength(0);
213+
});
214+
215+
it('should skip if lastModified is less then last profile parse date', async () => {
216+
const userId = '1-pcpw';
217+
218+
await con.getRepository(User).update(
219+
{ id: userId },
220+
{
221+
flags: updateFlagsStatement<User>({
222+
lastCVParseAt: new Date(Date.now() + 1 * 24 * 60 * 60 * 1000), // 1 day in future
223+
}),
224+
},
225+
);
226+
227+
const payload = new CandidatePreferenceUpdated({
228+
payload: {
229+
userId,
230+
cv: {
231+
blob: userId,
232+
bucket: 'bucket-test',
233+
lastModified: getSecondsTimestamp(new Date()),
234+
},
235+
},
236+
});
237+
238+
const parseCVSpy = jest.spyOn(
239+
brokkrCommon.getBrokkrClient().instance,
240+
'parseCV',
241+
);
242+
243+
await expectSuccessfulTypedBackground<'api.v1.candidate-preference-updated'>(
244+
worker,
245+
payload,
246+
);
247+
248+
expect(parseCVSpy).toHaveBeenCalledTimes(0);
249+
250+
const experiences = await con.getRepository(UserExperience).find({
251+
where: { userId },
252+
});
253+
254+
expect(experiences).toHaveLength(0);
255+
});
256+
257+
it('should fail if parsedCV in result is empty', async () => {
258+
const userId = '1-pcpw';
259+
260+
const payload = new CandidatePreferenceUpdated({
261+
payload: {
262+
userId,
263+
cv: {
264+
blob: 'empty-cv-mock',
265+
bucket: 'bucket-test',
266+
lastModified: getSecondsTimestamp(new Date()),
267+
},
268+
},
269+
});
270+
271+
const parseCVSpy = jest.spyOn(
272+
brokkrCommon.getBrokkrClient().instance,
273+
'parseCV',
274+
);
275+
276+
await expectSuccessfulTypedBackground<'api.v1.candidate-preference-updated'>(
277+
worker,
278+
payload,
279+
);
280+
281+
expect(parseCVSpy).toHaveBeenCalledTimes(1);
282+
283+
const experiences = await con.getRepository(UserExperience).find({
284+
where: { userId },
285+
});
286+
287+
expect(experiences).toHaveLength(0);
288+
289+
const user = await con.getRepository(User).findOneBy({ id: userId });
290+
expect(user?.flags.lastCVParseAt).toBeNull();
291+
});
292+
293+
it('should revert date of profile parse if parsing fails', async () => {
294+
const userId = '1-pcpw';
295+
296+
const parseDate = new Date('2024-01-01T00:00:00Z');
297+
298+
await con.getRepository(User).update(
299+
{ id: userId },
300+
{
301+
flags: updateFlagsStatement<User>({
302+
lastCVParseAt: parseDate,
303+
}),
304+
},
305+
);
306+
307+
const payload = new CandidatePreferenceUpdated({
308+
payload: {
309+
userId,
310+
cv: {
311+
blob: 'empty-cv-mock',
312+
bucket: 'bucket-test',
313+
lastModified: getSecondsTimestamp(new Date()),
314+
},
315+
},
316+
});
317+
318+
const parseCVSpy = jest.spyOn(
319+
brokkrCommon.getBrokkrClient().instance,
320+
'parseCV',
321+
);
322+
323+
await expectSuccessfulTypedBackground<'api.v1.candidate-preference-updated'>(
324+
worker,
325+
payload,
326+
);
327+
328+
expect(parseCVSpy).toHaveBeenCalledTimes(1);
329+
330+
const user = await con.getRepository(User).findOneBy({ id: userId });
331+
expect(user?.flags.lastCVParseAt).toBe(parseDate.toISOString());
332+
});
333+
});

0 commit comments

Comments
 (0)