Skip to content

Commit d24c864

Browse files
committed
write tests
1 parent ad93bbe commit d24c864

File tree

6 files changed

+147
-219
lines changed

6 files changed

+147
-219
lines changed

src/api/functions/entraId.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ export async function resolveEmailToOid(
180180
};
181181

182182
if (!data.value || data.value.length === 0) {
183-
throw new Error(`No user found with email: ${email}`);
183+
throw new EntraFetchError({ message: "No user found with email", email });
184184
}
185185

186186
return data.value[0].id;

src/api/functions/membership.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { FastifyBaseLogger, FastifyInstance } from "fastify";
2+
3+
export async function checkPaidMembership(
4+
endpoint: string,
5+
log: FastifyBaseLogger,
6+
netId: string,
7+
) {
8+
const membershipApiPayload = (await (
9+
await fetch(`${endpoint}?netId=${netId}`)
10+
).json()) as { netId: string; isPaidMember: boolean };
11+
log.trace(`Got Membership API Payload for ${netId}: ${membershipApiPayload}`);
12+
try {
13+
return membershipApiPayload["isPaidMember"];
14+
} catch (e: any) {
15+
log.error(`Failed to get response from membership API: ${e.toString()}`);
16+
throw e;
17+
}
18+
}

src/api/routes/mobileWallet.ts

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { FastifyPluginAsync } from "fastify";
22
import { issueAppleWalletMembershipCard } from "../functions/mobileWallet.js";
33
import {
4+
EntraFetchError,
45
UnauthenticatedError,
56
UnauthorizedError,
67
ValidationError,
78
} from "../../common/errors/index.js";
8-
import { generateMembershipEmailCommand } from "api/functions/ses.js";
9+
import { generateMembershipEmailCommand } from "../functions/ses.js";
910
import { z } from "zod";
10-
import { getEntraIdToken, getUserProfile } from "api/functions/entraId.js";
11+
import { getEntraIdToken, getUserProfile } from "../functions/entraId.js";
12+
import { checkPaidMembership } from "../functions/membership.js";
1113

