Skip to content

Commit 4c01c88

Browse files
committed
feat: user profile updated publish
1 parent 5f2696c commit 4c01c88

11 files changed

Lines changed: 509 additions & 16 deletions

File tree

.infra/crons.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,8 @@ export const crons: Cron[] = [
134134
name: 'clean-zombie-opportunities',
135135
schedule: '30 6 * * *',
136136
},
137+
{
138+
name: 'user-profile-updated-sync',
139+
schedule: '45 */6 * * *',
140+
},
137141
];
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
import { userProfileUpdatedSync as cron } from '../../src/cron/userProfileUpdatedSync';
2+
import { expectSuccessfulCron, saveFixtures } from '../helpers';
3+
import { DataSource } from 'typeorm';
4+
import createOrGetConnection from '../../src/db';
5+
import { crons } from '../../src/cron/index';
6+
import { organizationsFixture } from '../fixture/opportunity';
7+
import { userExperienceWorkFixture } from '../fixture/profile/work';
8+
import { Organization } from '../../src/entity/Organization';
9+
import { insertOrIgnoreUserExperienceSkills } from '../../src/entity/user/experiences/UserExperienceSkill';
10+
import { User } from '../../src/entity/user/User';
11+
import { usersFixture } from '../fixture/user';
12+
import { DatasetLocation } from '../../src/entity/dataset/DatasetLocation';
13+
import {
14+
datasetLocationFixture,
15+
userExperienceFixture,
16+
} from '../fixture/profile/experience';
17+
import { randomUUID } from 'node:crypto';
18+
import { Company } from '../../src/entity/Company';
19+
import { companyFixture } from '../fixture/company';
20+
import { triggerTypedEvent } from '../../src/common';
21+
import { UserProfileUpdatedMessage } from '@dailydotdev/schema';
22+
import { UserExperience } from '../../src/entity/user/experiences/UserExperience';
23+
24+
jest.mock('../../src/common/typedPubsub', () => ({
25+
...(jest.requireActual('../../src/common/typedPubsub') as Record<
26+
string,
27+
unknown
28+
>),
29+
triggerTypedEvent: jest.fn(),
30+
}));
31+
32+
let con: DataSource;
33+
34+
beforeAll(async () => {
35+
con = await createOrGetConnection();
36+
});
37+
38+
beforeEach(async () => {
39+
jest.clearAllMocks();
40+
41+
await saveFixtures(con, Organization, organizationsFixture);
42+
await saveFixtures(con, User, usersFixture);
43+
await saveFixtures(con, Company, companyFixture);
44+
45+
const datasetLocations = await con
46+
.getRepository(DatasetLocation)
47+
.save(datasetLocationFixture);
48+
49+
const experiencesToInsert = userExperienceFixture.map((item) => {
50+
const experienceId = randomUUID();
51+
52+
return {
53+
...item,
54+
skills: undefined,
55+
id: experienceId,
56+
locationId: item.customLocation ? undefined : datasetLocations[0].id,
57+
};
58+
});
59+
60+
await con.getRepository(UserExperience).save(experiencesToInsert);
61+
62+
await Promise.all(
63+
userExperienceWorkFixture.map((item, index) => {
64+
const experienceId = experiencesToInsert[index].id;
65+
66+
return insertOrIgnoreUserExperienceSkills(
67+
con,
68+
experienceId,
69+
userExperienceWorkFixture[index].skills || [],
70+
);
71+
}),
72+
);
73+
});
74+
75+
describe('userProfileUpdatedSync cron', () => {
76+
it('should be registered', () => {
77+
const registeredWorker = crons.find((item) => item.name === cron.name);
78+
79+
expect(registeredWorker).toBeDefined();
80+
});
81+
82+
it('should publish updated user experiences', async () => {
83+
await expectSuccessfulCron(cron);
84+
85+
expect(triggerTypedEvent).toHaveBeenCalledTimes(2);
86+
87+
const mockCalls = jest.mocked(triggerTypedEvent).mock.calls;
88+
89+
const user1Call = mockCalls.find(
90+
(item) =>
91+
(item[2] as unknown as UserProfileUpdatedMessage).profile?.userId ===
92+
'1',
93+
);
94+
expect(user1Call).toBeDefined();
95+
96+
expect(user1Call![2]).toEqual({
97+
profile: {
98+
experiences: expect.arrayContaining([
99+
{
100+
companyName: 'daily.dev',
101+
createdAt: expect.any(Number),
102+
description: 'Working on API infrastructure',
103+
employmentType: 0,
104+
id: expect.any(String),
105+
location: {
106+
city: 'San Francisco',
107+
country: 'USA',
108+
subdivision: 'CA',
109+
},
110+
locationType: 3,
111+
startedAt: expect.any(Number),
112+
subtitle: 'Backend Team',
113+
title: 'Senior Software Engineer',
114+
type: 1,
115+
updatedAt: expect.any(Number),
116+
verified: true,
117+
},
118+
{
119+
companyName: 'daily.dev',
120+
createdAt: expect.any(Number),
121+
description: 'Worked on search infrastructure',
122+
employmentType: 3,
123+
endedAt: expect.any(Number),
124+
125+
id: expect.any(String),
126+
location: {
127+
city: 'San Francisco',
128+
country: 'United States',
129+
subdivision: 'California',
130+
},
131+
locationType: 2,
132+
startedAt: expect.any(Number),
133+
title: 'Software Engineer',
134+
type: 1,
135+
updatedAt: expect.any(Number),
136+
verified: false,
137+
},
138+
{
139+
companyName: 'daily.dev',
140+
createdAt: expect.any(Number),
141+
description: 'Focused on distributed systems',
142+
employmentType: 0,
143+
endedAt: expect.any(Number),
144+
id: expect.any(String),
145+
location: {
146+
city: 'San Francisco',
147+
country: 'United States',
148+
subdivision: 'California',
149+
},
150+
locationType: 0,
151+
startedAt: expect.any(Number),
152+
subtitle: 'Bachelor of Science',
153+
title: 'Computer Science',
154+
type: 2,
155+
updatedAt: expect.any(Number),
156+
verified: false,
157+
grade: '9/5',
158+
},
159+
]),
160+
skills: expect.arrayContaining([
161+
{
162+
experienceId: expect.any(String),
163+
value: 'CMS',
164+
},
165+
{
166+
experienceId: expect.any(String),
167+
value: 'VIVO CMS',
168+
},
169+
{
170+
experienceId: expect.any(String),
171+
value: 'PHP',
172+
},
173+
{
174+
experienceId: expect.any(String),
175+
value: 'Paiting',
176+
},
177+
{
178+
experienceId: expect.any(String),
179+
value: 'Woodworking',
180+
},
181+
]),
182+
userId: '1',
183+
},
184+
});
185+
186+
const user2Call = mockCalls.find(
187+
(item) =>
188+
(item[2] as unknown as UserProfileUpdatedMessage).profile?.userId ===
189+
'2',
190+
);
191+
expect(user2Call).toBeDefined();
192+
193+
expect(user2Call![2]).toEqual({
194+
profile: {
195+
experiences: expect.arrayContaining([
196+
{
197+
companyName: 'daily.dev',
198+
createdAt: expect.any(Number),
199+
description: 'Managing product roadmap',
200+
employmentType: 1,
201+
id: expect.any(String),
202+
location: {
203+
city: 'San Francisco',
204+
country: 'United States',
205+
subdivision: 'California',
206+
},
207+
locationType: 0,
208+
startedAt: expect.any(Number),
209+
title: 'Product Manager',
210+
type: 1,
211+
updatedAt: expect.any(Number),
212+
verified: true,
213+
},
214+
{
215+
companyName: 'daily.dev',
216+
createdAt: expect.any(Number),
217+
description: 'Contributing to TypeScript projects',
218+
employmentType: 0,
219+
id: expect.any(String),
220+
location: {
221+
city: 'San Francisco',
222+
country: 'United States',
223+
subdivision: 'California',
224+
},
225+
locationType: 0,
226+
startedAt: expect.any(Number),
227+
title: 'Open Source Contributor',
228+
type: 3,
229+
updatedAt: expect.any(Number),
230+
verified: false,
231+
url: 'https://example.com/project',
232+
},
233+
]),
234+
skills: expect.arrayContaining([]),
235+
userId: '2',
236+
},
237+
});
238+
});
239+
});

