Skip to content

Commit 84f2e15

Browse files
authored
test: add tests for recruiter redirector (#3235)
1 parent bb1c8e7 commit 84f2e15

File tree

3 files changed

+247
-14
lines changed

3 files changed

+247
-14
lines changed

.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,4 @@ CLICKHOUSE_USER=default
9292
CLICKHOUSE_PASSWORD=changeme
9393
RESUME_BUCKET_NAME=bucket-name
9494
EMPLOYMENT_AGREEMENT_BUCKET_NAME=other-bucket-name
95+
HMAC_SECRET=supersecretkey

__tests__/redirector.ts

Lines changed: 220 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import appFunc from '../src';
22
import { FastifyInstance } from 'fastify';
3-
import { saveFixtures, TEST_UA } from './helpers';
3+
import { authorizeRequest, saveFixtures, TEST_UA } from './helpers';
44
import { ArticlePost, Source, User, YouTubePost } from '../src/entity';
55
import { sourcesFixture } from './fixture/source';
66
import request from 'supertest';
77
import { postsFixture, videoPostsFixture } from './fixture/post';
8-
import { notifyView } from '../src/common';
8+
import { hmacHashIP, notifyView } from '../src/common';
99
import { DataSource } from 'typeorm';
1010
import createOrGetConnection from '../src/db';
1111
import { fallbackImages } from '../src/config';
12+
import { usersFixture } from './fixture/user';
13+
import { UserReferralLinkedin } from '../src/entity/user/referral/UserReferralLinkedin';
14+
import { logger } from '../src/logger';
15+
import { UserReferralStatus } from '../src/entity/user/referral/UserReferral';
16+
import { BASE_RECRUITER_URL } from '../src/routes/redirector';
1217

1318
jest.mock('../src/common', () => ({
1419
...(jest.requireActual('../src/common') as Record<string, unknown>),
@@ -134,3 +139,216 @@ describe('GET /:id/profile-image', () => {
134139
.expect('Location', fallbackImages.avatar);
135140
});
136141
});
142+
143+
describe('GET /r/recruiter/:id', () => {
144+
const spyLogger = jest.fn();
145+
146+
const saveReferral = async (override?: Partial<UserReferralLinkedin>) => {
147+
return con.getRepository(UserReferralLinkedin).save({
148+
userId: usersFixture[0].id,
149+
externalUserId: 'ext-0',
150+
flags: { hashedRequestIP: hmacHashIP('198.51.100.1') },
151+
...override,
152+
});
153+
};
154+
155+
const getReferral = async (id: string) => {
156+
return con.getRepository(UserReferralLinkedin).findOne({ where: { id } });
157+
};
158+
159+
beforeEach(async () => {
160+
await saveFixtures(con, User, usersFixture);
161+
});
162+
163+
it('should redirect to recruiter landing', async () => {
164+
const r = await saveReferral();
165+
const res = await request(app.server)
166+
.get(`/r/recruiter/${r.id}`)
167+
.set('Referer', 'https://www.linkedin.com/')
168+
.set('X-Forwarded-For', '203.0.113.1');
169+
170+
expect(res.status).toBe(302);
171+
expect(res.headers.location).toBe(BASE_RECRUITER_URL);
172+
});
173+
174+
it('should redirect to recruiter landing even with invalid UUID', async () => {
175+
jest.spyOn(logger, 'debug').mockImplementation(spyLogger);
176+
const res = await request(app.server)
177+
.get(`/r/recruiter/invalid-uuid`)
178+
.set('Referer', 'https://www.linkedin.com/')
179+
.set('X-Forwarded-For', '203.0.113.1');
180+
181+
expect(res.status).toBe(302);
182+
expect(res.headers.location).toBe(BASE_RECRUITER_URL);
183+
184+
await new Promise((r) => setTimeout(r, 100)); // wait for onResponse async tasks
185+
186+
expect(spyLogger).toHaveBeenCalledTimes(1);
187+
expect(spyLogger).toHaveBeenCalledWith(
188+
{ referralId: 'invalid-uuid' },
189+
'Invalid referral id provided, skipping recruiter redirector',
190+
);
191+
});
192+
193+
it('should redirect to recruiter landing without marking visited if user is logged in', async () => {
194+
jest.spyOn(logger, 'debug').mockImplementation(spyLogger);
195+
const r = await saveReferral();
196+
197+
const res = await authorizeRequest(
198+
request(app.server)
199+
.get(`/r/recruiter/${r.id}`)
200+
.set('Referer', 'https://www.linkedin.com/')
201+
.set('X-Forwarded-For', '203.0.113.1'),
202+
);
203+
expect(res.status).toBe(302);
204+
expect(res.headers.location).toBe(BASE_RECRUITER_URL);
205+
206+
await new Promise((r) => setTimeout(r, 100)); // wait for onResponse async tasks
207+
208+
expect(spyLogger).toHaveBeenCalledTimes(1);
209+
expect(spyLogger).toHaveBeenCalledWith(
210+
{ referralId: r.id },
211+
'User is logged in, skipping recruiter redirector',
212+
);
213+
214+
const referral = await getReferral(r.id);
215+
expect(referral?.visited).toBe(false);
216+
});
217+
218+
it('should redirect to recruiter landing without marking visited if no referrer', async () => {
219+
jest.spyOn(logger, 'debug').mockImplementation(spyLogger);
220+
const r = await saveReferral();
221+
222+
const res = await request(app.server)
223+
.get(`/r/recruiter/${r.id}`)
224+
.set('X-Forwarded-For', '203.0.113.1');
225+
expect(res.status).toBe(302);
226+
expect(res.headers.location).toBe(BASE_RECRUITER_URL);
227+
228+
await new Promise((r) => setTimeout(r, 100)); // wait for onResponse async tasks
229+
230+
expect(spyLogger).toHaveBeenCalledTimes(1);
231+
expect(spyLogger).toHaveBeenCalledWith(
232+
{ referralId: r.id },
233+
'No referrer provided, skipping recruiter redirector',
234+
);
235+
236+
const referral = await getReferral(r.id);
237+
expect(referral?.visited).toBe(false);
238+
});
239+
240+
it('should redirect to recruiter landing without marking visited if referrer is not linkedin', async () => {
241+
jest.spyOn(logger, 'debug').mockImplementation(spyLogger);
242+
const r = await saveReferral();
243+
244+
const res = await request(app.server)
245+
.get(`/r/recruiter/${r.id}`)
246+
.set('Referer', 'https://daily.dev/')
247+
.set('X-Forwarded-For', '203.0.113.1');
248+
expect(res.status).toBe(302);
249+
expect(res.headers.location).toBe(BASE_RECRUITER_URL);
250+
251+
await new Promise((r) => setTimeout(r, 100)); // wait for onResponse async tasks
252+
253+
expect(spyLogger).toHaveBeenCalledTimes(1);
254+
expect(spyLogger).toHaveBeenCalledWith(
255+
{
256+
referralId: r.id,
257+
referrer: 'https://daily.dev/',
258+
},
259+
'Referrer is not linkedin, skipping recruiter redirector',
260+
);
261+
262+
const referral = await getReferral(r.id);
263+
expect(referral?.visited).toBe(false);
264+
});
265+
266+
it('should mark referral as visited when all conditions met', async () => {
267+
jest.spyOn(logger, 'debug').mockImplementation(spyLogger);
268+
const r = await saveReferral();
269+
270+
const res = await request(app.server)
271+
.get(`/r/recruiter/${r.id}`)
272+
.set('Referer', 'https://www.linkedin.com/')
273+
.set('X-Forwarded-For', '203.0.113.1');
274+
expect(res.status).toBe(302);
275+
expect(res.headers.location).toBe(BASE_RECRUITER_URL);
276+
277+
await new Promise((r) => setTimeout(r, 100)); // wait for onResponse async tasks
278+
279+
expect(spyLogger).toHaveBeenCalledTimes(1);
280+
expect(spyLogger).toHaveBeenCalledWith(
281+
{ referralId: r.id },
282+
'Marked referral as visited',
283+
);
284+
285+
const referral = await getReferral(r.id);
286+
expect(referral?.visited).toBe(true);
287+
});
288+
289+
it('should not mark referral as visited if visitor is the requester', async () => {
290+
jest.spyOn(logger, 'debug').mockImplementation(spyLogger);
291+
const r = await saveReferral();
292+
293+
const res = await request(app.server)
294+
.get(`/r/recruiter/${r.id}`)
295+
.set('Referer', 'https://www.linkedin.com/')
296+
.set('X-Forwarded-For', '198.51.100.1');
297+
expect(res.status).toBe(302);
298+
expect(res.headers.location).toBe(BASE_RECRUITER_URL);
299+
300+
await new Promise((r) => setTimeout(r, 100)); // wait for onResponse async tasks
301+
302+
expect(spyLogger).toHaveBeenCalledTimes(1);
303+
expect(spyLogger).toHaveBeenCalledWith(
304+
{ referralId: r.id },
305+
'No referral found or referral already marked as visited',
306+
);
307+
308+
const referral = await getReferral(r.id);
309+
expect(referral?.visited).toBe(false);
310+
});
311+
312+
it('should not do anything if already visited', async () => {
313+
jest.spyOn(logger, 'debug').mockImplementation(spyLogger);
314+
const r = await saveReferral({ visited: true });
315+
316+
const res = await request(app.server)
317+
.get(`/r/recruiter/${r.id}`)
318+
.set('Referer', 'https://www.linkedin.com/')
319+
.set('X-Forwarded-For', '203.0.113.1');
320+
expect(res.status).toBe(302);
321+
expect(res.headers.location).toBe(BASE_RECRUITER_URL);
322+
323+
await new Promise((r) => setTimeout(r, 100)); // wait for onResponse async tasks
324+
325+
expect(spyLogger).toHaveBeenCalledTimes(1);
326+
expect(spyLogger).toHaveBeenCalledWith(
327+
{ referralId: r.id },
328+
'No referral found or referral already marked as visited',
329+
);
330+
});
331+
332+
it('should not do anything if referral status is not pending', async () => {
333+
jest.spyOn(logger, 'debug').mockImplementation(spyLogger);
334+
const r = await saveReferral({ status: UserReferralStatus.Rejected });
335+
336+
const res = await request(app.server)
337+
.get(`/r/recruiter/${r.id}`)
338+
.set('Referer', 'https://www.linkedin.com/')
339+
.set('X-Forwarded-For', '203.0.113.1');
340+
expect(res.status).toBe(302);
341+
expect(res.headers.location).toBe(BASE_RECRUITER_URL);
342+
343+
await new Promise((r) => setTimeout(r, 100)); // wait for onResponse async tasks
344+
345+
expect(spyLogger).toHaveBeenCalledTimes(1);
346+
expect(spyLogger).toHaveBeenCalledWith(
347+
{ referralId: r.id },
348+
'No referral found or referral already marked as visited',
349+
);
350+
351+
const referral = await getReferral(r.id);
352+
expect(referral?.visited).toBe(false);
353+
});
354+
});

src/routes/redirector.ts

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import createOrGetConnection from '../db';
77
import { isNullOrUndefined } from '../common/object';
88
import { UserReferralLinkedin } from '../entity/user/referral/UserReferralLinkedin';
99
import { JsonContains, Not } from 'typeorm';
10+
import { logger } from '../logger';
11+
import { UserReferralStatus } from '../entity/user/referral/UserReferral';
1012

1113
export default async function (fastify: FastifyInstance): Promise<void> {
1214
fastify.get<{ Params: { postId: string }; Querystring: { a?: string } }>(
@@ -46,7 +48,7 @@ export default async function (fastify: FastifyInstance): Promise<void> {
4648
const userId = req.userId || req.trackingId;
4749
if (userId) {
4850
notifyView(
49-
req.log,
51+
logger,
5052
post.id,
5153
userId,
5254
req.headers['referer'],
@@ -71,29 +73,42 @@ export default async function (fastify: FastifyInstance): Promise<void> {
7173
fastify.register(recruiterRedirector, { prefix: '/recruiter' });
7274
}
7375

76+
export const BASE_RECRUITER_URL =
77+
'https://recruiter.daily.dev/?utm_source=dailydev&utm_medium=linkedin_referral';
78+
7479
const recruiterRedirector = async (fastify: FastifyInstance): Promise<void> => {
7580
fastify.addHook<{ Params: { id: string } }>('onResponse', async (req) => {
7681
const { error, data: id } = z.uuidv4().safeParse(req.params.id);
7782
if (error) {
78-
req.log.debug(
83+
logger.debug(
84+
{ referralId: req.params.id },
7985
'Invalid referral id provided, skipping recruiter redirector',
8086
);
8187
return;
8288
}
8389

8490
if (req.userId) {
85-
req.log.debug('User is logged in, skipping recruiter redirector');
91+
logger.debug(
92+
{ referralId: id },
93+
'User is logged in, skipping recruiter redirector',
94+
);
8695
return;
8796
}
8897

8998
const referrer = req.headers['referer'];
9099
if (isNullOrUndefined(referrer)) {
91-
req.log.debug('No referrer provided, skipping recruiter redirector');
100+
logger.debug(
101+
{ referralId: id },
102+
'No referrer provided, skipping recruiter redirector',
103+
);
92104
return;
93105
}
94106

95107
if (referrer.startsWith('https://www.linkedin.com/') === false) {
96-
req.log.debug('Referrer is not linkedin, skipping recruiter redirector');
108+
logger.debug(
109+
{ referralId: id, referrer },
110+
'Referrer is not linkedin, skipping recruiter redirector',
111+
);
97112
return;
98113
}
99114

@@ -103,33 +118,32 @@ const recruiterRedirector = async (fastify: FastifyInstance): Promise<void> => {
103118
const result = await con.getRepository(UserReferralLinkedin).update(
104119
{
105120
id: id,
121+
status: UserReferralStatus.Pending,
106122
visited: false,
107123
flags: Not(JsonContains({ hashedRequestIP: hmacHashIP(req.ip) })),
108124
},
109125
{ visited: true },
110126
);
111127

112128
if (result.affected === 0) {
113-
req.log.debug(
114-
{ id },
129+
logger.debug(
130+
{ referralId: id },
115131
`No referral found or referral already marked as visited`,
116132
);
117133
return;
118134
}
119135

120-
req.log.debug({ id }, `Marked referral as visited`);
136+
logger.debug({ referralId: id }, 'Marked referral as visited');
121137
} catch (_err) {
122138
const err = _err as Error;
123-
req.log.error(
139+
logger.error(
124140
{ err, referralId: id },
125141
'Failed to mark referral as visited',
126142
);
127143
}
128144
});
129145

130146
fastify.get<{ Params: { id: string } }>('/:id', (_, res) =>
131-
res.redirect(
132-
'https://recruiter.daily.dev/?utm_source=dailydev&utm_medium=linkedin_referral',
133-
),
147+
res.redirect(BASE_RECRUITER_URL),
134148
);
135149
};

0 commit comments

Comments
 (0)