Skip to content

Commit 7a49d2a

Browse files
committed
try a basic test
1 parent 1b3ff85 commit 7a49d2a

File tree

5 files changed

+185
-5
lines changed

5 files changed

+185
-5
lines changed

src/api/plugins/auth.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ const authPlugin: FastifyPluginAsync = async (fastify, _options) => {
237237
});
238238
}
239239
request.log.info(`authenticated request from ${request.username} `);
240+
request.userRoles = userRoles;
240241
return userRoles;
241242
},
242243
);

src/api/routes/stripe.ts

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
import { PutItemCommand } from "@aws-sdk/client-dynamodb";
2-
import { marshall } from "@aws-sdk/util-dynamodb";
1+
import {
2+
PutItemCommand,
3+
QueryCommand,
4+
ScanCommand,
5+
} from "@aws-sdk/client-dynamodb";
6+
import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
37
import {
48
createStripeLink,
59
StripeLinkCreateParams,
@@ -9,19 +13,63 @@ import { genericConfig } from "common/config.js";
913
import {
1014
InternalServerError,
1115
UnauthenticatedError,
16+
UnauthorizedError,
1217
} from "common/errors/index.js";
1318
import { AppRoles } from "common/roles.js";
1419
import {
1520
invoiceLinkPostResponseSchema,
1621
invoiceLinkPostRequestSchema,
22+
invoiceLinkGetResponseSchema,
1723
} from "common/types/stripe.js";
1824
import { FastifyPluginAsync } from "fastify";
19-
import { z } from "zod";
25+
import { object, z } from "zod";
2026
import { zodToJsonSchema } from "zod-to-json-schema";
2127

2228
const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => {
29+
fastify.get(
30+
"/paymentLinks",
31+
{
32+
schema: {
33+
response: { 200: zodToJsonSchema(invoiceLinkGetResponseSchema) },
34+
},
35+
onRequest: async (request, reply) => {
36+
await fastify.authorize(request, reply, [AppRoles.STRIPE_LINK_CREATOR]);
37+
},
38+
},
39+
async (request, reply) => {
40+
let dynamoCommand;
41+
if (request.userRoles?.has(AppRoles.BYPASS_OBJECT_LEVEL_AUTH)) {
42+
dynamoCommand = new ScanCommand({
43+
TableName: genericConfig.StripeLinksDynamoTableName,
44+
});
45+
} else {
46+
dynamoCommand = new QueryCommand({
47+
TableName: genericConfig.StripeLinksDynamoTableName,
48+
KeyConditionExpression: "userId = :userId",
49+
ExpressionAttributeValues: {
50+
":userId": { S: request.username! },
51+
},
52+
});
53+
}
54+
const result = await fastify.dynamoClient.send(dynamoCommand);
55+
if (result.Count === 0 || !result.Items) {
56+
return [];
57+
}
58+
const parsed = result.Items.map((item) => unmarshall(item)).map(
59+
(item) => ({
60+
id: item.linkId,
61+
userId: item.userId,
62+
link: item.url,
63+
active: item.active,
64+
invoiceId: item.invoiceId,
65+
invoiceAmountUsd: item.amount,
66+
}),
67+
);
68+
reply.status(200).send(parsed);
69+
},
70+
);
2371
fastify.post<{ Body: z.infer<typeof invoiceLinkPostRequestSchema> }>(
24-
"/paymentLink",
72+
"/paymentLinks",
2573
{
2674
schema: {
2775
response: { 201: zodToJsonSchema(invoiceLinkPostResponseSchema) },
@@ -66,7 +114,10 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => {
66114
linkId,
67115
priceId,
68116
productId,
117+
invoiceId,
69118
url,
119+
amount: request.body.invoiceAmountUsd,
120+
active: true,
70121
}),
71122
});
72123
await fastify.dynamoClient.send(dynamoCommand);

src/api/types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ declare module "fastify" {
3333
interface FastifyRequest {
3434
startTime: number;
3535
username?: string;
36+
userRoles?: Set<AppRoles>;
3637
tokenPayload?: AadToken;
3738
}
3839
}

src/common/types/stripe.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { z } from 'zod';
22

33
export const invoiceLinkPostResponseSchema = z.object({
4-
invoiceId: z.string().min(1),
4+
id: z.string().min(1),
55
link: z.string().url(),
66
})
77

@@ -11,3 +11,12 @@ export const invoiceLinkPostRequestSchema = z.object({
1111
contactName: z.string().min(1),
1212
contactEmail: z.string().email()
1313
})
14+
15+
export const invoiceLinkGetResponseSchema = z.array(z.object({
16+
id: z.string().min(1),
17+
userId: z.string().email(),
18+
link: z.string().url(),
19+
active: z.boolean(),
20+
invoiceId: z.string().min(1),
21+
invoiceAmountUsd: z.number().min(50)
22+
}))

tests/unit/stripe.test.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { afterAll, expect, test, beforeEach, vi, describe } from "vitest";
2+
import init from "../../src/api/index.js";
3+
import {
4+
GetSecretValueCommand,
5+
SecretsManagerClient,
6+
} from "@aws-sdk/client-secrets-manager";
7+
import { mockClient } from "aws-sdk-client-mock";
8+
import { secretJson } from "./secret.testdata.js";
9+
import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";
10+
import supertest from "supertest";
11+
import { createJwt } from "./auth.test.js";
12+
13+
const smMock = mockClient(SecretsManagerClient);
14+
const ddbMock = mockClient(DynamoDBClient);
15+
16+
vi.mock("stripe", () => {
17+
const productMock = { id: "prod_123" };
18+
const priceMock = { id: "price_123" };
19+
const paymentLinkMock = {
20+
id: "plink_123",
21+
url: "https://stripe.com/payment-link",
22+
};
23+
24+
return {
25+
default: vi.fn(() => ({
26+
products: {
27+
create: vi.fn().mockResolvedValue(productMock),
28+
},
29+
prices: {
30+
create: vi.fn().mockResolvedValue(priceMock),
31+
},
32+
paymentLinks: {
33+
create: vi.fn().mockResolvedValue(paymentLinkMock),
34+
},
35+
})),
36+
};
37+
});
38+
39+
const app = await init();
40+
describe("Test Stripe link creation", async () => {
41+
test("Test body validation 1", async () => {
42+
smMock.on(GetSecretValueCommand).resolves({
43+
SecretString: secretJson,
44+
});
45+
ddbMock.on(PutItemCommand).rejects();
46+
const testJwt = createJwt();
47+
await app.ready();
48+
49+
const response = await supertest(app.server)
50+
.post("/api/v1/stripe/paymentLinks")
51+
.set("authorization", `Bearer ${testJwt}`)
52+
.send({
53+
invoiceId: "",
54+
invoiceAmountUsd: 49,
55+
contactName: "",
56+
});
57+
expect(response.statusCode).toBe(400);
58+
expect(response.body).toStrictEqual({
59+
error: true,
60+
name: "ValidationError",
61+
id: 104,
62+
message:
63+
'String must contain at least 1 character(s) at "invoiceId"; Number must be greater than or equal to 50 at "invoiceAmountUsd"; String must contain at least 1 character(s) at "contactName"; Required at "contactEmail"',
64+
});
65+
});
66+
test("Test body validation 2", async () => {
67+
smMock.on(GetSecretValueCommand).resolves({
68+
SecretString: secretJson,
69+
});
70+
ddbMock.on(PutItemCommand).rejects();
71+
const testJwt = createJwt();
72+
await app.ready();
73+
74+
const response = await supertest(app.server)
75+
.post("/api/v1/stripe/paymentLinks")
76+
.set("authorization", `Bearer ${testJwt}`)
77+
.send({
78+
invoiceId: "ACM102",
79+
invoiceAmountUsd: 51,
80+
contactName: "Dev",
81+
contactEmail: "invalidEmail",
82+
});
83+
expect(response.statusCode).toBe(400);
84+
expect(response.body).toStrictEqual({
85+
error: true,
86+
name: "ValidationError",
87+
id: 104,
88+
message: 'Invalid email at "contactEmail"',
89+
});
90+
});
91+
test("Happy Path", async () => {
92+
ddbMock.on(PutItemCommand).resolves({});
93+
const testJwt = createJwt();
94+
await app.ready();
95+
96+
const response = await supertest(app.server)
97+
.post("/api/v1/stripe/paymentLinks")
98+
.set("authorization", `Bearer ${testJwt}`)
99+
.send({
100+
invoiceId: "ACM102",
101+
invoiceAmountUsd: 51,
102+
contactName: "Infra User",
103+
contactEmail: "[email protected]",
104+
});
105+
expect(response.statusCode).toBe(201);
106+
console.log(response.body);
107+
});
108+
afterAll(async () => {
109+
await app.close();
110+
});
111+
beforeEach(() => {
112+
(app as any).nodeCache.flushAll();
113+
vi.clearAllMocks();
114+
smMock.on(GetSecretValueCommand).resolves({
115+
SecretString: secretJson,
116+
});
117+
});
118+
});

0 commit comments

Comments
 (0)