Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions tests/e2e/avatar-upload.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
96 changes: 96 additions & 0 deletions tests/e2e/batch-enrollment.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
95 changes: 95 additions & 0 deletions tests/e2e/certificate-download.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
});
67 changes: 67 additions & 0 deletions tests/e2e/course-popular.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});