250 lines
7.8 KiB
TypeScript
250 lines
7.8 KiB
TypeScript
/**
|
|
* External API (/api/v1) E2E Tests
|
|
*
|
|
* Tests the public REST API endpoints for prompt management.
|
|
* These endpoints require API key authentication via Bearer token.
|
|
*/
|
|
import { test, expect } from "@playwright/test";
|
|
|
|
const API_KEY = "test-api-key-e2e";
|
|
const BRAND_ID = "default";
|
|
|
|
// Helper to make authenticated API requests
|
|
function authHeaders() {
|
|
return {
|
|
Authorization: `Bearer ${API_KEY}`,
|
|
"Content-Type": "application/json",
|
|
};
|
|
}
|
|
|
|
test.describe("External API - Authentication", () => {
|
|
test("returns 401 without API key", async ({ request }) => {
|
|
const response = await request.get(`/api/v1/prompts`);
|
|
expect(response.status()).toBe(401);
|
|
|
|
const body = await response.json();
|
|
expect(body.error).toBe("Unauthorized");
|
|
});
|
|
|
|
test("returns 401 with invalid API key", async ({ request }) => {
|
|
const response = await request.get(`/api/v1/prompts`, {
|
|
headers: { Authorization: "Bearer invalid-key" },
|
|
});
|
|
expect(response.status()).toBe(401);
|
|
});
|
|
});
|
|
|
|
test.describe("External API - GET /api/v1/prompts", () => {
|
|
test("lists all prompts with valid API key", async ({ request }) => {
|
|
const response = await request.get(`/api/v1/prompts`, {
|
|
headers: authHeaders(),
|
|
});
|
|
expect(response.status()).toBe(200);
|
|
|
|
const body = await response.json();
|
|
expect(body.prompts).toBeDefined();
|
|
expect(Array.isArray(body.prompts)).toBeTruthy();
|
|
expect(body.prompts.length).toBeGreaterThan(0);
|
|
expect(body.pagination).toBeDefined();
|
|
expect(body.pagination.total).toBeGreaterThanOrEqual(5); // We seeded 5 prompts
|
|
});
|
|
|
|
test("filters prompts by brandId", async ({ request }) => {
|
|
const response = await request.get(`/api/v1/prompts?brandId=${BRAND_ID}`, {
|
|
headers: authHeaders(),
|
|
});
|
|
expect(response.status()).toBe(200);
|
|
|
|
const body = await response.json();
|
|
expect(body.prompts.length).toBeGreaterThan(0);
|
|
|
|
// All returned prompts should belong to the specified brand
|
|
for (const prompt of body.prompts) {
|
|
expect(prompt.brandId).toBe(BRAND_ID);
|
|
}
|
|
});
|
|
|
|
test("returns empty list for non-existent brand", async ({ request }) => {
|
|
const response = await request.get(`/api/v1/prompts?brandId=non-existent`, {
|
|
headers: authHeaders(),
|
|
});
|
|
expect(response.status()).toBe(200);
|
|
|
|
const body = await response.json();
|
|
expect(body.prompts).toHaveLength(0);
|
|
expect(body.pagination.total).toBe(0);
|
|
});
|
|
|
|
test("supports pagination", async ({ request }) => {
|
|
const response = await request.get(`/api/v1/prompts?page=1&limit=2`, {
|
|
headers: authHeaders(),
|
|
});
|
|
expect(response.status()).toBe(200);
|
|
|
|
const body = await response.json();
|
|
expect(body.prompts.length).toBeLessThanOrEqual(2);
|
|
expect(body.pagination.page).toBe(1);
|
|
expect(body.pagination.limit).toBe(2);
|
|
expect(body.pagination.totalPages).toBeGreaterThan(1); // 5 prompts / 2 per page
|
|
});
|
|
});
|
|
|
|
test.describe("External API - GET /api/v1/prompts/:id", () => {
|
|
const PROMPT_ID = "00000000-0000-0000-0000-000000000001";
|
|
|
|
test("returns a specific prompt", async ({ request }) => {
|
|
const response = await request.get(`/api/v1/prompts/${PROMPT_ID}`, {
|
|
headers: authHeaders(),
|
|
});
|
|
expect(response.status()).toBe(200);
|
|
|
|
const body = await response.json();
|
|
expect(body.id).toBe(PROMPT_ID);
|
|
expect(body.brandId).toBe(BRAND_ID);
|
|
expect(body.value).toContain("monitoring tool");
|
|
expect(body.tags).toContain("monitoring");
|
|
expect(body.enabled).toBe(true);
|
|
});
|
|
|
|
test("returns 404 for non-existent prompt", async ({ request }) => {
|
|
const response = await request.get(`/api/v1/prompts/00000000-0000-0000-0000-999999999999`, {
|
|
headers: authHeaders(),
|
|
});
|
|
expect(response.status()).toBe(404);
|
|
});
|
|
|
|
test("returns 400 for invalid UUID format", async ({ request }) => {
|
|
const response = await request.get(`/api/v1/prompts/not-a-uuid`, {
|
|
headers: authHeaders(),
|
|
});
|
|
expect(response.status()).toBe(400);
|
|
});
|
|
});
|
|
|
|
test.describe("External API - CRUD Operations", () => {
|
|
let createdPromptId: string;
|
|
|
|
test("POST creates a new prompt", async ({ request }) => {
|
|
const response = await request.post(`/api/v1/prompts`, {
|
|
headers: authHeaders(),
|
|
data: {
|
|
brandId: BRAND_ID,
|
|
value: "E2E test prompt - what is the best testing framework?",
|
|
tags: ["e2e-test", "testing"],
|
|
},
|
|
});
|
|
expect(response.status()).toBe(201);
|
|
|
|
const body = await response.json();
|
|
expect(body.id).toBeDefined();
|
|
expect(body.brandId).toBe(BRAND_ID);
|
|
expect(body.value).toContain("E2E test prompt");
|
|
expect(body.tags).toContain("e2e-test");
|
|
expect(body.enabled).toBe(true);
|
|
|
|
createdPromptId = body.id;
|
|
});
|
|
|
|
test("POST validates required fields", async ({ request }) => {
|
|
// Missing value
|
|
const response1 = await request.post(`/api/v1/prompts`, {
|
|
headers: authHeaders(),
|
|
data: { brandId: BRAND_ID },
|
|
});
|
|
expect(response1.status()).toBe(400);
|
|
|
|
// Missing brandId
|
|
const response2 = await request.post(`/api/v1/prompts`, {
|
|
headers: authHeaders(),
|
|
data: { value: "test prompt" },
|
|
});
|
|
expect(response2.status()).toBe(400);
|
|
});
|
|
|
|
test("POST rejects invalid brand ID", async ({ request }) => {
|
|
const response = await request.post(`/api/v1/prompts`, {
|
|
headers: authHeaders(),
|
|
data: {
|
|
brandId: "non-existent-brand",
|
|
value: "This should fail",
|
|
},
|
|
});
|
|
expect(response.status()).toBe(400);
|
|
const body = await response.json();
|
|
expect(body.message).toContain("not found");
|
|
});
|
|
|
|
test("PATCH updates a prompt", async ({ request }) => {
|
|
// First create a prompt to update
|
|
const createResponse = await request.post(`/api/v1/prompts`, {
|
|
headers: authHeaders(),
|
|
data: {
|
|
brandId: BRAND_ID,
|
|
value: "Original prompt text for PATCH test",
|
|
tags: ["original"],
|
|
},
|
|
});
|
|
const created = await createResponse.json();
|
|
|
|
// Update it
|
|
const response = await request.patch(`/api/v1/prompts/${created.id}`, {
|
|
headers: authHeaders(),
|
|
data: {
|
|
value: "Updated prompt text via PATCH",
|
|
tags: ["updated"],
|
|
},
|
|
});
|
|
expect(response.status()).toBe(200);
|
|
|
|
const body = await response.json();
|
|
expect(body.value).toBe("Updated prompt text via PATCH");
|
|
expect(body.tags).toContain("updated");
|
|
|
|
// Clean up
|
|
await request.delete(`/api/v1/prompts/${created.id}`, {
|
|
headers: authHeaders(),
|
|
});
|
|
});
|
|
|
|
test("PATCH rejects an empty body", async ({ request }) => {
|
|
const response = await request.patch(`/api/v1/prompts/00000000-0000-0000-0000-000000000001`, {
|
|
headers: authHeaders(),
|
|
data: {},
|
|
});
|
|
expect(response.status()).toBe(400);
|
|
|
|
const body = await response.json();
|
|
expect(body.error).toBe("Validation Error");
|
|
expect(body.message).toContain("At least one of");
|
|
});
|
|
|
|
test("DELETE removes a prompt", async ({ request }) => {
|
|
// First create a prompt to delete
|
|
const createResponse = await request.post(`/api/v1/prompts`, {
|
|
headers: authHeaders(),
|
|
data: {
|
|
brandId: BRAND_ID,
|
|
value: "Prompt to be deleted in E2E test",
|
|
},
|
|
});
|
|
const created = await createResponse.json();
|
|
|
|
// Delete it
|
|
const response = await request.delete(`/api/v1/prompts/${created.id}`, {
|
|
headers: authHeaders(),
|
|
});
|
|
expect(response.status()).toBe(200);
|
|
|
|
// DELETE returns the deleted resource bare, plus cascade info
|
|
const body = await response.json();
|
|
expect(body.id).toBe(created.id);
|
|
expect(body.value).toContain("Prompt to be deleted");
|
|
expect(body.deletedRunsCount).toBe(0);
|
|
|
|
// Verify it's gone
|
|
const getResponse = await request.get(`/api/v1/prompts/${created.id}`, {
|
|
headers: authHeaders(),
|
|
});
|
|
expect(getResponse.status()).toBe(404);
|
|
});
|
|
});
|