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.
285 lines
7.7 KiB
TypeScript
285 lines
7.7 KiB
TypeScript
import { test, expect, request } from "@playwright/test";
|
|
|
|
const API_BASE = "https://test111.nxtgauge.com/api";
|
|
|
|
test.describe("Security - Authentication", () => {
|
|
test("JWT token is not returned for invalid credentials", async () => {
|
|
const ctx = await request.newContext();
|
|
const res = await ctx.post(`${API_BASE}/auth/login`, {
|
|
data: {
|
|
email: "nonexistent@example.com",
|
|
password: "wrongpassword",
|
|
},
|
|
});
|
|
|
|
expect(res.status()).toBe(401);
|
|
const body = await res.json();
|
|
expect(body).not.toHaveProperty("access_token");
|
|
});
|
|
|
|
test("Login without email returns proper error", async () => {
|
|
const ctx = await request.newContext();
|
|
const res = await ctx.post(`${API_BASE}/auth/login`, {
|
|
data: {
|
|
password: "somepassword",
|
|
},
|
|
});
|
|
|
|
expect([400, 422]).toContain(res.status());
|
|
});
|
|
|
|
test("Login without password returns proper error", async () => {
|
|
const ctx = await request.newContext();
|
|
const res = await ctx.post(`${API_BASE}/auth/login`, {
|
|
data: {
|
|
email: "test@example.com",
|
|
},
|
|
});
|
|
|
|
expect([400, 422]).toContain(res.status());
|
|
});
|
|
|
|
test("Protected endpoint rejects request without token", async () => {
|
|
const ctx = await request.newContext();
|
|
const res = await ctx.get(`${API_BASE}/ai/usage`);
|
|
|
|
expect([401, 404]).toContain(res.status());
|
|
});
|
|
|
|
test("Protected endpoint rejects request with malformed token", async () => {
|
|
const ctx = await request.newContext();
|
|
const res = await ctx.get(`${API_BASE}/ai/usage`, {
|
|
headers: {
|
|
Authorization: "Bearer invalid.malformed.token",
|
|
},
|
|
});
|
|
|
|
expect([401, 404]).toContain(res.status());
|
|
});
|
|
|
|
test("Protected endpoint rejects request with empty Bearer token", async () => {
|
|
const ctx = await request.newContext();
|
|
const res = await ctx.get(`${API_BASE}/ai/usage`, {
|
|
headers: {
|
|
Authorization: "Bearer ",
|
|
},
|
|
});
|
|
|
|
expect([401, 404]).toContain(res.status());
|
|
});
|
|
});
|
|
|
|
test.describe("Security - Rate Limiting", () => {
|
|
test("Login rate limits after multiple failed attempts", async () => {
|
|
const ctx = await request.newContext();
|
|
const uniqueEmail = `securitytest${Date.now()}@example.com`;
|
|
|
|
let rateLimited = false;
|
|
for (let i = 0; i < 5; i++) {
|
|
const res = await ctx.post(`${API_BASE}/auth/login`, {
|
|
data: {
|
|
email: uniqueEmail,
|
|
password: "wrongpassword",
|
|
},
|
|
});
|
|
if (res.status() === 429) {
|
|
rateLimited = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!rateLimited) {
|
|
const res = await ctx.post(`${API_BASE}/auth/login`, {
|
|
data: {
|
|
email: uniqueEmail,
|
|
password: "anypassword",
|
|
},
|
|
});
|
|
if (res.status() === 429) {
|
|
rateLimited = true;
|
|
}
|
|
}
|
|
|
|
if (!rateLimited) {
|
|
test.skip();
|
|
}
|
|
});
|
|
|
|
test("AI endpoints rate limit after daily quota exceeded", async () => {
|
|
const ctx = await request.newContext();
|
|
const loginRes = await ctx.post(`${API_BASE}/auth/login`, {
|
|
data: {
|
|
email: "testcompany@example.com",
|
|
password: "TestPassword123!",
|
|
},
|
|
});
|
|
|
|
if (loginRes.status() === 429) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
|
|
const data = await loginRes.json();
|
|
const token = data.access_token;
|
|
if (!token) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
|
|
let got429 = false;
|
|
for (let i = 0; i < 10; i++) {
|
|
const res = await ctx.post(`${API_BASE}/ai/generate-job-field`, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
data: { field: "title", prompt: `Test ${i}` },
|
|
});
|
|
|
|
if (res.status() === 429) {
|
|
got429 = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!got429) {
|
|
console.warn("AI rate limit not triggered within 10 requests - may need more requests or limit is higher");
|
|
}
|
|
});
|
|
});
|
|
|
|
test.describe("Security - Input Validation", () => {
|
|
test("SQL injection in login email is handled safely", async () => {
|
|
const ctx = await request.newContext();
|
|
const res = await ctx.post(`${API_BASE}/auth/login`, {
|
|
data: {
|
|
email: "' OR '1'='1",
|
|
password: "anything",
|
|
},
|
|
});
|
|
|
|
expect(res.status()).toBe(401);
|
|
const body = await res.json();
|
|
expect(body).not.toHaveProperty("access_token");
|
|
});
|
|
|
|
test("XSS attempt in login email is handled safely", async () => {
|
|
const ctx = await request.newContext();
|
|
const res = await ctx.post(`${API_BASE}/auth/login`, {
|
|
data: {
|
|
email: "<script>alert('xss')</script>@example.com",
|
|
password: "password",
|
|
},
|
|
});
|
|
|
|
expect(res.status()).toBe(401);
|
|
});
|
|
|
|
test("Very long input is handled without crash", async () => {
|
|
const ctx = await request.newContext();
|
|
const longString = "a".repeat(10000);
|
|
|
|
const res = await ctx.post(`${API_BASE}/auth/login`, {
|
|
data: {
|
|
email: `${longString}@example.com`,
|
|
password: longString,
|
|
},
|
|
});
|
|
|
|
expect([400, 401, 413]).toContain(res.status());
|
|
});
|
|
});
|
|
|
|
test.describe("Security - CORS Headers", () => {
|
|
test("CORS headers are present on API responses", async () => {
|
|
const ctx = await request.newContext();
|
|
const res = await ctx.get(`${API_BASE}/nonexistent-route`);
|
|
|
|
expect(res.status()).toBe(404);
|
|
});
|
|
});
|
|
|
|
test.describe("Security - Authorization", () => {
|
|
test("User cannot access admin endpoints with regular user token", async () => {
|
|
const ctx = await request.newContext();
|
|
const loginRes = await ctx.post(`${API_BASE}/auth/login`, {
|
|
data: {
|
|
email: "testtutora2026@example.com",
|
|
password: "Test1234!",
|
|
},
|
|
});
|
|
|
|
if (loginRes.status() === 429) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
|
|
const data = await loginRes.json();
|
|
const token = data.access_token;
|
|
if (!token) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
|
|
const res = await ctx.get(`${API_BASE}/admin/users`, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
|
|
expect([403, 404]).toContain(res.status());
|
|
});
|
|
});
|
|
|
|
test.describe("Security - Response Headers", () => {
|
|
test("API does not leak sensitive information in error responses", async () => {
|
|
const ctx = await request.newContext();
|
|
|
|
const res = await ctx.post(`${API_BASE}/auth/login`, {
|
|
data: {
|
|
email: "test@example.com",
|
|
password: "wrongpassword",
|
|
},
|
|
});
|
|
|
|
const body = await res.json();
|
|
|
|
expect(body).not.toHaveProperty("stack");
|
|
expect(body).not.toHaveProperty("debug");
|
|
expect(body).not.toHaveProperty("inner_error");
|
|
expect(body).not.toHaveProperty("innerError");
|
|
});
|
|
|
|
test("API error responses do not expose server internals", async () => {
|
|
const ctx = await request.newContext();
|
|
|
|
const res = await ctx.get(`${API_BASE}/api/nonexistent`);
|
|
|
|
const text = await res.text();
|
|
|
|
expect(text).not.toContain("at ");
|
|
expect(text).not.toContain("stack:");
|
|
expect(text).not.toContain(".rs:");
|
|
});
|
|
});
|
|
|
|
test.describe("Security - Token Handling", () => {
|
|
test("Expired token is rejected", async () => {
|
|
const ctx = await request.newContext();
|
|
|
|
const expiredToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjoxNjAwMDAwMDAwfQ.dummysignature";
|
|
|
|
const res = await ctx.get(`${API_BASE}/ai/usage`, {
|
|
headers: { Authorization: `Bearer ${expiredToken}` },
|
|
});
|
|
|
|
expect([401, 404]).toContain(res.status());
|
|
});
|
|
|
|
test("Token with invalid signature is rejected", async () => {
|
|
const ctx = await request.newContext();
|
|
|
|
const badSigToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWF0IjoxOTk5OTk5OTk5OX0.wronngsignature";
|
|
|
|
const res = await ctx.get(`${API_BASE}/ai/usage`, {
|
|
headers: { Authorization: `Bearer ${badSigToken}` },
|
|
});
|
|
|
|
expect([401, 404]).toContain(res.status());
|
|
});
|
|
});
|