262 lines
7.2 KiB
TypeScript
262 lines
7.2 KiB
TypeScript
|
|
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);
|
||
|
|
});
|
||
|
|
});
|