Some checks failed
build-and-release / build (push) Failing after 1m19s
- Changed all buttons with orange background (#fd6116) and white text to navy blue background (#050026) with white text - Maintains 12.5:1 contrast ratio (exceeds WCAG AA 4.5:1 requirement) - Keeps brand orange (#fd6116) for decorative elements and accents - Preserves all hover states and transitions Fixes accessibility violations for chip buttons, nav buttons, auth buttons, submit buttons, and primary action buttons.
263 lines
7.2 KiB
TypeScript
263 lines
7.2 KiB
TypeScript
import { test, expect, request } from "@playwright/test";
|
|
|
|
const API_BASE = process.env.TEST_ENV === 'production'
|
|
? "https://test111.nxtgauge.com/api"
|
|
: "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);
|
|
});
|
|
});
|