Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .infra/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,10 @@ export const workers: Worker[] = [
topic: 'api.v1.recruiter-rejected-candidate-match',
subscription: 'api.recruiter-rejected-candidate-match-email',
},
{
topic: 'api.v1.candidate-preference-updated',
subscription: 'api.parse-cv-profile',
},
];

export const personalizedDigestWorkers: Worker[] = [
Expand Down
19 changes: 19 additions & 0 deletions __tests__/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,16 @@ import {
ScreeningQuestionsResponse,
BrokkrService,
ExtractMarkdownResponse,
ParseCVResponse,
} from '@dailydotdev/schema';
import { createClient, type ClickHouseClient } from '@clickhouse/client';
import * as clickhouseCommon from '../src/common/clickhouse';
import { Message as ProtobufMessage } from '@bufbuild/protobuf';
import { GarmrService } from '../src/integrations/garmr';
import { userExperienceCertificationFixture } from './fixture/profile/certification';
import { userExperienceEducationFixture } from './fixture/profile/education';
import { userExperienceProjectFixture } from './fixture/profile/project';
import { userExperienceWorkFixture } from './fixture/profile/work';

export class MockContext extends Context {
mockSpan: MockProxy<opentelemetry.Span> & opentelemetry.Span;
Expand Down Expand Up @@ -443,6 +448,20 @@ export const createMockBrokkrTransport = () =>
content: `# Extracted content for ${request.blobName} in ${request.bucketName}`,
});
},
parseCV: (request) => {
if (request.blobName === 'empty-cv-mock') {
return new ParseCVResponse({});
}

return new ParseCVResponse({
parsedCv: JSON.stringify([
userExperienceCertificationFixture[0],
userExperienceEducationFixture[0],
userExperienceProjectFixture[0],
userExperienceWorkFixture[0],
]),
});
},
});
});

Expand Down
333 changes: 333 additions & 0 deletions __tests__/workers/opportunity/parseCVProfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,333 @@
import {
createGarmrMock,
createMockBrokkrTransport,
expectSuccessfulTypedBackground,
saveFixtures,
} from '../../helpers';
import { DataSource } from 'typeorm';
import createOrGetConnection from '../../../src/db';
import { User } from '../../../src/entity/user/User';
import { usersFixture } from '../../fixture/user';
import { parseCVProfileWorker as worker } from '../../../src/workers/opportunity/parseCVProfile';
import { BrokkrService, CandidatePreferenceUpdated } from '@dailydotdev/schema';
import { createClient } from '@connectrpc/connect';
import type { ServiceClient } from '../../../src/types';
import * as brokkrCommon from '../../../src/common/brokkr';
import { UserExperience } from '../../../src/entity/user/experiences/UserExperience';
import { getSecondsTimestamp, updateFlagsStatement } from '../../../src/common';

let con: DataSource;

beforeAll(async () => {
con = await createOrGetConnection();
});

