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
96 changes: 96 additions & 0 deletions tests/e2e/course-report.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 course report endpoint (#632).
*
* Covers:
* - POST /api/v1/courses/:id/report (report creation, duplicate handling, input validation)
*/
describe("Course Report API (#632)", () => {
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 report submissions", async () => {
const response = await app.inject({
method: "POST",
url: "/api/v1/courses/c1111111-1111-4111-8111-111111111111/report",
payload: {
reason: "inappropriate",
description: "Inappropriate language in module 2",
},
});

expect(response.statusCode).toBe(401);
const body = JSON.parse(response.payload);
expect(body.error).toBe("UNAUTHORIZED");
});

it("rejects requests with invalid course UUID parameter", async () => {
const token = tokenFor("a1111111-1111-4111-8111-111111111111");
const response = await app.inject({
method: "POST",
url: "/api/v1/courses/invalid-uuid/report",
headers: {
authorization: `Bearer ${token}`,
},
payload: {
reason: "inappropriate",
description: "Report description",
},
});

expect(response.statusCode).toBe(400);
const body = JSON.parse(response.payload);
expect(body.error).toBe("VALIDATION_ERROR");
});

it("rejects invalid report payload", async () => {
const token = tokenFor("a1111111-1111-4111-8111-111111111111");
const response = await app.inject({
method: "POST",
url: "/api/v1/courses/c1111111-1111-4111-8111-111111111111/report",
headers: {
authorization: `Bearer ${token}`,
},
payload: {},
});

expect(response.statusCode).toBe(400);
const body = JSON.parse(response.payload);
expect(body.error).toBe("VALIDATION_ERROR");
});

it("handles valid report submission structure", async () => {
const token = tokenFor("a1111111-1111-4111-8111-111111111111");
const response = await app.inject({
method: "POST",
url: "/api/v1/courses/c1111111-1111-4111-8111-111111111111/report",
headers: {
authorization: `Bearer ${token}`,
},
payload: {
reason: "inappropriate",
description: "Detailed description of the issue in course materials",
},
});

expect([200, 201, 404, 409, 500]).toContain(response.statusCode);
});
});
116 changes: 116 additions & 0 deletions tests/e2e/course-reviews.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import type { FastifyInstance } from "fastify";
import { buildApp } from "../../src/server.js";

/**
* E2E tests for course reviews endpoints (#630).
*
* Covers:
* - GET /api/v1/courses/:id/reviews (listing course reviews)
* - POST /api/v1/courses/:id/reviews (creating/updating a course review)
*/
describe("Course Reviews API (#630)", () => {
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/courses/:id/reviews", () => {
it("rejects invalid course UUID parameter", async () => {
const response = await app.inject({
method: "GET",
url: "/api/v1/courses/not-a-valid-uuid/reviews",
});

expect(response.statusCode).toBe(400);
const body = JSON.parse(response.payload);
expect(body.error).toBe("VALIDATION_ERROR");
});

it("handles review listing with valid course ID", async () => {
const response = await app.inject({
method: "GET",
url: "/api/v1/courses/c1111111-1111-4111-8111-111111111111/reviews",
});

expect([200, 404, 500]).toContain(response.statusCode);
if (response.statusCode === 200) {
const body = JSON.parse(response.payload);
expect(body).toHaveProperty("success");
}
});

it("rejects invalid query parameters (e.g. invalid page)", async () => {
const response = await app.inject({
method: "GET",
url: "/api/v1/courses/c1111111-1111-4111-8111-111111111111/reviews?page=0",
});

expect(response.statusCode).toBe(400);
});
});

describe("POST /api/v1/courses/:id/reviews", () => {
it("rejects unauthenticated review submission", async () => {
const response = await app.inject({
method: "POST",
url: "/api/v1/courses/c1111111-1111-4111-8111-111111111111/reviews",
payload: {
rating: 5,
comment: "Great introductory course!",
},
});

expect(response.statusCode).toBe(401);
});

it("validates rating range and required fields", async () => {
const token = tokenFor("a1111111-1111-4111-8111-111111111111");
const response = await app.inject({
method: "POST",
url: "/api/v1/courses/c1111111-1111-4111-8111-111111111111/reviews",
headers: {
authorization: `Bearer ${token}`,
},
payload: {
rating: 10, // Invalid: exceeds maximum rating
comment: "Invalid rating value",
},
});

expect(response.statusCode).toBe(400);
const body = JSON.parse(response.payload);
expect(body.error).toBe("VALIDATION_ERROR");
});

it("handles valid review submission attempt", async () => {
const token = tokenFor("a1111111-1111-4111-8111-111111111111");
const response = await app.inject({
method: "POST",
url: "/api/v1/courses/c1111111-1111-4111-8111-111111111111/reviews",
headers: {
authorization: `Bearer ${token}`,
},
payload: {
rating: 5,
comment: "Great introductory course on blockchain!",
},
});

expect([200, 201, 403, 404, 500]).toContain(response.statusCode);
});
});
});
84 changes: 84 additions & 0 deletions tests/e2e/course-share.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import type { FastifyInstance } from "fastify";
import { buildApp } from "../../src/server.js";

/**
* E2E tests for course share endpoints (#631).
*
* Covers:
* - POST /api/v1/courses/:id/share (share link creation, referral code, QR code generation)
* - GET /api/v1/courses/shared/:code (share link resolution)
*/
describe("Course Share API (#631)", () => {
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("POST /api/v1/courses/:id/share", () => {
it("rejects unauthenticated share link creation", async () => {
const response = await app.inject({
method: "POST",
url: "/api/v1/courses/c1111111-1111-4111-8111-111111111111/share",
});

expect(response.statusCode).toBe(401);
});

it("rejects invalid course UUID parameter", async () => {
const token = tokenFor("a1111111-1111-4111-8111-111111111111");
const response = await app.inject({
method: "POST",
url: "/api/v1/courses/invalid-id/share",
headers: {
authorization: `Bearer ${token}`,
},
});

expect(response.statusCode).toBe(400);
const body = JSON.parse(response.payload);
expect(body.error).toBe("VALIDATION_ERROR");
});

it("handles share generation attempt with valid UUID", async () => {
const token = tokenFor("a1111111-1111-4111-8111-111111111111");
const response = await app.inject({
method: "POST",
url: "/api/v1/courses/c1111111-1111-4111-8111-111111111111/share",
headers: {
authorization: `Bearer ${token}`,
},
});

expect([200, 201, 404, 500]).toContain(response.statusCode);
if (response.statusCode === 200 || response.statusCode === 201) {
const body = JSON.parse(response.payload);
expect(body).toHaveProperty("success");
}
});
});

describe("GET /api/v1/courses/shared/:code", () => {
it("handles share code resolution attempt", async () => {
const response = await app.inject({
method: "GET",
url: "/api/v1/courses/shared/SAMPLECODE123",
});

expect([200, 404, 500]).toContain(response.statusCode);
});
});
});
Loading