Skip to content

Commit 872633a

Browse files
committed
feat: new endpoint to expose user opportuntity matches
1 parent 1428135 commit 872633a

File tree

2 files changed

+386
-0
lines changed

2 files changed

+386
-0
lines changed

__tests__/schema/opportunity.ts

Lines changed: 342 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,6 +1068,348 @@ describe('query opportunityMatches', () => {
10681068
});
10691069
});
10701070

1071+
describe('query userOpportunityMatches', () => {
1072+
const GET_USER_OPPORTUNITY_MATCHES_QUERY = /* GraphQL */ `
1073+
query GetUserOpportunityMatches($first: Int, $after: String) {
1074+
userOpportunityMatches(first: $first, after: $after) {
1075+
pageInfo {
1076+
hasNextPage
1077+
hasPreviousPage
1078+
endCursor
1079+
startCursor
1080+
}
1081+
edges {
1082+
node {
1083+
userId
1084+
opportunityId
1085+
status
1086+
description {
1087+
reasoning
1088+
}
1089+
screening {
1090+
screening
1091+
answer
1092+
}
1093+
feedback {
1094+
screening
1095+
answer
1096+
}
1097+
applicationRank {
1098+
score
1099+
description
1100+
warmIntro
1101+
}
1102+
user {
1103+
id
1104+
name
1105+
}
1106+
candidatePreferences {
1107+
status
1108+
role
1109+
}
1110+
createdAt
1111+
updatedAt
1112+
}
1113+
}
1114+
}
1115+
}
1116+
`;
1117+
1118+
it('should require authentication', async () => {
1119+
await testQueryErrorCode(
1120+
client,
1121+
{
1122+
query: GET_USER_OPPORTUNITY_MATCHES_QUERY,
1123+
variables: {
1124+
first: 10,
1125+
},
1126+
},
1127+
'UNAUTHENTICATED',
1128+
);
1129+
});
1130+
1131+
it('should return all matches for the authenticated user', async () => {
1132+
loggedUser = '1';
1133+
1134+
const res = await client.query(GET_USER_OPPORTUNITY_MATCHES_QUERY, {
1135+
variables: {
1136+
first: 10,
1137+
},
1138+
});
1139+
1140+
expect(res.errors).toBeFalsy();
1141+
expect(res.data.userOpportunityMatches.edges).toHaveLength(2);
1142+
1143+
const opportunityIds = res.data.userOpportunityMatches.edges.map(
1144+
(e: { node: { opportunityId: string } }) => e.node.opportunityId,
1145+
);
1146+
1147+
// User 1 has matches for opportunities 1 and 3
1148+
expect(opportunityIds).toContain('550e8400-e29b-41d4-a716-446655440001');
1149+
expect(opportunityIds).toContain('550e8400-e29b-41d4-a716-446655440003');
1150+
1151+
// All matches should belong to user 1
1152+
const userIds = res.data.userOpportunityMatches.edges.map(
1153+
(e: { node: { userId: string } }) => e.node.userId,
1154+
);
1155+
expect(userIds.every((id: string) => id === '1')).toBe(true);
1156+
});
1157+
1158+
it('should return matches ordered by updatedAt DESC', async () => {
1159+
loggedUser = '2';
1160+
1161+
// Add more matches for user 2 with different updatedAt dates
1162+
await saveFixtures(con, OpportunityMatch, [
1163+
{
1164+
opportunityId: '550e8400-e29b-41d4-a716-446655440002',
1165+
userId: '2',
1166+
status: OpportunityMatchStatus.Pending,
1167+
description: { reasoning: 'Newer match' },
1168+
screening: [],
1169+
feedback: [],
1170+
applicationRank: {},
1171+
createdAt: new Date('2023-01-10'),
1172+
updatedAt: new Date('2023-01-10'),
1173+
},
1174+
]);
1175+
1176+
const res = await client.query(GET_USER_OPPORTUNITY_MATCHES_QUERY, {
1177+
variables: {
1178+
first: 10,
1179+
},
1180+
});
1181+
1182+
expect(res.errors).toBeFalsy();
1183+
expect(res.data.userOpportunityMatches.edges).toHaveLength(2);
1184+
1185+
const updatedDates = res.data.userOpportunityMatches.edges.map(
1186+
(e: { node: { updatedAt: string } }) => new Date(e.node.updatedAt),
1187+
);
1188+
1189+
// Verify DESC ordering (most recent first)
1190+
expect(updatedDates[0].getTime()).toBeGreaterThan(updatedDates[1].getTime());
1191+
});
1192+
1193+
it('should return different matches for different users', async () => {
1194+
loggedUser = '2';
1195+
1196+
const res = await client.query(GET_USER_OPPORTUNITY_MATCHES_QUERY, {
1197+
variables: {
1198+
first: 10,
1199+
},
1200+
});
1201+
1202+
expect(res.errors).toBeFalsy();
1203+
expect(res.data.userOpportunityMatches.edges).toHaveLength(1);
1204+
1205+
const match = res.data.userOpportunityMatches.edges[0].node;
1206+
expect(match.userId).toBe('2');
1207+
expect(match.opportunityId).toBe('550e8400-e29b-41d4-a716-446655440001');
1208+
expect(match.status).toBe('candidate_accepted');
1209+
expect(match.description.reasoning).toBe('Accepted candidate');
1210+
});
1211+
1212+
it('should include all match statuses for the user', async () => {
1213+
loggedUser = '1';
1214+
1215+
const res = await client.query(GET_USER_OPPORTUNITY_MATCHES_QUERY, {
1216+
variables: {
1217+
first: 10,
1218+
},
1219+
});
1220+
1221+
expect(res.errors).toBeFalsy();
1222+
1223+
const statuses = res.data.userOpportunityMatches.edges.map(
1224+
(e: { node: { status: string } }) => e.node.status,
1225+
);
1226+
1227+
// User 1 has two pending matches
1228+
expect(statuses).toContain('pending');
1229+
});
1230+
1231+
it('should include screening, feedback, and application rank data', async () => {
1232+
loggedUser = '1';
1233+
1234+
const res = await client.query(GET_USER_OPPORTUNITY_MATCHES_QUERY, {
1235+
variables: {
1236+
first: 10,
1237+
},
1238+
});
1239+
1240+
expect(res.errors).toBeFalsy();
1241+
1242+
const matchWithData = res.data.userOpportunityMatches.edges.find(
1243+
(e: { node: { opportunityId: string } }) =>
1244+
e.node.opportunityId === '550e8400-e29b-41d4-a716-446655440001',
1245+
);
1246+
1247+
expect(matchWithData.node.screening).toEqual([
1248+
{ screening: 'What is your favorite language?', answer: 'TypeScript' },
1249+
]);
1250+
1251+
expect(matchWithData.node.applicationRank).toEqual({
1252+
score: 85,
1253+
description: 'Strong candidate',
1254+
warmIntro: null,
1255+
});
1256+
});
1257+
1258+
it('should support pagination with first parameter', async () => {
1259+
loggedUser = '1';
1260+
1261+
const res = await client.query(GET_USER_OPPORTUNITY_MATCHES_QUERY, {
1262+
variables: {
1263+
first: 1,
1264+
},
1265+
});
1266+
1267+
expect(res.errors).toBeFalsy();
1268+
expect(res.data.userOpportunityMatches.edges).toHaveLength(1);
1269+
expect(res.data.userOpportunityMatches.pageInfo.hasNextPage).toBe(true);
1270+
expect(res.data.userOpportunityMatches.pageInfo.endCursor).toBeTruthy();
1271+
});
1272+
1273+
it('should support pagination with after cursor', async () => {
1274+
loggedUser = '1';
1275+
1276+
// Update one match to have a different updatedAt for proper pagination testing
1277+
await con.getRepository(OpportunityMatch).update(
1278+
{
1279+
opportunityId: '550e8400-e29b-41d4-a716-446655440001',
1280+
userId: '1',
1281+
},
1282+
{
1283+
updatedAt: new Date('2023-01-08'),
1284+
},
1285+
);
1286+
1287+
// Get first page
1288+
const firstPage = await client.query(GET_USER_OPPORTUNITY_MATCHES_QUERY, {
1289+
variables: {
1290+
first: 1,
1291+
},
1292+
});
1293+
1294+
expect(firstPage.errors).toBeFalsy();
1295+
expect(firstPage.data.userOpportunityMatches.edges).toHaveLength(1);
1296+
expect(firstPage.data.userOpportunityMatches.pageInfo.hasNextPage).toBe(
1297+
true,
1298+
);
1299+
const firstOpportunityId =
1300+
firstPage.data.userOpportunityMatches.edges[0].node.opportunityId;
1301+
const endCursor = firstPage.data.userOpportunityMatches.pageInfo.endCursor;
1302+
1303+
// Get second page
1304+
const secondPage = await client.query(GET_USER_OPPORTUNITY_MATCHES_QUERY, {
1305+
variables: {
1306+
first: 10,
1307+
after: endCursor,
1308+
},
1309+
});
1310+
1311+
expect(secondPage.errors).toBeFalsy();
1312+
expect(secondPage.data.userOpportunityMatches.edges).toHaveLength(1);
1313+
expect(secondPage.data.userOpportunityMatches.pageInfo.hasNextPage).toBe(
1314+
false,
1315+
);
1316+
// Verify we got different results
1317+
expect(
1318+
secondPage.data.userOpportunityMatches.edges[0].node.opportunityId,
1319+
).not.toBe(firstOpportunityId);
1320+
expect(
1321+
secondPage.data.userOpportunityMatches.pageInfo.hasPreviousPage,
1322+
).toBe(true);
1323+
});
1324+
1325+
it('should return empty list for user with no matches', async () => {
1326+
loggedUser = '5'; // User with no matches
1327+
1328+
const res = await client.query(GET_USER_OPPORTUNITY_MATCHES_QUERY, {
1329+
variables: {
1330+
first: 10,
1331+
},
1332+
});
1333+
1334+
expect(res.errors).toBeFalsy();
1335+
expect(res.data.userOpportunityMatches.edges).toHaveLength(0);
1336+
expect(res.data.userOpportunityMatches.pageInfo.hasNextPage).toBe(false);
1337+
});
1338+
1339+
it('should include user data in the response', async () => {
1340+
loggedUser = '1';
1341+
1342+
const res = await client.query(GET_USER_OPPORTUNITY_MATCHES_QUERY, {
1343+
variables: {
1344+
first: 10,
1345+
},
1346+
});
1347+
1348+
expect(res.errors).toBeFalsy();
1349+
1350+
const firstMatch = res.data.userOpportunityMatches.edges[0].node;
1351+
expect(firstMatch.user).toEqual({
1352+
id: '1',
1353+
name: 'Ido',
1354+
});
1355+
});
1356+
1357+
it('should expose salaryExpectation to user viewing their own matches', async () => {
1358+
loggedUser = '1';
1359+
1360+
// Add salaryExpectation to user 1's candidate preferences
1361+
await con.getRepository(UserCandidatePreference).upsert(
1362+
{
1363+
userId: '1',
1364+
salaryExpectation: {
1365+
min: 100000,
1366+
period: SalaryPeriod.ANNUAL,
1367+
},
1368+
},
1369+
{
1370+
conflictPaths: ['userId'],
1371+
skipUpdateIfNoValuesChanged: true,
1372+
},
1373+
);
1374+
1375+
const GET_USER_MATCHES_WITH_SALARY_QUERY = /* GraphQL */ `
1376+
query GetUserOpportunityMatchesWithSalary($first: Int) {
1377+
userOpportunityMatches(first: $first) {
1378+
edges {
1379+
node {
1380+
userId
1381+
updatedAt
1382+
candidatePreferences {
1383+
status
1384+
role
1385+
salaryExpectation {
1386+
min
1387+
period
1388+
}
1389+
}
1390+
}
1391+
}
1392+
}
1393+
}
1394+
`;
1395+
1396+
const res = await client.query(GET_USER_MATCHES_WITH_SALARY_QUERY, {
1397+
variables: {
1398+
first: 10,
1399+
},
1400+
});
1401+
1402+
expect(res.errors).toBeFalsy();
1403+
1404+
const firstMatch = res.data.userOpportunityMatches.edges[0].node;
1405+
expect(firstMatch.userId).toBe('1');
1406+
expect(firstMatch.candidatePreferences.salaryExpectation).toEqual({
1407+
min: 100000,
1408+
period: 1, // ANNUAL
1409+
});
1410+
});
1411+
});
1412+
10711413
describe('query getCandidatePreferences', () => {
10721414
const QUERY = /* GraphQL */ `
10731415
query GetCandidatePreferences {

0 commit comments

Comments
 (0)