nxtgauge-frontend-solid/tests/e2e/api.spec.ts

262 lines
7.2 KiB
TypeScript
Raw Normal View History

import { test, expect, request } from "@playwright/test";
const API_BASE = "http://localhost:3000/api";
async function getAuthToken(): Promise<string | null> {
const ctx = await request.newContext();
const res = await ctx.post(`${API_BASE}/auth/login`, {
data: {
email: "testtutora2026@example.com",
password: "Test1234!",
},
});
if (!res.ok()) return null;
const data = await res.json();
return data.access_token || null;
}
async function getCompanyAuthToken(): Promise<string | null> {
const ctx = await request.newContext();
const res = await ctx.post(`${API_BASE}/auth/login`, {
data: {
email: "testcompany@example.com",
password: "TestPassword123!",
},
});
if (!res.ok()) return null;
const data = await res.json();
return data.access_token || null;
}
test.describe("AI API Endpoints", () => {
let companyToken: string | null;
let jobSeekerToken: string | null;
test.beforeAll(async () => {
companyToken = await getCompanyAuthToken();
jobSeekerToken = await getAuthToken();
});
test.describe("Company AI - Generate Job Field", () => {
test("POST /ai/generate-job-field returns generated content", async ({ page }) => {
if (!companyToken) test.skip();
await page.goto(`${API_BASE}/`);
await page.evaluate((t: string) => {
window.sessionStorage.setItem("nxtgauge_access_token", t);
}, companyToken);
const ctx = await request.newContext();
const res = await ctx.post(`${API_BASE}/ai/generate-job-field`, {
headers: {
Authorization: `Bearer ${companyToken}`,
},
data: {
field: "title",
prompt: "Generate a job title for a senior frontend developer position",
},
});
if (res.status() === 404) {
test.skip();
return;
}
expect(res.status()).toBe(200);
const body = await res.json();
expect(body).toHaveProperty("field", "title");
expect(body).toHaveProperty("content");
expect(typeof body.content).toBe("string");
expect(body.content.length).toBeGreaterThan(0);
});
test("POST /ai/generate-job-field rejects invalid field", async () => {
if (!companyToken) test.skip();
const ctx = await request.newContext();
const res = await ctx.post(`${API_BASE}/ai/generate-job-field`, {
headers: {
Authorization: `Bearer ${companyToken}`,
},
data: {
field: "invalid_field",
prompt: "test",
},
});
if (res.status() === 404) test.skip();
else expect(res.status()).toBe(400);
});
test("POST /ai/generate-job-field rate limits after daily quota", async () => {
if (!companyToken) test.skip();
const ctx = await request.newContext();
let got429 = false;
for (let i = 0; i < 6; i++) {
const res = await ctx.post(`${API_BASE}/ai/generate-job-field`, {
headers: {
Authorization: `Bearer ${companyToken}`,
},
data: {
field: "title",
prompt: `Test prompt ${i}`,
},
});
if (res.status() === 404) {
test.skip();
return;
}
if (res.status() === 429) {
got429 = true;
const body = await res.json();
expect(body.code).toBe("AI_LIMIT_EXCEEDED");
break;
}
}
if (!got429) {
console.warn("Did not hit rate limit within 6 requests - this may indicate the feature is not working or limit is higher than expected");
}
});
test("POST /ai/generate-job-field returns 401 without auth when route exists", async () => {
const ctx = await request.newContext();
const res = await ctx.post(`${API_BASE}/ai/generate-job-field`, {
data: {
field: "title",
prompt: "test",
},
});
if (res.status() === 404) {
test.skip();
return;
}
expect(res.status()).toBe(401);
});
});
test.describe("AI Usage Endpoint", () => {
test("GET /ai/usage returns usage stats for company", async () => {
if (!companyToken) test.skip();
const ctx = await request.newContext();
const res = await ctx.get(`${API_BASE}/ai/usage`, {
headers: {
Authorization: `Bearer ${companyToken}`,
},
});
if (res.status() === 404) {
test.skip();
return;
}
expect(res.status()).toBe(200);
const body = await res.json();
expect(body).toHaveProperty("used_today");
expect(body).toHaveProperty("limit");
expect(body).toHaveProperty("has_ai_pack");
expect(typeof body.used_today).toBe("number");
expect(typeof body.limit).toBe("number");
});
test("GET /ai/usage returns 401 without auth when route exists", async () => {
const ctx = await request.newContext();
const res = await ctx.get(`${API_BASE}/ai/usage`);
if (res.status() === 404) {
test.skip();
return;
}
expect(res.status()).toBe(401);
});
});
});
test.describe("Auth API Endpoints", () => {
test("POST /auth/login returns token for valid credentials", async () => {
const ctx = await request.newContext();
const res = await ctx.post(`${API_BASE}/auth/login`, {
data: {
email: "testtutora2026@example.com",
password: "Test1234!",
},
});
if (res.status() === 429) {
test.skip();
return;
}
expect(res.status()).toBe(200);
const body = await res.json();
expect(body).toHaveProperty("access_token");
expect(typeof body.access_token).toBe("string");
});
test("POST /auth/login returns 401 for invalid credentials", async () => {
const ctx = await request.newContext();
const res = await ctx.post(`${API_BASE}/auth/login`, {
data: {
email: "invalid@example.com",
password: "wrongpassword",
},
});
if (res.status() === 429) test.skip();
else expect(res.status()).toBe(401);
});
test("POST /auth/login rate limits after too many attempts", async () => {
const ctx = await request.newContext();
for (let i = 0; i < 6; i++) {
await ctx.post(`${API_BASE}/auth/login`, {
data: {
email: "testtutora2026@example.com",
password: "wrongpassword",
},
});
}
const res = await ctx.post(`${API_BASE}/auth/login`, {
data: {
email: "testtutora2026@example.com",
password: "Test1234!",
},
});
expect(res.status()).toBe(429);
const body = await res.json();
expect(body.code).toBe("RATE_LIMITED");
});
});
test.describe("Gateway API", () => {
test("Gateway routes /api/ai/* to users service", async () => {
const ctx = await request.newContext();
const res = await ctx.get(`${API_BASE}/ai/usage`, {
headers: {
Authorization: `Bearer dummy`,
},
});
if (res.status() === 404) {
test.skip();
return;
}
expect(res.status()).not.toBe(404);
});
test("Gateway returns 404 for unknown routes", async () => {
const ctx = await request.newContext();
const res = await ctx.get(`${API_BASE}/nonexistent-route`);
expect(res.status()).toBe(404);
});
});