1214
const mobileWalletRoute: FastifyPluginAsync = async (fastify, _options) => {
1315
fastify.get<{ Querystring: { email: string } }>(
@@ -41,32 +43,26 @@ const mobileWalletRoute: FastifyPluginAsync = async (fastify, _options) => {
4143
message: "Email query parameter is not a valid email",
4244
});
4345
}
44-
45-
const membershipApiPayload = (await (
46-
await fetch(
47-
`${fastify.environmentConfig.MembershipApiEndpoint}?netId=${request.query.email.replace("@illinois.edu", "")}`,
48-
)
49-
).json()) as { netId: string; isPaidMember: boolean };
50-
try {
51-
if (!membershipApiPayload["isPaidMember"]) {
52-
throw new UnauthorizedError({
53-
message: "User is not a paid member.",
54-
});
55-
}
56-
} catch (e: any) {
57-
request.log.error(
58-
`Failed to get response from membership API: ${e.toString()}`,
59-
);
60-
throw e;
46+
const isPaidMember = await checkPaidMembership(
47+
fastify.environmentConfig.MembershipApiEndpoint,
48+
request.log,
49+
request.query.email.replace("@illinois.edu", ""),
50+
);
51+
if (!isPaidMember) {
52+
throw new UnauthenticatedError({
53+
message: `${request.query.email} is not a paid member.`,
54+
});
6155
}
6256
const entraIdToken = await getEntraIdToken(
6357
fastify,
6458
fastify.environmentConfig.AadValidClientId,
6559
);
60+
6661
const userProfile = await getUserProfile(
6762
entraIdToken,
6863
request.query.email,
6964
);
65+
7066
const item = await issueAppleWalletMembershipCard(
7167
fastify,
7268
request,

tests/unit/mobileWallet.test.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { afterAll, expect, test, beforeEach, vi } from "vitest";
2+
import { SendRawEmailCommand, SESClient } from "@aws-sdk/client-ses";
3+
import { mockClient } from "aws-sdk-client-mock";
4+
import { secretObject } from "./secret.testdata.js";
5+
import init from "../../src/api/index.js";
6+
import { describe } from "node:test";
7+
import { EntraFetchError } from "../../src/common/errors/index.js";
8+
9+
const sesMock = mockClient(SESClient);
10+
const jwt_secret = secretObject["jwt_key"];
11+
vi.stubEnv("JwtSigningKey", jwt_secret);
12+
13+
vi.mock("fs", () => {
14+
return {
15+
...vi.importActual("fs"),
16+
promises: {
17+
readFile: vi.fn(() => {
18+
return "";
19+
}),
20+
},
21+
};
22+
});
23+
24+
vi.mock("../../src/api/functions/membership.js", () => {
25+
return {
26+
...vi.importActual("../../src/api/functions/membership.js"),
27+
checkPaidMembership: vi.fn(
28+
(_endpoint: string, _log: any, netId: string) => {
29+
if (netId === "valid") {
30+
return true;
31+
}
32+
return false;
33+
},
34+
),
35+
};
36+
});
37+
38+
vi.mock("../../src/api/functions/entraId.js", () => {
39+
return {
40+
...vi.importActual("../../src/api/functions/entraId.js"),
41+
getEntraIdToken: vi.fn().mockImplementation(async () => {
42+
return "atokenofalltime";
43+
}),
44+
getUserProfile: vi
45+
.fn()
46+
.mockImplementation(async (_token: string, email: string) => {
47+
if (email === "[email protected]") {
48+
return { displayName: "John Doe" };
49+
}
50+
throw new EntraFetchError({
51+
message: "User not found",
52+
email,
53+
});
54+
}),
55+
resolveEmailToOid: vi.fn().mockImplementation(async () => {
56+
return "12345";
57+
}),
58+
};
59+
});
60+
61+
const app = await init();
62+
describe("Mobile wallet pass issuance", async () => {
63+
test("Test that passes will not be issued for non-emails", async () => {
64+
const response = await app.inject({
65+
method: "GET",
66+
url: "/api/v1/mobileWallet/membership?email=notanemail",
67+
});
68+
expect(response.statusCode).toBe(400);
69+
await response.json();
70+
});
71+
test("Test that passes will not be issued for non-members", async () => {
72+
const response = await app.inject({
73+
method: "GET",
74+
url: "/api/v1/mobileWallet/[email protected]",
75+
});
76+
expect(response.statusCode).toBe(403);
77+
await response.json();
78+
});
79+
test("Test that passes will be issued for members", async () => {
80+
sesMock.on(SendRawEmailCommand).resolves({});
81+
const response = await app.inject({
82+
method: "GET",
83+
url: "/api/v1/mobileWallet/[email protected]",
84+
});
85+
expect(response.statusCode).toBe(202);
86+
});
87+
test("Test that SES errors result in a server error", async () => {
88+
sesMock.on(SendRawEmailCommand).rejects({});
89+
const response = await app.inject({
90+
method: "GET",
91+
url: "/api/v1/mobileWallet/[email protected]",
92+
});
93+
expect(response.statusCode).toBe(500);
94+
});
95+
afterAll(async () => {
96+
await app.close();
97+
});
98+
beforeEach(() => {
99+
(app as any).nodeCache.flushAll();
100+
vi.clearAllMocks();
101+
});
102+
});

tests/unit/secret.testdata.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ const secretObject = {
66
discord_bot_token: "12345",
77
entra_id_private_key: "",
88
entra_id_thumbprint: "",
9+
acm_passkit_signerCert_base64: "",
10+
acm_passkit_signerKey_base64: "",
11+
apple_signing_cert_base64: "",
912
} as SecretConfig & { jwt_key: string };
1013

1114
const secretJson = JSON.stringify(secretObject);

0 commit comments

Comments
 (0)