diff --git a/tests/e2e/avatar-upload.test.ts b/tests/e2e/avatar-upload.test.ts new file mode 100644 index 0000000..33f4a20 --- /dev/null +++ b/tests/e2e/avatar-upload.test.ts @@ -0,0 +1,71 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import type { FastifyInstance } from "fastify"; +import { buildApp } from "../../src/server.js"; + +/** + * E2E tests for avatar upload endpoint (#639). + * + * Covers: + * - PUT /api/v1/users/me/avatar (uploading user avatar, file validation, auth checks) + */ +describe("Avatar Upload API (#639)", () => { + let app: FastifyInstance; + + beforeAll(async () => { + app = await buildApp(); + await app.ready(); + }); + + afterAll(async () => { + await app.close(); + }); + + const tokenFor = (sub: string) => + app.jwt.sign({ + sub, + stellarAddress: + "GALICE0000000000000000000000000000000000000000000000000000000", + }); + + it("rejects unauthenticated avatar uploads", async () => { + const response = await app.inject({ + method: "PUT", + url: "/api/v1/users/me/avatar", + }); + + expect(response.statusCode).toBe(401); + }); + + it("rejects non-multipart requests when authenticated", async () => { + const token = tokenFor("a1111111-1111-4111-8111-111111111111"); + const response = await app.inject({ + method: "PUT", + url: "/api/v1/users/me/avatar", + headers: { + authorization: `Bearer ${token}`, + "content-type": "application/json", + }, + payload: { + avatar: "invalid-payload", + }, + }); + + expect([400, 415, 500]).toContain(response.statusCode); + }); + + it("rejects empty multipart payload", async () => { + const token = tokenFor("a1111111-1111-4111-8111-111111111111"); + const boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW"; + const response = await app.inject({ + method: "PUT", + url: "/api/v1/users/me/avatar", + headers: { + authorization: `Bearer ${token}`, + "content-type": `multipart/form-data; boundary=${boundary}`, + }, + payload: `--${boundary}--\r\n`, + }); + + expect([400, 422, 500]).toContain(response.statusCode); + }); +}); diff --git a/tests/e2e/batch-enrollment.test.ts b/tests/e2e/batch-enrollment.test.ts new file mode 100644 index 0000000..c585f6d --- /dev/null +++ b/tests/e2e/batch-enrollment.test.ts @@ -0,0 +1,96 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import type { FastifyInstance } from "fastify"; +import { buildApp } from "../../src/server.js"; + +/** + * E2E tests for batch enrollment endpoint (#640). + * + * Covers: + * - POST /api/v1/courses/enroll/batch (enrolling in multiple courses in one call) + */ +describe("Batch Enrollment API (#640)", () => { + let app: FastifyInstance; + + beforeAll(async () => { + app = await buildApp(); + await app.ready(); + }); + + afterAll(async () => { + await app.close(); + }); + + const tokenFor = (sub: string) => + app.jwt.sign({ + sub, + stellarAddress: + "GALICE0000000000000000000000000000000000000000000000000000000", + }); + + it("rejects unauthenticated batch enrollment requests", async () => { + const response = await app.inject({ + method: "POST", + url: "/api/v1/courses/enroll/batch", + payload: { + courseIds: ["c1111111-1111-4111-8111-111111111111"], + }, + }); + + expect(response.statusCode).toBe(401); + }); + + it("rejects empty courseIds list in payload", async () => { + const token = tokenFor("a1111111-1111-4111-8111-111111111111"); + const response = await app.inject({ + method: "POST", + url: "/api/v1/courses/enroll/batch", + headers: { + authorization: `Bearer ${token}`, + }, + payload: { + courseIds: [], + }, + }); + + expect(response.statusCode).toBe(400); + const body = JSON.parse(response.payload); + expect(body.error).toBe("VALIDATION_ERROR"); + }); + + it("rejects invalid UUIDs inside courseIds list", async () => { + const token = tokenFor("a1111111-1111-4111-8111-111111111111"); + const response = await app.inject({ + method: "POST", + url: "/api/v1/courses/enroll/batch", + headers: { + authorization: `Bearer ${token}`, + }, + payload: { + courseIds: ["invalid-uuid"], + }, + }); + + expect(response.statusCode).toBe(400); + const body = JSON.parse(response.payload); + expect(body.error).toBe("VALIDATION_ERROR"); + }); + + it("handles valid batch enrollment payload structure", async () => { + const token = tokenFor("a1111111-1111-4111-8111-111111111111"); + const response = await app.inject({ + method: "POST", + url: "/api/v1/courses/enroll/batch", + headers: { + authorization: `Bearer ${token}`, + }, + payload: { + courseIds: [ + "c1111111-1111-4111-8111-111111111111", + "c2222222-2222-4222-8222-222222222222", + ], + }, + }); + + expect([200, 207, 400, 404, 500]).toContain(response.statusCode); + }); +}); diff --git a/tests/e2e/certificate-download.test.ts b/tests/e2e/certificate-download.test.ts new file mode 100644 index 0000000..c99d026 --- /dev/null +++ b/tests/e2e/certificate-download.test.ts @@ -0,0 +1,95 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import type { FastifyInstance } from "fastify"; +import { buildApp } from "../../src/server.js"; + +/** + * E2E tests for certificate download endpoints (#638). + * + * Covers: + * - GET /api/v1/credentials/:id/certificate (download certificate payload/format) + * - GET /api/v1/courses/:id/completion-certificate (download course completion PDF) + */ +describe("Certificate Download API (#638)", () => { + let app: FastifyInstance; + + beforeAll(async () => { + app = await buildApp(); + await app.ready(); + }); + + afterAll(async () => { + await app.close(); + }); + + const tokenFor = (sub: string) => + app.jwt.sign({ + sub, + stellarAddress: + "GALICE0000000000000000000000000000000000000000000000000000000", + }); + + describe("GET /api/v1/credentials/:id/certificate", () => { + it("rejects unauthenticated requests", async () => { + const response = await app.inject({ + method: "GET", + url: "/api/v1/credentials/c1111111-1111-4111-8111-111111111111/certificate", + }); + + expect(response.statusCode).toBe(401); + }); + + it("rejects invalid certificate UUID param", async () => { + const token = tokenFor("a1111111-1111-4111-8111-111111111111"); + const response = await app.inject({ + method: "GET", + url: "/api/v1/credentials/invalid-uuid/certificate", + headers: { + authorization: `Bearer ${token}`, + }, + }); + + expect(response.statusCode).toBe(400); + const body = JSON.parse(response.payload); + expect(body.error).toBe("VALIDATION_ERROR"); + }); + + it("handles download attempt for valid UUID", async () => { + const token = tokenFor("a1111111-1111-4111-8111-111111111111"); + const response = await app.inject({ + method: "GET", + url: "/api/v1/credentials/c1111111-1111-4111-8111-111111111111/certificate", + headers: { + authorization: `Bearer ${token}`, + }, + }); + + expect([200, 404, 500]).toContain(response.statusCode); + }); + }); + + describe("GET /api/v1/courses/:id/completion-certificate", () => { + it("rejects unauthenticated requests", async () => { + const response = await app.inject({ + method: "GET", + url: "/api/v1/courses/c1111111-1111-4111-8111-111111111111/completion-certificate", + }); + + expect(response.statusCode).toBe(401); + }); + + it("rejects invalid course UUID param", async () => { + const token = tokenFor("a1111111-1111-4111-8111-111111111111"); + const response = await app.inject({ + method: "GET", + url: "/api/v1/courses/not-uuid/completion-certificate", + headers: { + authorization: `Bearer ${token}`, + }, + }); + + expect(response.statusCode).toBe(400); + const body = JSON.parse(response.payload); + expect(body.error).toBe("VALIDATION_ERROR"); + }); + }); +}); diff --git a/tests/e2e/course-popular.test.ts b/tests/e2e/course-popular.test.ts new file mode 100644 index 0000000..0462b90 --- /dev/null +++ b/tests/e2e/course-popular.test.ts @@ -0,0 +1,67 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import type { FastifyInstance } from "fastify"; +import { buildApp } from "../../src/server.js"; + +/** + * E2E tests for popular courses endpoint (#641). + * + * Covers: + * - GET /api/v1/courses/popular (listing popular courses by enrollment count, pagination limits) + */ +describe("Popular Courses API (#641)", () => { + let app: FastifyInstance; + + beforeAll(async () => { + app = await buildApp(); + await app.ready(); + }); + + afterAll(async () => { + await app.close(); + }); + + it("returns popular courses list without requiring auth", async () => { + const response = await app.inject({ + method: "GET", + url: "/api/v1/courses/popular", + }); + + expect([200, 500]).toContain(response.statusCode); + if (response.statusCode === 200) { + const body = JSON.parse(response.payload); + expect(body).toHaveProperty("success"); + expect(Array.isArray(body.data)).toBe(true); + } + }); + + it("accepts valid limit query parameter", async () => { + const response = await app.inject({ + method: "GET", + url: "/api/v1/courses/popular?limit=5", + }); + + expect([200, 500]).toContain(response.statusCode); + }); + + it("rejects invalid limit query parameter", async () => { + const response = await app.inject({ + method: "GET", + url: "/api/v1/courses/popular?limit=0", + }); + + expect(response.statusCode).toBe(400); + const body = JSON.parse(response.payload); + expect(body.error).toBe("VALIDATION_ERROR"); + }); + + it("rejects limit exceeding maximum allowed", async () => { + const response = await app.inject({ + method: "GET", + url: "/api/v1/courses/popular?limit=100", + }); + + expect(response.statusCode).toBe(400); + const body = JSON.parse(response.payload); + expect(body.error).toBe("VALIDATION_ERROR"); + }); +});