__tests__/fixture/company.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type { DeepPartial } from 'typeorm';
2+
import type { Company } from '../../src/entity/Company';
3+
4+
export const companyFixture: DeepPartial<Company>[] = [
5+
{
6+
id: 'dailydev',
7+
name: 'daily.dev',
8+
image: 'cloudinary.com/dailydev/121232121/image',
9+
domains: ['daily.dev', 'dailydev.com'],
10+
},
11+
];
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import type { DeepPartial } from 'typeorm';
2+
import type { UserExperience } from '../../../src/entity/user/experiences/UserExperience';
3+
import { UserExperienceType } from '../../../src/entity/user/experiences/types';
4+
import type { UserExperienceEducation } from '../../../src/entity/user/experiences/UserExperienceEducation';
5+
import type { UserExperienceProject } from '../../../src/entity/user/experiences/UserExperienceProject';
6+
import { EmploymentType, LocationType } from '@dailydotdev/schema';
7+
import type { DatasetLocation } from '../../../src/entity/dataset/DatasetLocation';
8+
import type { UserExperienceWork } from '../../../src/entity/user/experiences/UserExperienceWork';
9+
10+
export const datasetLocationFixture: DeepPartial<DatasetLocation>[] = [
11+
{
12+
country: 'United States',
13+
city: 'San Francisco',
14+
subdivision: 'California',
15+
iso2: 'US',
16+
iso3: 'USA',
17+
},
18+
];
19+
20+
export const userExperienceFixture: DeepPartial<
21+
UserExperience &
22+
UserExperienceEducation &
23+
UserExperienceProject &
24+
UserExperienceWork & {
25+
skills?: string[];
26+
}
27+
>[] = [
28+
{
29+
userId: '1',
30+
companyId: 'dailydev',
31+
title: 'Senior Software Engineer',
32+
subtitle: 'Backend Team',
33+
description: 'Working on API infrastructure',
34+
startedAt: new Date('2022-01-01'),
35+
endedAt: null, // Current position
36+
type: UserExperienceType.Work,
37+
customLocation: {
38+
city: 'San Francisco',
39+
subdivision: 'CA',
40+
country: 'USA',
41+
},
42+
locationType: LocationType.HYBRID,
43+
skills: ['TypeScript', 'Node.js', 'PostgreSQL'],
44+
verified: true,
45+
},
46+
{
47+
userId: '1',
48+
companyId: 'dailydev',
49+
title: 'Software Engineer',
50+
subtitle: null,
51+
description: 'Worked on search infrastructure',
52+
startedAt: new Date('2020-01-01'),
53+
endedAt: new Date('2021-12-31'),
54+
type: UserExperienceType.Work,
55+
locationType: LocationType.OFFICE,
56+
skills: ['Elasticsearch', 'Go', 'Docker'],
57+
employmentType: EmploymentType.CONTRACT,
58+
},
59+
{
60+
userId: '1',
61+
companyId: 'dailydev',
62+
title: 'Computer Science',
63+
subtitle: 'Bachelor of Science',
64+
description: 'Focused on distributed systems',
65+
startedAt: new Date('2016-09-01'),
66+
endedAt: new Date('2020-06-30'),
67+
type: UserExperienceType.Education,
68+
grade: '9/5',
69+
},
70+
{
71+
userId: '2',
72+
companyId: 'dailydev',
73+
title: 'Open Source Contributor',
74+
subtitle: null,
75+
description: 'Contributing to TypeScript projects',
76+
startedAt: new Date('2021-06-01'),
77+
endedAt: null,
78+
type: UserExperienceType.Project,
79+
url: 'https://example.com/project',
80+
},
81+
{
82+
userId: '2',
83+
companyId: 'dailydev',
84+
title: 'Product Manager',
85+
subtitle: null,
86+
description: 'Managing product roadmap',
87+
startedAt: new Date('2021-01-01'),
88+
endedAt: null,
89+
type: UserExperienceType.Work,
90+
employmentType: EmploymentType.FULL_TIME,
91+
skills: ['Agile', 'Scrum', 'Roadmapping'],
92+
verified: true,
93+
},
94+
];

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"@connectrpc/connect-fastify": "^1.6.1",
3737
"@connectrpc/connect-node": "^1.6.1",
3838
"@dailydotdev/graphql-redis-subscriptions": "^2.4.3",
39-
"@dailydotdev/schema": "0.2.51",
39+
"@dailydotdev/schema": "0.2.53",
4040
"@dailydotdev/ts-ioredis-pool": "^1.0.2",
4141
"@fastify/cookie": "^11.0.2",
4242
"@fastify/cors": "^11.1.0",

pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)