From 88799b6e928af675b64819e96a1a473079a37a71 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sat, 12 Jul 2025 22:24:04 -0400 Subject: [PATCH 01/10] Documentation Updates --- cloudformation/main.yml | 2 +- src/api/components/index.ts | 30 +++++++++++++++++++---- src/api/createSwagger.ts | 9 +++---- src/api/index.ts | 44 +++++++++++++++++++++++++++++----- src/api/package.json | 1 - src/api/routes/mobileWallet.ts | 4 ++++ src/common/roles.ts | 16 +++++++++++++ yarn.lock | 15 ++---------- 8 files changed, 91 insertions(+), 30 deletions(-) diff --git a/cloudformation/main.yml b/cloudformation/main.yml index c7fb4fb0..56495c23 100644 --- a/cloudformation/main.yml +++ b/cloudformation/main.yml @@ -845,7 +845,7 @@ Resources: Origins: - Id: LambdaOrigin DomainName: !Select [0, !Split ['/', !Select [1, !Split ['https://', !GetAtt AppLambdaUrl.FunctionUrl]]]] - OriginPath: "/api/v1/ical" + OriginPath: "/api/v1/ical/" CustomOriginConfig: HTTPPort: 80 HTTPSPort: 443 diff --git a/src/api/components/index.ts b/src/api/components/index.ts index 46c5a660..fef0d961 100644 --- a/src/api/components/index.ts +++ b/src/api/components/index.ts @@ -1,4 +1,4 @@ -import { AppRoles } from "common/roles.js"; +import { AppRoleHumanMapper, AppRoles } from "common/roles.js"; import { FastifyZodOpenApiSchema } from "fastify-zod-openapi"; import * as z from "zod/v4"; import { CoreOrganizationList } from "@acm-uiuc/js-shared"; @@ -208,10 +208,30 @@ export function withRoles( security, "x-required-roles": roles, "x-disable-api-key-auth": disableApiKeyAuth, - description: - roles.length > 0 - ? `${disableApiKeyAuth ? "API key authentication is not permitted for this route.\n\n" : ""}Requires one of the following roles: ${roles.join(", ")}.${schema.description ? `\n\n${schema.description}` : ""}` - : "Requires valid authentication but no specific role.", + description: ` +${ + disableApiKeyAuth + ? ` +> [!important] +> This resource cannot be accessed with an API key. +` + : "" +} + +${ + schema.description + ? ` +#### Description +
+${schema.description} +` + : "" +} + +#### Authorization +
+${roles.length > 0 ? `Requires any of the following roles:\n\n${roles.map((item) => `* ${AppRoleHumanMapper[item]} (${item})`).join("\n")}` : "Requires valid authentication but no specific authorization."} + `, ...schema, response: responses, }; diff --git a/src/api/createSwagger.ts b/src/api/createSwagger.ts index a76b7fba..a69952c9 100644 --- a/src/api/createSwagger.ts +++ b/src/api/createSwagger.ts @@ -3,7 +3,7 @@ import path from "node:path"; import { writeFile, mkdir } from "fs/promises"; import init from "./index.js"; // Assuming this is your Fastify app initializer -const html = ` +export const docsHtml = ` @@ -157,7 +157,7 @@ async function createSwaggerFiles() { const yamlSpec = app.swagger({ yaml: true }); await writeFile(path.join(outputDir, "openapi.json"), jsonSpec); await writeFile(path.join(outputDir, "openapi.yaml"), yamlSpec); - await writeFile(path.join(outputDir, "index.html"), html); + await writeFile(path.join(outputDir, "index.html"), docsHtml); console.log(`✅ Swagger files successfully generated in ${outputDir}`); await app.close(); @@ -166,5 +166,6 @@ async function createSwaggerFiles() { process.exit(1); } } - -createSwaggerFiles(); +if (import.meta.url === `file://createSwagger.ts`) { + createSwaggerFiles(); +} diff --git a/src/api/index.ts b/src/api/index.ts index 1ca0ecca..fb08c1db 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -60,6 +60,7 @@ import protectedRoute from "./routes/protected.js"; import eventsPlugin from "./routes/events.js"; import mobileWalletV2Route from "./routes/v2/mobileWallet.js"; import membershipV2Plugin from "./routes/v2/membership.js"; +import { docsHtml } from "./createSwagger.js"; /** END ROUTES */ export const instanceId = randomUUID(); @@ -114,14 +115,31 @@ async function init(prettyPrint: boolean = false, initClients: boolean = true) { if (!isRunningInLambda) { try { const fastifySwagger = import("@fastify/swagger"); - const fastifySwaggerUI = import("@fastify/swagger-ui"); await app.register(fastifySwagger, { openapi: { info: { title: "ACM @ UIUC Core API", - description: - "The ACM @ UIUC Core API provides services for managing chapter operations.", - version: "2.0.0", + description: ` +The ACM @ UIUC Core API provides services for managing chapter operations. + +## Usage + +The primary consumer of the Core API is the Management Portal, which allows members to manage the chapter. +Others may call the API with an API key; please contact us to obtain one. + +This API also integrates into the ACM website and other suborganization to provide calendar services. + +Calendar clients call the iCal endpoints (available through [ical.acm.illinois.edu](https://ical.acm.illinois.edu)) for calendar services. + +## Contact +
+ +If you are an ACM @ UIUC member, please join the Infra Committee Discord for support. +Otherwise, email [infra@acm.illinois.edu](mailto:infra@acm.illinois.edu) for support. + +**For all security concerns, please email [infra@acm.illinois.edu](mailto:infra@acm.illinois.edu) with the subject "Security Concern".** +`, + version: "2.0.1", contact: { name: "ACM @ UIUC Infrastructure Team", email: "infra@acm.illinois.edu", @@ -215,9 +233,23 @@ async function init(prettyPrint: boolean = false, initClients: boolean = true) { transform: fastifyZodOpenApiTransform, transformObject: fastifyZodOpenApiTransformObject, }); - await app.register(fastifySwaggerUI, { - routePrefix: "/api/documentation", + app.get("/docs", { schema: { hide: true } }, (_request, reply) => { + reply.type("text/html").send(docsHtml); }); + app.get( + "/docs/openapi.json", + { schema: { hide: true } }, + (_request, reply) => { + reply.send(app.swagger()); + }, + ); + app.get( + "/docs/openapi.yml", + { schema: { hide: true } }, + (_request, reply) => { + reply.send(app.swagger({ yaml: true })); + }, + ); isSwaggerServer = true; } catch (e) { app.log.error(e); diff --git a/src/api/package.json b/src/api/package.json index 04e03f37..219eeea8 100644 --- a/src/api/package.json +++ b/src/api/package.json @@ -63,7 +63,6 @@ "@fastify/swagger": "^9.5.0" }, "devDependencies": { - "@fastify/swagger-ui": "^5.2.2", "@tsconfig/node22": "^22.0.1", "@types/aws-lambda": "^8.10.149", "@types/qrcode": "^1.5.5", diff --git a/src/api/routes/mobileWallet.ts b/src/api/routes/mobileWallet.ts index ec6bb3fc..0b730144 100644 --- a/src/api/routes/mobileWallet.ts +++ b/src/api/routes/mobileWallet.ts @@ -43,6 +43,10 @@ const mobileWalletRoute: FastifyPluginAsync = async (fastify, _options) => { }), }, async (request, reply) => { + reply.header( + "Deprecation", + "The V1 endpoint will soon be deprecated. Please use the V2 endpoint moving forward.", + ); const isPaidMember = (fastify.runEnvironment === "dev" && request.query.email === "testinguser@illinois.edu") || diff --git a/src/common/roles.ts b/src/common/roles.ts index cb99cf6f..23b7dc90 100644 --- a/src/common/roles.ts +++ b/src/common/roles.ts @@ -19,3 +19,19 @@ export enum AppRoles { export const allAppRoles = Object.values(AppRoles).filter( (value) => typeof value === "string", ); + +export const AppRoleHumanMapper: Record = { + [AppRoles.EVENTS_MANAGER]: "Events Manager", + [AppRoles.TICKETS_SCANNER]: "Tickets Scanner", + [AppRoles.TICKETS_MANAGER]: "Tickets Manager", + [AppRoles.IAM_ADMIN]: "IAM Admin", + [AppRoles.IAM_INVITE_ONLY]: "IAM Inviter", + [AppRoles.LINKS_MANAGER]: "Links Manager", + [AppRoles.LINKS_ADMIN]: "Links Admin", + [AppRoles.STRIPE_LINK_CREATOR]: "Stripe Link Creator", + [AppRoles.BYPASS_OBJECT_LEVEL_AUTH]: "Object Level Auth Bypass", + [AppRoles.ROOM_REQUEST_CREATE]: "Room Request Creator", + [AppRoles.ROOM_REQUEST_UPDATE]: "Room Request Updater", + [AppRoles.AUDIT_LOG_VIEWER]: "Audit Log Viewer", + [AppRoles.MANAGE_ORG_API_KEYS]: "Org API Keys Manager" +} diff --git a/yarn.lock b/yarn.lock index a114b71f..6fcbb34b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1457,7 +1457,7 @@ http-errors "^2.0.0" mime "^3" -"@fastify/static@^8.0.0", "@fastify/static@^8.1.1": +"@fastify/static@^8.1.1": version "8.2.0" resolved "https://registry.yarnpkg.com/@fastify/static/-/static-8.2.0.tgz#5ad4878f13f415d1ee78448020a6f522ac7af595" integrity sha512-PejC/DtT7p1yo3p+W7LiUtLMsV8fEvxAK15sozHy9t8kwo5r0uLYmhV/inURmGz1SkHZFz/8CNtHLPyhKcx4SQ== @@ -1469,17 +1469,6 @@ fastq "^1.17.1" glob "^11.0.0" -"@fastify/swagger-ui@^5.2.2": - version "5.2.3" - resolved "https://registry.yarnpkg.com/@fastify/swagger-ui/-/swagger-ui-5.2.3.tgz#3dc7f5ed3c226f55d894cdc2d45490efd22f3c4a" - integrity sha512-e7ivEJi9EpFcxTONqICx4llbpB2jmlI+LI1NQ/mR7QGQnyDOqZybPK572zJtcdHZW4YyYTBHcP3a03f1pOh0SA== - dependencies: - "@fastify/static" "^8.0.0" - fastify-plugin "^5.0.0" - openapi-types "^12.1.3" - rfdc "^1.3.1" - yaml "^2.4.1" - "@fastify/swagger@^9.5.0": version "9.5.1" resolved "https://registry.yarnpkg.com/@fastify/swagger/-/swagger-9.5.1.tgz#8ec9e2e6e8390a674cf3da9f339c9e59466e46fa" @@ -10433,7 +10422,7 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^2.4.1, yaml@^2.4.2: +yaml@^2.4.2: version "2.8.0" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.0.tgz#15f8c9866211bdc2d3781a0890e44d4fa1a5fff6" integrity sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ== From a8fb07be1e7b5974311adef5bfa8e69dd77e6199 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sat, 12 Jul 2025 22:30:41 -0400 Subject: [PATCH 02/10] fix unit tests --- tests/unit/documentation.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/documentation.test.ts b/tests/unit/documentation.test.ts index 94962fbf..c10e846d 100644 --- a/tests/unit/documentation.test.ts +++ b/tests/unit/documentation.test.ts @@ -5,7 +5,7 @@ const app = await init(); test("Test getting OpenAPI JSON", async () => { const response = await app.inject({ method: "GET", - url: "/api/documentation/json", + url: "/docs/openapi.json", }); expect(response.statusCode).toBe(200); const responseDataJson = await response.json(); @@ -19,7 +19,7 @@ afterAll(async () => { test("Test getting OpenAPI UI", async () => { const response = await app.inject({ method: "GET", - url: "/api/documentation", + url: "/docs", }); expect(response.statusCode).toBe(200); const contentType = response.headers["content-type"]; From 363a9af882c0dfbf9af22d618c4a494324c93936 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sat, 12 Jul 2025 22:40:12 -0400 Subject: [PATCH 03/10] fix cfn --- cloudformation/main.yml | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/cloudformation/main.yml b/cloudformation/main.yml index 56495c23..d16f9900 100644 --- a/cloudformation/main.yml +++ b/cloudformation/main.yml @@ -845,7 +845,17 @@ Resources: Origins: - Id: LambdaOrigin DomainName: !Select [0, !Split ['/', !Select [1, !Split ['https://', !GetAtt AppLambdaUrl.FunctionUrl]]]] - OriginPath: "/api/v1/ical/" + OriginPath: "/api/v1/ical" + CustomOriginConfig: + HTTPPort: 80 + HTTPSPort: 443 + OriginProtocolPolicy: https-only + OriginCustomHeaders: + - HeaderName: X-Origin-Verify + HeaderValue: !Join ['-', ['secret', !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]]] + - Id: ACMRootOrigi + DomainName: !Select [0, !Split ['/', !Select [1, !Split ['https://', !GetAtt AppLambdaUrl.FunctionUrl]]]] + OriginPath: "/api/v1/ical/ACM" CustomOriginConfig: HTTPPort: 80 HTTPSPort: 443 @@ -883,6 +893,28 @@ Resources: Forward: none CachePolicyId: !Ref CloudfrontCachePolicy OriginRequestPolicyId: b689b0a8-53d0-40ab-baf2-68738e2966ac + CacheBehaviors: + - PathPattern: "/" + Compress: true + TargetOriginId: ACMRootOrigin + ViewerProtocolPolicy: redirect-to-https + AllowedMethods: + - GET + - HEAD + - OPTIONS + - PUT + - POST + - DELETE + - PATCH + CachedMethods: + - GET + - HEAD + ForwardedValues: + QueryString: false + Cookies: + Forward: none + CachePolicyId: !Ref CloudfrontCachePolicy + OriginRequestPolicyId: b689b0a8-53d0-40ab-baf2-68738e2966ac ViewerCertificate: AcmCertificateArn: !FindInMap - ApiGwConfig From 05b25e5e37f5af0746953ca7fd4473d61e1513fd Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sat, 12 Jul 2025 22:51:35 -0400 Subject: [PATCH 04/10] fix cfn and live test --- cloudformation/main.yml | 2 +- tests/live/ical.test.ts | 37 ++++++++++++++++++++++++++----------- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/cloudformation/main.yml b/cloudformation/main.yml index d16f9900..0f7851fe 100644 --- a/cloudformation/main.yml +++ b/cloudformation/main.yml @@ -853,7 +853,7 @@ Resources: OriginCustomHeaders: - HeaderName: X-Origin-Verify HeaderValue: !Join ['-', ['secret', !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]]] - - Id: ACMRootOrigi + - Id: ACMRootOrigin DomainName: !Select [0, !Split ['/', !Select [1, !Split ['https://', !GetAtt AppLambdaUrl.FunctionUrl]]]] OriginPath: "/api/v1/ical/ACM" CustomOriginConfig: diff --git a/tests/live/ical.test.ts b/tests/live/ical.test.ts index f7cc2c6d..192d3bc0 100644 --- a/tests/live/ical.test.ts +++ b/tests/live/ical.test.ts @@ -47,14 +47,29 @@ describe( }, ); -test("Check that the ical base works", { timeout: 45000 }, async () => { - const response = await fetchWithRateLimit( - `${baseEndpoint.replace("core", "ical")}/ACM`, - ); - expect(response.status).toBe(200); - expect(response.headers.get("Content-Disposition")).toEqual( - 'attachment; filename="calendar.ics"', - ); - const calendar = ical.sync.parseICS(await response.text()); - expect(calendar["vcalendar"]["type"]).toEqual("VCALENDAR"); -}); +test( + "Check that the ical base works and uses a default host of ACM", + { timeout: 45000 }, + async () => { + const response = await fetchWithRateLimit( + `${baseEndpoint.replace("core", "ical")}/ACM`, + ); + const responseBase = await fetchWithRateLimit( + `${baseEndpoint.replace("core", "ical")}`, + ); + expect(response.status).toBe(200); + expect(responseBase.status).toBe(200); + expect(response.headers.get("Content-Disposition")).toEqual( + 'attachment; filename="calendar.ics"', + ); + expect(responseBase.headers.get("Content-Disposition")).toEqual( + 'attachment; filename="calendar.ics"', + ); + const text1 = await response.text(); + const text2 = await responseBase.text(); + expect(text1).toStrictEqual(text2); + + const calendar = ical.sync.parseICS(text1); + expect(calendar["vcalendar"]["type"]).toEqual("VCALENDAR"); + }, +); From ba1b146dc370d3b5ffc2cd5385d12c78a7d6778f Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sat, 12 Jul 2025 23:02:41 -0400 Subject: [PATCH 05/10] fixup docs --- cloudformation/main.yml | 32 -------------------------------- src/api/index.ts | 2 ++ tests/live/documentation.test.ts | 4 ++-- 3 files changed, 4 insertions(+), 34 deletions(-) diff --git a/cloudformation/main.yml b/cloudformation/main.yml index 0f7851fe..c7fb4fb0 100644 --- a/cloudformation/main.yml +++ b/cloudformation/main.yml @@ -853,16 +853,6 @@ Resources: OriginCustomHeaders: - HeaderName: X-Origin-Verify HeaderValue: !Join ['-', ['secret', !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]]] - - Id: ACMRootOrigin - DomainName: !Select [0, !Split ['/', !Select [1, !Split ['https://', !GetAtt AppLambdaUrl.FunctionUrl]]]] - OriginPath: "/api/v1/ical/ACM" - CustomOriginConfig: - HTTPPort: 80 - HTTPSPort: 443 - OriginProtocolPolicy: https-only - OriginCustomHeaders: - - HeaderName: X-Origin-Verify - HeaderValue: !Join ['-', ['secret', !Select [4, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]]] Enabled: true Aliases: - !Join @@ -893,28 +883,6 @@ Resources: Forward: none CachePolicyId: !Ref CloudfrontCachePolicy OriginRequestPolicyId: b689b0a8-53d0-40ab-baf2-68738e2966ac - CacheBehaviors: - - PathPattern: "/" - Compress: true - TargetOriginId: ACMRootOrigin - ViewerProtocolPolicy: redirect-to-https - AllowedMethods: - - GET - - HEAD - - OPTIONS - - PUT - - POST - - DELETE - - PATCH - CachedMethods: - - GET - - HEAD - ForwardedValues: - QueryString: false - Cookies: - Forward: none - CachePolicyId: !Ref CloudfrontCachePolicy - OriginRequestPolicyId: b689b0a8-53d0-40ab-baf2-68738e2966ac ViewerCertificate: AcmCertificateArn: !FindInMap - ApiGwConfig diff --git a/src/api/index.ts b/src/api/index.ts index fb08c1db..5299c992 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -92,6 +92,8 @@ async function init(prettyPrint: boolean = false, initClients: boolean = true) { level: process.env.LOG_LEVEL || "info", transport, }, + ignoreTrailingSlash: true, + ignoreDuplicateSlashes: true, disableRequestLogging: true, genReqId: (request) => { const header = request.headers["x-apigateway-event"]; diff --git a/tests/live/documentation.test.ts b/tests/live/documentation.test.ts index 1b77528b..8985adf9 100644 --- a/tests/live/documentation.test.ts +++ b/tests/live/documentation.test.ts @@ -4,7 +4,7 @@ import { getBaseEndpoint } from "./utils.js"; const baseEndpoint = getBaseEndpoint(); test("Get OpenAPI JSON", async () => { - const response = await fetch(`${baseEndpoint}/api/documentation/json`); + const response = await fetch(`${baseEndpoint}/docs/openapi.json`); expect(response.status).toBe(200); const responseDataJson = await response.json(); @@ -13,7 +13,7 @@ test("Get OpenAPI JSON", async () => { }); test("Get OpenAPI UI", async () => { - const response = await fetch(`${baseEndpoint}/api/documentation`); + const response = await fetch(`${baseEndpoint}/docs`); expect(response.status).toBe(200); const contentType = response.headers.get("content-type"); expect(contentType).toContain("text/html"); From d8de7e2393c1c9e3951cb92e2d78f14b96df15b2 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sat, 12 Jul 2025 23:06:28 -0400 Subject: [PATCH 06/10] fix script --- src/api/createSwagger.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/createSwagger.ts b/src/api/createSwagger.ts index a69952c9..909bb401 100644 --- a/src/api/createSwagger.ts +++ b/src/api/createSwagger.ts @@ -166,6 +166,6 @@ async function createSwaggerFiles() { process.exit(1); } } -if (import.meta.url === `file://createSwagger.ts`) { +if (import.meta.url.includes("createSwagger.ts")) { createSwaggerFiles(); } From d584f5639ababed77b462e9f8cc7dd6f053afa06 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sat, 12 Jul 2025 23:13:32 -0400 Subject: [PATCH 07/10] fix live tests --- tests/live/ical.test.ts | 52 +++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/tests/live/ical.test.ts b/tests/live/ical.test.ts index 192d3bc0..2b72d9c7 100644 --- a/tests/live/ical.test.ts +++ b/tests/live/ical.test.ts @@ -47,29 +47,31 @@ describe( }, ); -test( - "Check that the ical base works and uses a default host of ACM", - { timeout: 45000 }, - async () => { - const response = await fetchWithRateLimit( - `${baseEndpoint.replace("core", "ical")}/ACM`, - ); - const responseBase = await fetchWithRateLimit( - `${baseEndpoint.replace("core", "ical")}`, - ); - expect(response.status).toBe(200); - expect(responseBase.status).toBe(200); - expect(response.headers.get("Content-Disposition")).toEqual( - 'attachment; filename="calendar.ics"', - ); - expect(responseBase.headers.get("Content-Disposition")).toEqual( - 'attachment; filename="calendar.ics"', - ); - const text1 = await response.text(); - const text2 = await responseBase.text(); - expect(text1).toStrictEqual(text2); +test("Check that the ACM host works", { timeout: 45000 }, async () => { + const response = await fetchWithRateLimit( + `${baseEndpoint.replace("core", "ical")}/ACM`, + ); - const calendar = ical.sync.parseICS(text1); - expect(calendar["vcalendar"]["type"]).toEqual("VCALENDAR"); - }, -); + expect(response.status).toBe(200); + expect(response.headers.get("Content-Disposition")).toEqual( + 'attachment; filename="calendar.ics"', + ); + const text1 = await response.text(); + const calendar = ical.sync.parseICS(text1); + expect(calendar["vcalendar"]["type"]).toEqual("VCALENDAR"); +}); + +test("Check that the base route works", { timeout: 45000 }, async () => { + const response = await fetchWithRateLimit( + `${baseEndpoint.replace("core", "ical")}`, + ); + + expect(response.status).toBe(200); + expect(response.headers.get("Content-Disposition")).toEqual( + 'attachment; filename="calendar.ics"', + ); + const text1 = await response.text(); + const calendar = ical.sync.parseICS(text1); + expect(calendar["vcalendar"]["type"]).toEqual("VCALENDAR"); +}); +s; From 279b96a6c906f7b0e765a50c138d8b723d656ccd Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sun, 13 Jul 2025 00:00:09 -0400 Subject: [PATCH 08/10] fixes --- tests/e2e/auditLogs.spec.ts | 30 ++++++++++++ tests/e2e/docs.spec.ts | 43 +++++++++++++++++ tests/e2e/events.spec.ts | 2 +- tests/e2e/linkry.spec.ts | 96 +++++++++++++++++++++++++++++++++++++ tests/e2e/login.spec.ts | 2 +- 5 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 tests/e2e/auditLogs.spec.ts create mode 100644 tests/e2e/docs.spec.ts create mode 100644 tests/e2e/linkry.spec.ts diff --git a/tests/e2e/auditLogs.spec.ts b/tests/e2e/auditLogs.spec.ts new file mode 100644 index 00000000..fecee243 --- /dev/null +++ b/tests/e2e/auditLogs.spec.ts @@ -0,0 +1,30 @@ +import { expect } from "@playwright/test"; +import { test } from "./base.js"; +import { describe } from "node:test"; + +describe("Audit Log tests", () => { + test("A user can view audit logs with filters", async ({ + page, + becomeUser, + }) => { + await becomeUser(page); + await page.locator("a").filter({ hasText: "Audit Logs" }).click(); + await page.getByRole("textbox", { name: "Module" }).click(); + await page.getByRole("option", { name: "Audit Log" }).click(); + await page.getByRole("button", { name: "Fetch Logs" }).click(); + await expect(page.getByRole("cell", { name: "Timestamp" })).toBeVisible(); + await page.getByRole("cell", { name: "Actor" }).click(); + await expect(page.getByRole("cell", { name: "Actor" })).toBeVisible(); + await expect(page.getByRole("cell", { name: "Action" })).toBeVisible(); + await expect(page.getByRole("cell", { name: "Target" })).toBeVisible(); + await expect(page.getByRole("cell", { name: "Request ID" })).toBeVisible(); + await page.getByRole("button", { name: "Fetch Logs" }).click(); + await page.getByRole("button", { name: "Fetch Logs" }).click(); + await page.getByRole("button", { name: "Fetch Logs" }).click(); + await expect(page.locator("tbody")).toContainText( + "core-e2e-testing@illinois.edu", + ); + await expect(page.locator("tbody")).toContainText("Audit Log"); + await expect(page.locator("tbody")).toContainText("Viewed audit log from"); + }); +}); diff --git a/tests/e2e/docs.spec.ts b/tests/e2e/docs.spec.ts new file mode 100644 index 00000000..8071ca5b --- /dev/null +++ b/tests/e2e/docs.spec.ts @@ -0,0 +1,43 @@ +import { expect } from "@playwright/test"; +import { capitalizeFirstLetter, getUpcomingEvents, test } from "./base.js"; +import { describe } from "node:test"; + +describe("Docs tests", () => { + test("A user can view the API documentation", async ({ page }) => { + await page.goto( + process.env.E2E_TEST_HOST || "https://core.aws.qa.acmuiuc.org/docs", + ); + expect( + page.getByRole("heading", { name: "ACM @ UIUC Core API" }), + ).toBeDefined(); + expect(page.getByRole("heading", { name: "Usage" })).toBeDefined(); + expect(page.getByRole("heading", { name: "Contact" })).toBeDefined(); + expect( + page.getByRole("heading", { + name: "Retrieve calendar events with applied filters.", + }), + ).toBeDefined(); + }); + test("A user can make API requests using the API documentation site", async ({ + page, + }) => { + await page.goto( + process.env.E2E_TEST_HOST || "https://core.aws.qa.acmuiuc.org/docs", + ); + expect( + page.getByRole("heading", { name: "ACM @ UIUC Core API" }), + ).toBeDefined(); + await page + .getByRole("link", { name: "Retrieve calendar events with" }) + .click(); + await page + .getByRole("button", { name: "Test Request (get /api/v1/events)" }) + .click(); + await page + .getByRole("button", { name: "Send get request to https://" }) + .click(); + await expect(page.getByLabel("Response", { exact: true })).toContainText( + "200 OK", + ); + }); +}); diff --git a/tests/e2e/events.spec.ts b/tests/e2e/events.spec.ts index cd9d3661..27863648 100644 --- a/tests/e2e/events.spec.ts +++ b/tests/e2e/events.spec.ts @@ -1,5 +1,5 @@ import { expect } from "@playwright/test"; -import { capitalizeFirstLetter, getUpcomingEvents, test } from "./base"; +import { capitalizeFirstLetter, getUpcomingEvents, test } from "./base.js"; import { describe } from "node:test"; describe("Events tests", () => { diff --git a/tests/e2e/linkry.spec.ts b/tests/e2e/linkry.spec.ts new file mode 100644 index 00000000..09a89f06 --- /dev/null +++ b/tests/e2e/linkry.spec.ts @@ -0,0 +1,96 @@ +import { expect } from "@playwright/test"; +import { test } from "./base.js"; +import { describe } from "node:test"; +import { randomUUID } from "crypto"; + +describe("Link Shortener tests", () => { + test("A user can create shortened links, fetch them, and then delete them", async ({ + page, + becomeUser, + request, // Inject the request fixture + }) => { + test.slow(); + const uuid = `e2e-${randomUUID()}`; + await becomeUser(page); + await page.locator("a").filter({ hasText: "Link Shortener" }).click(); + await page.getByRole("button", { name: "Add New Link" }).click(); + await page.getByRole("button", { name: "Random" }).click(); + await page.getByRole("textbox", { name: "Short URL" }).fill(uuid); + await page.getByRole("textbox", { name: "URL to shorten" }).click(); + await page + .getByRole("textbox", { name: "URL to shorten" }) + .fill("https://google.com"); + await page + .locator("div") + .filter({ + hasText: + /^Access DelegationSelect groups which are permitted to manage this link\.$/, + }) + .locator("div") + .nth(1) + .click(); + await page.getByRole("option", { name: "ACM Infra Chairs" }).click(); + await page + .locator("div") + .filter({ hasText: /^ACM Infra Chairs$/ }) + .nth(1) + .click(); + await page.getByRole("button", { name: "Save" }).click(); + await page.waitForURL("https://core.aws.qa.acmuiuc.org/linkry"); + + const shortLinkBaseUrl = "go.aws.qa.acmuiuc.org/"; // Base URL for your shortened links + const fullShortLink = `${shortLinkBaseUrl}${uuid}`; + + let responseStatus: number | undefined; + let finalRedirectUrl: string | undefined; + const maxRetries = 20; + + for (let i = 0; i < maxRetries; i++) { + try { + console.log(`Attempt ${i + 1} to fetch ${fullShortLink}`); + const response = await request.get(`https://${fullShortLink}`, { + failOnStatusCode: false, // Don't fail the test immediately on non-2xx/3xx status codes + maxRedirects: 5, // Allow redirects + }); + responseStatus = response.status(); + finalRedirectUrl = response.url(); + console.log(`Response status for ${fullShortLink}: ${responseStatus}`); + console.log(`Final URL after redirect: ${finalRedirectUrl}`); + + if (responseStatus >= 200 && responseStatus < 400) { + expect(finalRedirectUrl).toBe("https://www.google.com/"); + break; + } + } catch (error) { + if (error instanceof Error) { + console.warn(`Fetch failed on attempt ${i + 1}: ${error.message}`); + } + } + + if (i < maxRetries - 1) { + const delay = Math.min(10000 * Math.pow(1.5, i), 30000); + console.log(`Retrying in ${delay / 1000} seconds...`); + await page.waitForTimeout(delay); + } + } + + expect(responseStatus).toBeGreaterThanOrEqual(200); + expect(responseStatus).toBeLessThan(400); + expect(finalRedirectUrl).toBe("https://www.google.com/"); + + // Continue with the UI assertion and deletion + await expect(page.getByLabel("My Links").locator("tbody")).toContainText( + fullShortLink, + ); + await page + .getByRole("row", { name: fullShortLink }) + .getByRole("button") + .click(); + await expect( + page.getByLabel("Confirm Deletion").getByRole("paragraph"), + ).toContainText( + `Are you sure you want to delete the redirect from ${uuid} to https://google.com?`, + ); + await page.getByRole("button", { name: "Delete" }).click(); + }); +}); diff --git a/tests/e2e/login.spec.ts b/tests/e2e/login.spec.ts index 6476d8fb..b1160b71 100644 --- a/tests/e2e/login.spec.ts +++ b/tests/e2e/login.spec.ts @@ -1,5 +1,5 @@ import { expect } from "@playwright/test"; -import { test } from "./base"; +import { test } from "./base.js"; import { describe } from "node:test"; describe("Login tests", () => { From 37ef056558aa9ade7daaf9e989f44dee8132be63 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sun, 13 Jul 2025 00:06:26 -0400 Subject: [PATCH 09/10] fix tests --- src/api/createSwagger.ts | 145 +-------------------------------------- src/api/docs.ts | 138 +++++++++++++++++++++++++++++++++++++ src/api/index.ts | 2 +- 3 files changed, 142 insertions(+), 143 deletions(-) create mode 100644 src/api/docs.ts diff --git a/src/api/createSwagger.ts b/src/api/createSwagger.ts index 909bb401..785d3092 100644 --- a/src/api/createSwagger.ts +++ b/src/api/createSwagger.ts @@ -2,145 +2,7 @@ import { fileURLToPath } from "url"; import path from "node:path"; import { writeFile, mkdir } from "fs/promises"; import init from "./index.js"; // Assuming this is your Fastify app initializer - -export const docsHtml = ` - - - - Core API Documentation | ACM @ UIUC - - - - - - - - - - - - -
- Loading API Documentation... -
- - - - - - - - - -`; +import { docsHtml } from "./docs.js"; /** * Generates and saves Swagger/OpenAPI specification files. */ @@ -166,6 +28,5 @@ async function createSwaggerFiles() { process.exit(1); } } -if (import.meta.url.includes("createSwagger.ts")) { - createSwaggerFiles(); -} + +createSwaggerFiles(); diff --git a/src/api/docs.ts b/src/api/docs.ts new file mode 100644 index 00000000..872e2823 --- /dev/null +++ b/src/api/docs.ts @@ -0,0 +1,138 @@ +export const docsHtml = ` + + + + Core API Documentation | ACM @ UIUC + + + + + + + + + + + + +
+ Loading API Documentation... +
+ + + + + + + + + +`; diff --git a/src/api/index.ts b/src/api/index.ts index 5299c992..dc4c1ced 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -60,7 +60,7 @@ import protectedRoute from "./routes/protected.js"; import eventsPlugin from "./routes/events.js"; import mobileWalletV2Route from "./routes/v2/mobileWallet.js"; import membershipV2Plugin from "./routes/v2/membership.js"; -import { docsHtml } from "./createSwagger.js"; +import { docsHtml } from "./docs.js"; /** END ROUTES */ export const instanceId = randomUUID(); From 61f1873b847b09928349233ebb020ef03ec4e6fe Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sun, 13 Jul 2025 00:15:49 -0400 Subject: [PATCH 10/10] fix live tests --- tests/live/ical.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/live/ical.test.ts b/tests/live/ical.test.ts index 2b72d9c7..890d1dca 100644 --- a/tests/live/ical.test.ts +++ b/tests/live/ical.test.ts @@ -74,4 +74,3 @@ test("Check that the base route works", { timeout: 45000 }, async () => { const calendar = ical.sync.parseICS(text1); expect(calendar["vcalendar"]["type"]).toEqual("VCALENDAR"); }); -s;