Skip to content

Commit 9e6ba13

Browse files
committed
[Fix] fixed RSVP get and post routes
1 parent 9e2f331 commit 9e6ba13

File tree

7 files changed

+215
-212
lines changed

7 files changed

+215
-212
lines changed

src/api/routes/rsvp.ts

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { FastifyPluginAsync } from "fastify";
22
import rateLimiter from "api/plugins/rateLimiter.js";
33
import { withRoles, withTags } from "api/components/index.js";
4-
import { QueryCommand } from "@aws-sdk/client-dynamodb";
5-
import { unmarshall } from "@aws-sdk/util-dynamodb";
6-
import { getUserOrgRoles } from "api/functions/organizations.js";
4+
import { QueryCommand, PutItemCommand } from "@aws-sdk/client-dynamodb";
5+
import { unmarshall, marshall } from "@aws-sdk/util-dynamodb";
76
import {
7+
DatabaseFetchError,
88
UnauthenticatedError,
99
UnauthorizedError,
1010
ValidationError,
@@ -14,14 +14,7 @@ import { verifyUiucAccessToken } from "api/functions/uin.js";
1414
import { checkPaidMembership } from "api/functions/membership.js";
1515
import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
1616
import { genericConfig } from "common/config.js";
17-
18-
const rsvpItemSchema = z.object({
19-
eventId: z.string(),
20-
userId: z.string(),
21-
isPaidMember: z.boolean(),
22-
createdAt: z.string(),
23-
});
24-
const rsvpListSchema = z.array(rsvpItemSchema);
17+
import { AppRoles } from "common/roles.js";
2518

2619
const rsvpRoutes: FastifyPluginAsync = async (fastify, _options) => {
2720
await fastify.register(rateLimiter, {
@@ -38,6 +31,9 @@ const rsvpRoutes: FastifyPluginAsync = async (fastify, _options) => {
3831
eventId: z.string().min(1).meta({
3932
description: "The previously-created event ID in the events API.",
4033
}),
34+
orgId: z.string().min(1).meta({
35+
description: "The organization ID the event belongs to.",
36+
}),
4137
}),
4238
headers: z.object({
4339
"x-uiuc-token": z.jwt().min(1).meta({
@@ -76,41 +72,53 @@ const rsvpRoutes: FastifyPluginAsync = async (fastify, _options) => {
7672
isPaidMember,
7773
createdAt: "",
7874
};
75+
const putCommand = new PutItemCommand({
76+
TableName: genericConfig.RSVPDynamoTableName,
77+
Item: marshall(entry),
78+
});
79+
await fastify.dynamoClient.send(putCommand);
80+
return reply.status(201).send(entry);
7981
},
8082
);
8183
fastify.withTypeProvider<FastifyZodOpenApiTypeProvider>().get(
8284
"/:orgId/event/:eventId",
8385
{
84-
schema: withTags(["RSVP"], {
85-
summary: "Get all RSVPs for an event.",
86-
params: z.object({
87-
eventId: z.string().min(1).meta({
88-
description: "The previously-created event ID in the events API.",
89-
}),
90-
orgId: z.string().min(1).meta({
91-
description: "The organization ID the event belongs to.",
92-
}),
93-
}),
94-
headers: z.object({
95-
"x-uiuc-token": z.jwt().min(1).meta({
96-
description:
97-
"An access token for the user in the UIUC Entra ID tenant.",
86+
schema: withRoles(
87+
[AppRoles.VIEW_RSVPS],
88+
withTags(["RSVP"], {
89+
summary: "Get all RSVPs for an event.",
90+
params: z.object({
91+
eventId: z.string().min(1).meta({
92+
description: "The previously-created event ID in the events API.",
93+
}),
94+
orgId: z.string().min(1).meta({
95+
description: "The organization ID the event belongs to.",
96+
}),
9897
}),
9998
}),
100-
}),
99+
),
100+
onRequest: fastify.authorizeFromSchema,
101101
},
102102
async (request, reply) => {
103-
const commnand = new QueryCommand({
104-
TableName: genericConfig.EventsDynamoTableName,
103+
const command = new QueryCommand({
104+
TableName: genericConfig.RSVPDynamoTableName,
105105
IndexName: "EventIdIndex",
106106
KeyConditionExpression: "eventId = :eid",
107107
ExpressionAttributeValues: {
108108
":eid": { S: request.params.eventId },
109109
},
110110
});
111-
const response = await fastify.dynamoClient.send(commnand);
112-
const items = response.Items?.map((item) => unmarshall(item)) || [];
113-
return reply.send(items as z.infer<typeof rsvpListSchema>);
111+
const response = await fastify.dynamoClient.send(command);
112+
if (!response || !response.Items) {
113+
throw new DatabaseFetchError({
114+
message: "Failed to get all member lists.",
115+
});
116+
}
117+
const rsvps = response.Items.map((x) => unmarshall(x));
118+
const uniqueRsvps = [
119+
...new Map(rsvps.map((item) => [item.userId, item])).values()
120+
];
121+
return reply.send(uniqueRsvps);
114122
},
115123
);
116124
};

src/common/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export type ConfigType = {
3838

3939
export type GenericConfigType = {
4040
EventsDynamoTableName: string;
41+
RSVPDynamoTableName: string;
4142
CacheDynamoTableName: string;
4243
LinkryDynamoTableName: string;
4344
StripeLinksDynamoTableName: string;
@@ -84,6 +85,7 @@ export const commChairsGroupId = "105e7d32-7289-435e-a67a-552c7f215507";
8485

8586
const genericConfig: GenericConfigType = {
8687
EventsDynamoTableName: "infra-core-api-events",
88+
RSVPDynamoTableName: "infra-core-api-events-rsvp",
8789
StripeLinksDynamoTableName: "infra-core-api-stripe-links",
8890
StripePaymentsDynamoTableName: "infra-core-api-stripe-payments",
8991
CacheDynamoTableName: "infra-core-api-cache",

src/common/roles.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export const META_ROLE_PREFIX = "__metaRole:"
77

88
export enum BaseRoles {
99
EVENTS_MANAGER = "manage:events",
10+
RSVPS_MANAGER = "manage:rsvps",
11+
VIEW_RSVPS = "view:rsvps",
1012
TICKETS_SCANNER = "scan:tickets",
1113
TICKETS_MANAGER = "manage:tickets",
1214
IAM_ADMIN = "admin:iam",

src/common/types/rsvp.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import * as z from "zod/v4";
2+
3+
export const rsvpItemSchema = z.object({
4+
eventId: z.string(),
5+
userId: z.string(),
6+
isPaidMember: z.boolean(),
7+
createdAt: z.string(),
8+
});

terraform/modules/dynamo/main.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -568,7 +568,7 @@ resource "aws_dynamodb_table" "store_limits" {
568568
}
569569
}
570570

571-
resource "aws_dynamodb_table" "store_limits" {
571+
resource "aws_dynamodb_table" "events_rsvp" {
572572
region = "us-east-2"
573573
billing_mode = "PAY_PER_REQUEST"
574574
name = "${var.ProjectId}-events-rsvp"

tests/unit/rsvps.test.ts

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { expect, test, vi, describe, beforeEach } from "vitest";
2+
import {
3+
DynamoDBClient,
4+
PutItemCommand,
5+
QueryCommand,
6+
} from "@aws-sdk/client-dynamodb";
7+
import { marshall } from "@aws-sdk/util-dynamodb";
8+
import { mockClient } from "aws-sdk-client-mock";
9+
import init from "../../src/api/index.js";
10+
import { createJwt } from "./auth.test.js";
11+
import { testSecretObject } from "./secret.testdata.js";
12+
import { Redis } from "../../src/api/types.js";
13+
import { FastifyBaseLogger } from "fastify";
14+
15+
const DUMMY_JWT =
16+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
17+
18+
vi.mock("../../src/api/functions/uin.js", async () => {
19+
const actual = await vi.importActual("../../src/api/functions/uin.js");
20+
return {
21+
...actual,
22+
verifyUiucAccessToken: vi
23+
.fn()
24+
.mockImplementation(
25+
async ({
26+
token,
27+
logger,
28+
}: {
29+
token: string;
30+
logger: FastifyBaseLogger;
31+
}) => {
32+
if (token === DUMMY_JWT) {
33+
console.log("DUMMY_JWT matched in mock implementation");
34+
}
35+
return {
36+
userPrincipalName: "[email protected]",
37+
givenName: "John",
38+
surname: "Doe",
39+
40+
};
41+
},
42+
),
43+
};
44+
});
45+
46+
vi.mock("../../src/api/functions/membership.js", async () => {
47+
const actual = await vi.importActual("../../src/api/functions/membership.js");
48+
return {
49+
...actual,
50+
checkPaidMembership: vi
51+
.fn()
52+
.mockImplementation(
53+
async ({
54+
netId,
55+
redisClient,
56+
dynamoClient,
57+
logger,
58+
}: {
59+
netId: string;
60+
redisClient: Redis;
61+
dynamoClient: DynamoDBClient;
62+
logger: FastifyBaseLogger;
63+
}) => {
64+
if (netId === "jd3") {
65+
return true;
66+
}
67+
return false;
68+
},
69+
),
70+
};
71+
});
72+
73+
const ddbMock = mockClient(DynamoDBClient);
74+
const jwt_secret = testSecretObject["jwt_key"];
75+
vi.stubEnv("JwtSigningKey", jwt_secret);
76+
77+
const app = await init();
78+
79+
describe("RSVP API tests", () => {
80+
beforeEach(() => {
81+
ddbMock.reset();
82+
vi.clearAllMocks();
83+
});
84+
85+
test("Test posting an RSVP for an event", async () => {
86+
ddbMock.on(PutItemCommand).resolves({});
87+
88+
const testJwt = createJwt();
89+
const mockUpn = "[email protected]";
90+
const eventId = "Make Your Own Database";
91+
const orgId = "SIGDatabase";
92+
93+
const response = await app.inject({
94+
method: "POST",
95+
url: `/api/v1/rsvp/${orgId}/event/${encodeURIComponent(eventId)}`,
96+
headers: {
97+
Authorization: `Bearer ${testJwt}`,
98+
"x-uiuc-token": DUMMY_JWT,
99+
},
100+
});
101+
102+
if (response.statusCode !== 201) {
103+
console.log("Test Failed Response:", response.body);
104+
}
105+
106+
expect(response.statusCode).toBe(201);
107+
108+
const body = JSON.parse(response.body);
109+
expect(body.userId).toBe(mockUpn);
110+
expect(body.eventId).toBe(eventId);
111+
expect(body.isPaidMember).toBe(true);
112+
expect(body.partitionKey).toBe(`${eventId}#${mockUpn}`);
113+
114+
expect(ddbMock.calls()).toHaveLength(1);
115+
const putItemInput = ddbMock.call(0).args[0].input as any;
116+
expect(putItemInput.TableName).toBe("infra-core-api-events-rsvp");
117+
});
118+
119+
test("Test getting RSVPs for an event (Mocking Query Response)", async () => {
120+
const eventId = "Make Your Own Database";
121+
const orgId = "SIGDatabase";
122+
const mockRsvps = [
123+
{
124+
eventId,
125+
userId: "[email protected]",
126+
isPaidMember: true,
127+
createdAt: "2023-01-01",
128+
},
129+
{
130+
eventId,
131+
userId: "[email protected]",
132+
isPaidMember: false,
133+
createdAt: "2023-01-02",
134+
},
135+
];
136+
ddbMock.on(QueryCommand).resolves({
137+
Items: mockRsvps.map((item) => marshall(item)),
138+
});
139+
140+
const adminJwt = await createJwt();
141+
142+
const response = await app.inject({
143+
method: "GET",
144+
url: `/api/v1/rsvp/${orgId}/event/${encodeURIComponent(eventId)}`,
145+
headers: {
146+
Authorization: `Bearer ${adminJwt}`,
147+
},
148+
});
149+
150+
expect(response.statusCode).toBe(200);
151+
const body = JSON.parse(response.body);
152+
153+
expect(body).toHaveLength(2);
154+
expect(body[0].userId).toBe("[email protected]");
155+
expect(body[1].userId).toBe("[email protected]");
156+
});
157+
});

0 commit comments

Comments
 (0)