describe('parseCVProfile worker', () => {
beforeEach(async () => {
jest.resetAllMocks();

await saveFixtures(
con,
User,
usersFixture.map((item) => {
return {
...item,
id: `${item.id}-pcpw`,
};
}),
);

const transport = createMockBrokkrTransport();

const serviceClient = {
instance: createClient(BrokkrService, transport),
garmr: createGarmrMock(),
};

jest
.spyOn(brokkrCommon, 'getBrokkrClient')
.mockImplementation((): ServiceClient<typeof BrokkrService> => {
return serviceClient;
});
});

it('should parse CV to profile', async () => {
const userId = '1-pcpw';

const payload = new CandidatePreferenceUpdated({
payload: {
userId,
cv: {
blob: userId,
bucket: 'bucket-test',
lastModified: getSecondsTimestamp(new Date()),
},
},
});

const parseCVSpy = jest.spyOn(
brokkrCommon.getBrokkrClient().instance,
'parseCV',
);

await expectSuccessfulTypedBackground<'api.v1.candidate-preference-updated'>(
worker,
payload,
);

expect(parseCVSpy).toHaveBeenCalledTimes(1);

const experiences = await con.getRepository(UserExperience).find({
where: { userId },
});

expect(experiences).toHaveLength(4);

const user = await con.getRepository(User).findOneBy({ id: userId });
expect(user?.flags.lastCVParseAt).toBeDefined();
});

it('should skip if CV blob or bucket is empty', async () => {
const userId = '1-pcpw';

const payload = new CandidatePreferenceUpdated({
payload: {
userId,
},
});

const parseCVSpy = jest.spyOn(
brokkrCommon.getBrokkrClient().instance,
'parseCV',
);

await expectSuccessfulTypedBackground<'api.v1.candidate-preference-updated'>(
worker,
payload,
);

expect(parseCVSpy).toHaveBeenCalledTimes(0);

const experiences = await con.getRepository(UserExperience).find({
where: { userId },
});

expect(experiences).toHaveLength(0);
});

it('should skip if CV lastModified is empty', async () => {
const userId = '1-pcpw';

const payload = new CandidatePreferenceUpdated({
payload: {
userId,
cv: {
blob: userId,
bucket: 'bucket-test',
},
},
});

const parseCVSpy = jest.spyOn(
brokkrCommon.getBrokkrClient().instance,
'parseCV',
);

await expectSuccessfulTypedBackground<'api.v1.candidate-preference-updated'>(
worker,
payload,
);

expect(parseCVSpy).toHaveBeenCalledTimes(0);

const experiences = await con.getRepository(UserExperience).find({
where: { userId },
});

expect(experiences).toHaveLength(0);
});

it('should skip if userId is empty', async () => {
const userId = '1-pcpw';

const payload = new CandidatePreferenceUpdated({
payload: {
cv: {
blob: userId,
bucket: 'bucket-test',
lastModified: getSecondsTimestamp(new Date()),
},
},
});

const parseCVSpy = jest.spyOn(
brokkrCommon.getBrokkrClient().instance,
'parseCV',
);

await expectSuccessfulTypedBackground<'api.v1.candidate-preference-updated'>(
worker,
payload,
);

expect(parseCVSpy).toHaveBeenCalledTimes(0);

const experiences = await con.getRepository(UserExperience).find({
where: { userId },
});

expect(experiences).toHaveLength(0);
});

it('should skip if user is not found', async () => {
const userId = 'non-existing-user';

const payload = new CandidatePreferenceUpdated({
payload: {
userId,
cv: {
blob: userId,
bucket: 'bucket-test',
lastModified: getSecondsTimestamp(new Date()),
},
},
});

const parseCVSpy = jest.spyOn(
brokkrCommon.getBrokkrClient().instance,
'parseCV',
);

await expectSuccessfulTypedBackground<'api.v1.candidate-preference-updated'>(
worker,
payload,
);

expect(parseCVSpy).toHaveBeenCalledTimes(0);

const experiences = await con.getRepository(UserExperience).find({
where: { userId },
});

expect(experiences).toHaveLength(0);
});

it('should skip if lastModified is less then last profile parse date', async () => {
const userId = '1-pcpw';

await con.getRepository(User).update(
{ id: userId },
{
flags: updateFlagsStatement<User>({
lastCVParseAt: new Date(Date.now() + 1 * 24 * 60 * 60 * 1000), // 1 day in future
}),
},
);

const payload = new CandidatePreferenceUpdated({
payload: {
userId,
cv: {
blob: userId,
bucket: 'bucket-test',
lastModified: getSecondsTimestamp(new Date()),
},
},
});

const parseCVSpy = jest.spyOn(
brokkrCommon.getBrokkrClient().instance,
'parseCV',
);

await expectSuccessfulTypedBackground<'api.v1.candidate-preference-updated'>(
worker,
payload,
);

expect(parseCVSpy).toHaveBeenCalledTimes(0);

const experiences = await con.getRepository(UserExperience).find({
where: { userId },
});

expect(experiences).toHaveLength(0);
});

it('should fail if parsedCV in result is empty', async () => {
const userId = '1-pcpw';

const payload = new CandidatePreferenceUpdated({
payload: {
userId,
cv: {
blob: 'empty-cv-mock',
bucket: 'bucket-test',
lastModified: getSecondsTimestamp(new Date()),
},
},
});

const parseCVSpy = jest.spyOn(
brokkrCommon.getBrokkrClient().instance,
'parseCV',
);

await expectSuccessfulTypedBackground<'api.v1.candidate-preference-updated'>(
worker,
payload,
);

expect(parseCVSpy).toHaveBeenCalledTimes(1);

const experiences = await con.getRepository(UserExperience).find({
where: { userId },
});

expect(experiences).toHaveLength(0);

const user = await con.getRepository(User).findOneBy({ id: userId });
expect(user?.flags.lastCVParseAt).toBeNull();
});

it('should revert date of profile parse if parsing fails', async () => {
const userId = '1-pcpw';

const parseDate = new Date('2024-01-01T00:00:00Z');

await con.getRepository(User).update(
{ id: userId },
{
flags: updateFlagsStatement<User>({
lastCVParseAt: parseDate,
}),
},
);

const payload = new CandidatePreferenceUpdated({
payload: {
userId,
cv: {
blob: 'empty-cv-mock',
bucket: 'bucket-test',
lastModified: getSecondsTimestamp(new Date()),
},
},
});

const parseCVSpy = jest.spyOn(
brokkrCommon.getBrokkrClient().instance,
'parseCV',
);

await expectSuccessfulTypedBackground<'api.v1.candidate-preference-updated'>(
worker,
payload,
);

expect(parseCVSpy).toHaveBeenCalledTimes(1);

const user = await con.getRepository(User).findOneBy({ id: userId });
expect(user?.flags.lastCVParseAt).toBe(parseDate.toISOString());
});
});
Loading
Loading