1331 lines
46 KiB
TypeScript
1331 lines
46 KiB
TypeScript
const API_URL = process.env.API_URL || 'http://localhost:3001';
|
|
const TOKEN = process.env.TEST_TOKEN;
|
|
|
|
if (!TOKEN) {
|
|
console.error('ERROR: TEST_TOKEN environment variable is required');
|
|
console.log('Run: npm run test:get-token');
|
|
console.log('Then: export TEST_TOKEN="<token>"');
|
|
process.exit(1);
|
|
}
|
|
|
|
interface TestResult {
|
|
name: string;
|
|
passed: boolean;
|
|
error?: string;
|
|
}
|
|
|
|
const results: TestResult[] = [];
|
|
|
|
async function request(
|
|
method: string,
|
|
path: string,
|
|
body?: unknown,
|
|
expectStatus = 200
|
|
): Promise<{ status: number; data: unknown }> {
|
|
const response = await fetch(`${API_URL}${path}`, {
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Authorization: `Bearer ${TOKEN}`,
|
|
},
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
|
|
const data = response.headers.get('content-type')?.includes('application/json')
|
|
? await response.json()
|
|
: null;
|
|
|
|
if (response.status !== expectStatus) {
|
|
throw new Error(`Expected ${expectStatus}, got ${response.status}: ${JSON.stringify(data)}`);
|
|
}
|
|
|
|
return { status: response.status, data };
|
|
}
|
|
|
|
async function test(name: string, fn: () => Promise<void>) {
|
|
try {
|
|
await fn();
|
|
results.push({ name, passed: true });
|
|
console.log(` ✓ ${name}`);
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
results.push({ name, passed: false, error: message });
|
|
console.log(` ✗ ${name}`);
|
|
console.log(` ${message}`);
|
|
}
|
|
}
|
|
|
|
async function runTests() {
|
|
console.log('\n=== Kaboot API Tests ===\n');
|
|
console.log(`API: ${API_URL}`);
|
|
console.log('');
|
|
|
|
let createdQuizId: string | null = null;
|
|
|
|
console.log('Health Check:');
|
|
await test('GET /health returns ok', async () => {
|
|
const res = await fetch(`${API_URL}/health`);
|
|
const data = await res.json();
|
|
if (data.status !== 'ok') throw new Error('Health check failed');
|
|
});
|
|
|
|
console.log('\nAuth Tests:');
|
|
await test('GET /api/quizzes without token returns 401', async () => {
|
|
const res = await fetch(`${API_URL}/api/quizzes`);
|
|
if (res.status !== 401) throw new Error(`Expected 401, got ${res.status}`);
|
|
});
|
|
|
|
await test('GET /api/quizzes with invalid token returns 401', async () => {
|
|
const res = await fetch(`${API_URL}/api/quizzes`, {
|
|
headers: { Authorization: 'Bearer invalid-token' },
|
|
});
|
|
if (res.status !== 401) throw new Error(`Expected 401, got ${res.status}`);
|
|
});
|
|
|
|
console.log('\nUser Tests:');
|
|
await test('GET /api/users/me returns user info', async () => {
|
|
const { data } = await request('GET', '/api/users/me');
|
|
const user = data as Record<string, unknown>;
|
|
if (!user.id) throw new Error('Missing user id');
|
|
if (!user.username) throw new Error('Missing username');
|
|
});
|
|
|
|
console.log('\nQuiz CRUD Tests:');
|
|
await test('GET /api/quizzes returns array', async () => {
|
|
const { data } = await request('GET', '/api/quizzes');
|
|
if (!Array.isArray(data)) throw new Error('Expected array');
|
|
});
|
|
|
|
await test('POST /api/quizzes creates quiz', async () => {
|
|
const quiz = {
|
|
title: 'Test Quiz',
|
|
source: 'manual',
|
|
questions: [
|
|
{
|
|
text: 'What is 2 + 2?',
|
|
timeLimit: 20,
|
|
options: [
|
|
{ text: '3', isCorrect: false, shape: 'triangle', color: 'red' },
|
|
{ text: '4', isCorrect: true, shape: 'diamond', color: 'blue' },
|
|
{ text: '5', isCorrect: false, shape: 'circle', color: 'yellow' },
|
|
{ text: '6', isCorrect: false, shape: 'square', color: 'green' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const { data } = await request('POST', '/api/quizzes', quiz, 201);
|
|
const result = data as { id: string };
|
|
if (!result.id) throw new Error('Missing quiz id');
|
|
createdQuizId = result.id;
|
|
});
|
|
|
|
await test('GET /api/quizzes/:id returns full quiz', async () => {
|
|
if (!createdQuizId) throw new Error('No quiz created');
|
|
const { data } = await request('GET', `/api/quizzes/${createdQuizId}`);
|
|
const quiz = data as Record<string, unknown>;
|
|
if (quiz.title !== 'Test Quiz') throw new Error('Wrong title');
|
|
if (!Array.isArray(quiz.questions)) throw new Error('Missing questions');
|
|
const questions = quiz.questions as Record<string, unknown>[];
|
|
if (questions.length !== 1) throw new Error('Wrong question count');
|
|
const q = questions[0];
|
|
if (!Array.isArray(q.options)) throw new Error('Missing options');
|
|
if ((q.options as unknown[]).length !== 4) throw new Error('Wrong option count');
|
|
});
|
|
|
|
await test('PUT /api/quizzes/:id updates quiz', async () => {
|
|
if (!createdQuizId) throw new Error('No quiz created');
|
|
const updatedQuiz = {
|
|
title: 'Updated Test Quiz',
|
|
questions: [
|
|
{
|
|
text: 'Updated question?',
|
|
timeLimit: 30,
|
|
options: [
|
|
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
|
|
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
|
|
{ text: 'C', isCorrect: false, shape: 'circle', color: 'yellow' },
|
|
{ text: 'D', isCorrect: false, shape: 'square', color: 'green' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
await request('PUT', `/api/quizzes/${createdQuizId}`, updatedQuiz);
|
|
|
|
const { data } = await request('GET', `/api/quizzes/${createdQuizId}`);
|
|
const quiz = data as Record<string, unknown>;
|
|
if (quiz.title !== 'Updated Test Quiz') throw new Error('Title not updated');
|
|
});
|
|
|
|
await test('DELETE /api/quizzes/:id deletes quiz', async () => {
|
|
if (!createdQuizId) throw new Error('No quiz created');
|
|
await request('DELETE', `/api/quizzes/${createdQuizId}`, undefined, 204);
|
|
});
|
|
|
|
await test('GET /api/quizzes/:id returns 404 for deleted quiz', async () => {
|
|
if (!createdQuizId) throw new Error('No quiz created');
|
|
await request('GET', `/api/quizzes/${createdQuizId}`, undefined, 404);
|
|
});
|
|
|
|
console.log('\nQuiz Validation Tests:');
|
|
await test('POST /api/quizzes without title returns 400', async () => {
|
|
const invalidQuiz = {
|
|
source: 'manual',
|
|
questions: [
|
|
{
|
|
text: 'Question?',
|
|
options: [
|
|
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
await request('POST', '/api/quizzes', invalidQuiz, 400);
|
|
});
|
|
|
|
await test('POST /api/quizzes without source returns 400', async () => {
|
|
const invalidQuiz = {
|
|
title: 'Missing Source Quiz',
|
|
questions: [
|
|
{
|
|
text: 'Question?',
|
|
options: [
|
|
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
await request('POST', '/api/quizzes', invalidQuiz, 400);
|
|
});
|
|
|
|
await test('POST /api/quizzes without questions returns 400', async () => {
|
|
const invalidQuiz = {
|
|
title: 'No Questions Quiz',
|
|
source: 'manual',
|
|
questions: [],
|
|
};
|
|
await request('POST', '/api/quizzes', invalidQuiz, 400);
|
|
});
|
|
|
|
await test('POST /api/quizzes with empty body returns 400', async () => {
|
|
await request('POST', '/api/quizzes', {}, 400);
|
|
});
|
|
|
|
console.log('\nQuiz Not Found Tests:');
|
|
await test('GET /api/quizzes/:id with non-existent ID returns 404', async () => {
|
|
await request('GET', '/api/quizzes/non-existent-uuid-12345', undefined, 404);
|
|
});
|
|
|
|
await test('PUT /api/quizzes/:id with non-existent ID returns 404', async () => {
|
|
const quiz = {
|
|
title: 'Update Non-Existent',
|
|
questions: [
|
|
{
|
|
text: 'Q?',
|
|
options: [
|
|
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
|
|
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
await request('PUT', '/api/quizzes/non-existent-uuid-12345', quiz, 404);
|
|
});
|
|
|
|
await test('DELETE /api/quizzes/:id with non-existent ID returns 404', async () => {
|
|
await request('DELETE', '/api/quizzes/non-existent-uuid-12345', undefined, 404);
|
|
});
|
|
|
|
console.log('\nQuiz Source Types Tests:');
|
|
let aiQuizId: string | null = null;
|
|
|
|
await test('POST /api/quizzes with ai_generated source and aiTopic', async () => {
|
|
const aiQuiz = {
|
|
title: 'AI Generated Quiz',
|
|
source: 'ai_generated',
|
|
aiTopic: 'Space Exploration',
|
|
questions: [
|
|
{
|
|
text: 'What planet is known as the Red Planet?',
|
|
timeLimit: 20,
|
|
options: [
|
|
{ text: 'Venus', isCorrect: false, shape: 'triangle', color: 'red' },
|
|
{ text: 'Mars', isCorrect: true, shape: 'diamond', color: 'blue' },
|
|
{ text: 'Jupiter', isCorrect: false, shape: 'circle', color: 'yellow' },
|
|
{ text: 'Saturn', isCorrect: false, shape: 'square', color: 'green' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const { data } = await request('POST', '/api/quizzes', aiQuiz, 201);
|
|
const result = data as { id: string };
|
|
if (!result.id) throw new Error('Missing quiz id');
|
|
aiQuizId = result.id;
|
|
});
|
|
|
|
await test('GET /api/quizzes/:id returns aiTopic for AI quiz', async () => {
|
|
if (!aiQuizId) throw new Error('No AI quiz created');
|
|
const { data } = await request('GET', `/api/quizzes/${aiQuizId}`);
|
|
const quiz = data as Record<string, unknown>;
|
|
if (quiz.source !== 'ai_generated') throw new Error('Wrong source');
|
|
if (quiz.aiTopic !== 'Space Exploration') throw new Error('Missing or wrong aiTopic');
|
|
});
|
|
|
|
await test('GET /api/quizzes list includes source and questionCount', async () => {
|
|
const { data } = await request('GET', '/api/quizzes');
|
|
const quizzes = data as Record<string, unknown>[];
|
|
if (quizzes.length === 0) throw new Error('Expected at least one quiz');
|
|
const quiz = quizzes.find((q) => q.id === aiQuizId);
|
|
if (!quiz) throw new Error('AI quiz not in list');
|
|
if (quiz.source !== 'ai_generated') throw new Error('Missing source in list');
|
|
if (typeof quiz.questionCount !== 'number') throw new Error('Missing questionCount');
|
|
if (quiz.questionCount !== 1) throw new Error('Wrong questionCount');
|
|
});
|
|
|
|
await test('DELETE cleanup AI quiz', async () => {
|
|
if (!aiQuizId) throw new Error('No AI quiz to delete');
|
|
await request('DELETE', `/api/quizzes/${aiQuizId}`, undefined, 204);
|
|
});
|
|
|
|
console.log('\nQuiz with Multiple Questions Tests:');
|
|
let multiQuizId: string | null = null;
|
|
|
|
await test('POST /api/quizzes with multiple questions', async () => {
|
|
const multiQuiz = {
|
|
title: 'Multi-Question Quiz',
|
|
source: 'manual',
|
|
questions: [
|
|
{
|
|
text: 'Question 1?',
|
|
timeLimit: 15,
|
|
options: [
|
|
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
|
|
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
|
|
],
|
|
},
|
|
{
|
|
text: 'Question 2?',
|
|
timeLimit: 25,
|
|
options: [
|
|
{ text: 'X', isCorrect: false, shape: 'circle', color: 'yellow' },
|
|
{ text: 'Y', isCorrect: true, shape: 'square', color: 'green' },
|
|
],
|
|
},
|
|
{
|
|
text: 'Question 3?',
|
|
timeLimit: 30,
|
|
options: [
|
|
{ text: 'P', isCorrect: false, shape: 'triangle', color: 'red', reason: 'Wrong because...' },
|
|
{ text: 'Q', isCorrect: true, shape: 'diamond', color: 'blue', reason: 'Correct because...' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const { data } = await request('POST', '/api/quizzes', multiQuiz, 201);
|
|
const result = data as { id: string };
|
|
multiQuizId = result.id;
|
|
});
|
|
|
|
await test('GET /api/quizzes/:id returns all questions with correct order', async () => {
|
|
if (!multiQuizId) throw new Error('No multi-question quiz created');
|
|
const { data } = await request('GET', `/api/quizzes/${multiQuizId}`);
|
|
const quiz = data as { questions: { text: string; timeLimit: number; options: { reason?: string }[] }[] };
|
|
if (quiz.questions.length !== 3) throw new Error(`Expected 3 questions, got ${quiz.questions.length}`);
|
|
if (quiz.questions[0].text !== 'Question 1?') throw new Error('Wrong order for Q1');
|
|
if (quiz.questions[1].text !== 'Question 2?') throw new Error('Wrong order for Q2');
|
|
if (quiz.questions[2].text !== 'Question 3?') throw new Error('Wrong order for Q3');
|
|
if (quiz.questions[0].timeLimit !== 15) throw new Error('Wrong timeLimit for Q1');
|
|
if (quiz.questions[2].options[1].reason !== 'Correct because...') throw new Error('Missing reason field');
|
|
});
|
|
|
|
await test('GET /api/quizzes shows correct questionCount for multi-question quiz', async () => {
|
|
const { data } = await request('GET', '/api/quizzes');
|
|
const quizzes = data as Record<string, unknown>[];
|
|
const quiz = quizzes.find((q) => q.id === multiQuizId);
|
|
if (!quiz) throw new Error('Multi-question quiz not in list');
|
|
if (quiz.questionCount !== 3) throw new Error(`Expected questionCount 3, got ${quiz.questionCount}`);
|
|
});
|
|
|
|
await test('PUT /api/quizzes/:id replaces all questions', async () => {
|
|
if (!multiQuizId) throw new Error('No multi-question quiz created');
|
|
const updatedQuiz = {
|
|
title: 'Updated Multi Quiz',
|
|
questions: [
|
|
{
|
|
text: 'Only One Question Now',
|
|
timeLimit: 10,
|
|
options: [
|
|
{ text: 'Solo', isCorrect: true, shape: 'triangle', color: 'red' },
|
|
{ text: 'Duo', isCorrect: false, shape: 'diamond', color: 'blue' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
await request('PUT', `/api/quizzes/${multiQuizId}`, updatedQuiz);
|
|
const { data } = await request('GET', `/api/quizzes/${multiQuizId}`);
|
|
const quiz = data as { title: string; questions: unknown[] };
|
|
if (quiz.title !== 'Updated Multi Quiz') throw new Error('Title not updated');
|
|
if (quiz.questions.length !== 1) throw new Error(`Expected 1 question after update, got ${quiz.questions.length}`);
|
|
});
|
|
|
|
await test('DELETE cleanup multi-question quiz', async () => {
|
|
if (!multiQuizId) throw new Error('No multi-question quiz to delete');
|
|
await request('DELETE', `/api/quizzes/${multiQuizId}`, undefined, 204);
|
|
});
|
|
|
|
console.log('\nTimestamp Tests:');
|
|
let timestampQuizId: string | null = null;
|
|
|
|
await test('POST /api/quizzes returns quiz with timestamps', async () => {
|
|
const quiz = {
|
|
title: 'Timestamp Test Quiz',
|
|
source: 'manual',
|
|
questions: [
|
|
{
|
|
text: 'Timestamp Q?',
|
|
options: [
|
|
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
|
|
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const { data: createData } = await request('POST', '/api/quizzes', quiz, 201);
|
|
timestampQuizId = (createData as { id: string }).id;
|
|
|
|
const { data } = await request('GET', `/api/quizzes/${timestampQuizId}`);
|
|
const result = data as Record<string, unknown>;
|
|
if (!result.createdAt) throw new Error('Missing createdAt');
|
|
if (!result.updatedAt) throw new Error('Missing updatedAt');
|
|
});
|
|
|
|
await test('PUT /api/quizzes/:id updates updatedAt timestamp', async () => {
|
|
if (!timestampQuizId) throw new Error('No timestamp quiz created');
|
|
|
|
const { data: beforeData } = await request('GET', `/api/quizzes/${timestampQuizId}`);
|
|
const beforeUpdatedAt = (beforeData as Record<string, unknown>).updatedAt;
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 1100));
|
|
|
|
await request('PUT', `/api/quizzes/${timestampQuizId}`, {
|
|
title: 'Updated Timestamp Quiz',
|
|
questions: [
|
|
{
|
|
text: 'Updated Q?',
|
|
options: [
|
|
{ text: 'B', isCorrect: true, shape: 'diamond', color: 'blue' },
|
|
{ text: 'C', isCorrect: false, shape: 'circle', color: 'yellow' },
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
const { data: afterData } = await request('GET', `/api/quizzes/${timestampQuizId}`);
|
|
const afterUpdatedAt = (afterData as Record<string, unknown>).updatedAt;
|
|
|
|
if (beforeUpdatedAt === afterUpdatedAt) throw new Error('updatedAt should have changed');
|
|
});
|
|
|
|
await test('DELETE cleanup timestamp quiz', async () => {
|
|
if (!timestampQuizId) throw new Error('No timestamp quiz to delete');
|
|
await request('DELETE', `/api/quizzes/${timestampQuizId}`, undefined, 204);
|
|
});
|
|
|
|
console.log('\nSave Integration Tests (Phase 5):');
|
|
let manualSaveQuizId: string | null = null;
|
|
let aiSaveQuizId: string | null = null;
|
|
|
|
await test('POST /api/quizzes manual quiz without aiTopic', async () => {
|
|
const manualQuiz = {
|
|
title: 'Manual Save Test Quiz',
|
|
source: 'manual',
|
|
questions: [
|
|
{
|
|
text: 'What is 1+1?',
|
|
timeLimit: 20,
|
|
options: [
|
|
{ text: '1', isCorrect: false, shape: 'triangle', color: 'red' },
|
|
{ text: '2', isCorrect: true, shape: 'diamond', color: 'blue' },
|
|
{ text: '3', isCorrect: false, shape: 'circle', color: 'yellow' },
|
|
{ text: '4', isCorrect: false, shape: 'square', color: 'green' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const { data } = await request('POST', '/api/quizzes', manualQuiz, 201);
|
|
const result = data as { id: string };
|
|
manualSaveQuizId = result.id;
|
|
});
|
|
|
|
await test('GET manual quiz has null aiTopic', async () => {
|
|
if (!manualSaveQuizId) throw new Error('No manual quiz created');
|
|
const { data } = await request('GET', `/api/quizzes/${manualSaveQuizId}`);
|
|
const quiz = data as Record<string, unknown>;
|
|
if (quiz.source !== 'manual') throw new Error('Wrong source');
|
|
if (quiz.aiTopic !== null) throw new Error(`Expected null aiTopic, got ${quiz.aiTopic}`);
|
|
});
|
|
|
|
await test('POST /api/quizzes ai_generated with empty aiTopic treated as null', async () => {
|
|
const aiQuiz = {
|
|
title: 'AI Quiz Empty Topic',
|
|
source: 'ai_generated',
|
|
aiTopic: '',
|
|
questions: [
|
|
{
|
|
text: 'Test?',
|
|
options: [
|
|
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
|
|
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const { data } = await request('POST', '/api/quizzes', aiQuiz, 201);
|
|
aiSaveQuizId = (data as { id: string }).id;
|
|
|
|
const { data: getResult } = await request('GET', `/api/quizzes/${aiSaveQuizId}`);
|
|
const quiz = getResult as Record<string, unknown>;
|
|
if (quiz.aiTopic !== null && quiz.aiTopic !== '') {
|
|
throw new Error(`Expected null/empty aiTopic for empty string, got ${quiz.aiTopic}`);
|
|
}
|
|
});
|
|
|
|
await test('DELETE cleanup manual save quiz', async () => {
|
|
if (manualSaveQuizId) {
|
|
await request('DELETE', `/api/quizzes/${manualSaveQuizId}`, undefined, 204);
|
|
}
|
|
});
|
|
|
|
await test('DELETE cleanup ai save quiz', async () => {
|
|
if (aiSaveQuizId) {
|
|
await request('DELETE', `/api/quizzes/${aiSaveQuizId}`, undefined, 204);
|
|
}
|
|
});
|
|
|
|
console.log('\nOption Preservation Tests:');
|
|
let optionQuizId: string | null = null;
|
|
|
|
await test('POST /api/quizzes preserves all option fields including reason', async () => {
|
|
const quizWithReasons = {
|
|
title: 'Quiz With Reasons',
|
|
source: 'manual',
|
|
questions: [
|
|
{
|
|
text: 'Capital of France?',
|
|
timeLimit: 15,
|
|
options: [
|
|
{ text: 'London', isCorrect: false, shape: 'triangle', color: 'red', reason: 'London is the capital of UK' },
|
|
{ text: 'Paris', isCorrect: true, shape: 'diamond', color: 'blue', reason: 'Correct! Paris is the capital of France' },
|
|
{ text: 'Berlin', isCorrect: false, shape: 'circle', color: 'yellow', reason: 'Berlin is the capital of Germany' },
|
|
{ text: 'Madrid', isCorrect: false, shape: 'square', color: 'green', reason: 'Madrid is the capital of Spain' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const { data } = await request('POST', '/api/quizzes', quizWithReasons, 201);
|
|
optionQuizId = (data as { id: string }).id;
|
|
|
|
const { data: getResult } = await request('GET', `/api/quizzes/${optionQuizId}`);
|
|
const quiz = getResult as { questions: { options: { text: string; isCorrect: boolean; shape: string; color: string; reason?: string }[] }[] };
|
|
|
|
const options = quiz.questions[0].options;
|
|
if (options.length !== 4) throw new Error('Expected 4 options');
|
|
|
|
const parisOpt = options.find(o => o.text === 'Paris');
|
|
if (!parisOpt) throw new Error('Paris option not found');
|
|
if (!parisOpt.isCorrect) throw new Error('Paris should be correct');
|
|
if (parisOpt.reason !== 'Correct! Paris is the capital of France') throw new Error('Paris reason not preserved');
|
|
|
|
const londonOpt = options.find(o => o.text === 'London');
|
|
if (!londonOpt) throw new Error('London option not found');
|
|
if (londonOpt.isCorrect) throw new Error('London should not be correct');
|
|
if (londonOpt.reason !== 'London is the capital of UK') throw new Error('London reason not preserved');
|
|
});
|
|
|
|
await test('POST /api/quizzes options without reason field are preserved', async () => {
|
|
const quizNoReasons = {
|
|
title: 'Quiz Without Reasons',
|
|
source: 'ai_generated',
|
|
aiTopic: 'Geography',
|
|
questions: [
|
|
{
|
|
text: 'Largest ocean?',
|
|
options: [
|
|
{ text: 'Atlantic', isCorrect: false, shape: 'triangle', color: 'red' },
|
|
{ text: 'Pacific', isCorrect: true, shape: 'diamond', color: 'blue' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const { data } = await request('POST', '/api/quizzes', quizNoReasons, 201);
|
|
const quizId = (data as { id: string }).id;
|
|
|
|
const { data: getResult } = await request('GET', `/api/quizzes/${quizId}`);
|
|
const quiz = getResult as { questions: { options: { reason?: string }[] }[] };
|
|
|
|
const options = quiz.questions[0].options;
|
|
const pacificOpt = options.find((o: any) => o.text === 'Pacific');
|
|
if (pacificOpt?.reason !== null && pacificOpt?.reason !== undefined) {
|
|
throw new Error('Expected null/undefined reason for option without reason');
|
|
}
|
|
|
|
await request('DELETE', `/api/quizzes/${quizId}`, undefined, 204);
|
|
});
|
|
|
|
await test('DELETE cleanup option quiz', async () => {
|
|
if (optionQuizId) {
|
|
await request('DELETE', `/api/quizzes/${optionQuizId}`, undefined, 204);
|
|
}
|
|
});
|
|
|
|
console.log('\nConcurrent Save Tests:');
|
|
|
|
await test('Multiple quizzes can be saved by same user', async () => {
|
|
const quiz1 = {
|
|
title: 'Concurrent Quiz 1',
|
|
source: 'manual',
|
|
questions: [{ text: 'Q1?', options: [{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' }, { text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' }] }],
|
|
};
|
|
const quiz2 = {
|
|
title: 'Concurrent Quiz 2',
|
|
source: 'ai_generated',
|
|
aiTopic: 'Science',
|
|
questions: [{ text: 'Q2?', options: [{ text: 'C', isCorrect: true, shape: 'circle', color: 'yellow' }, { text: 'D', isCorrect: false, shape: 'square', color: 'green' }] }],
|
|
};
|
|
|
|
const [res1, res2] = await Promise.all([
|
|
request('POST', '/api/quizzes', quiz1, 201),
|
|
request('POST', '/api/quizzes', quiz2, 201),
|
|
]);
|
|
|
|
const id1 = (res1.data as { id: string }).id;
|
|
const id2 = (res2.data as { id: string }).id;
|
|
|
|
if (id1 === id2) throw new Error('Quiz IDs should be unique');
|
|
|
|
const { data: listData } = await request('GET', '/api/quizzes');
|
|
const list = listData as { id: string; title: string }[];
|
|
|
|
const found1 = list.find(q => q.id === id1);
|
|
const found2 = list.find(q => q.id === id2);
|
|
|
|
if (!found1) throw new Error('Quiz 1 not in list');
|
|
if (!found2) throw new Error('Quiz 2 not in list');
|
|
|
|
await Promise.all([
|
|
request('DELETE', `/api/quizzes/${id1}`, undefined, 204),
|
|
request('DELETE', `/api/quizzes/${id2}`, undefined, 204),
|
|
]);
|
|
});
|
|
|
|
console.log('\nEdge Case Tests:');
|
|
|
|
await test('POST /api/quizzes with very long title', async () => {
|
|
const longTitle = 'A'.repeat(500);
|
|
const quiz = {
|
|
title: longTitle,
|
|
source: 'manual',
|
|
questions: [{ text: 'Q?', options: [{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' }, { text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' }] }],
|
|
};
|
|
|
|
const { data } = await request('POST', '/api/quizzes', quiz, 201);
|
|
const quizId = (data as { id: string }).id;
|
|
|
|
const { data: getResult } = await request('GET', `/api/quizzes/${quizId}`);
|
|
if ((getResult as { title: string }).title !== longTitle) {
|
|
throw new Error('Long title not preserved');
|
|
}
|
|
|
|
await request('DELETE', `/api/quizzes/${quizId}`, undefined, 204);
|
|
});
|
|
|
|
await test('POST /api/quizzes with special characters in title', async () => {
|
|
const specialTitle = 'Quiz with "quotes" & <tags> and emoji test';
|
|
const quiz = {
|
|
title: specialTitle,
|
|
source: 'manual',
|
|
questions: [{ text: 'Q?', options: [{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' }, { text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' }] }],
|
|
};
|
|
|
|
const { data } = await request('POST', '/api/quizzes', quiz, 201);
|
|
const quizId = (data as { id: string }).id;
|
|
|
|
const { data: getResult } = await request('GET', `/api/quizzes/${quizId}`);
|
|
if ((getResult as { title: string }).title !== specialTitle) {
|
|
throw new Error('Special characters not preserved');
|
|
}
|
|
|
|
await request('DELETE', `/api/quizzes/${quizId}`, undefined, 204);
|
|
});
|
|
|
|
await test('POST /api/quizzes with whitespace-only title returns 400', async () => {
|
|
const quiz = {
|
|
title: ' ',
|
|
source: 'manual',
|
|
questions: [{ text: 'Q?', options: [{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' }, { text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' }] }],
|
|
};
|
|
|
|
await request('POST', '/api/quizzes', quiz, 400);
|
|
});
|
|
|
|
console.log('\nPhase 6 - Error Handling Tests:');
|
|
|
|
await test('POST /api/quizzes with question without text returns 400', async () => {
|
|
const invalidQuiz = {
|
|
title: 'Quiz with empty question',
|
|
source: 'manual',
|
|
questions: [
|
|
{
|
|
text: '',
|
|
options: [
|
|
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
|
|
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
await request('POST', '/api/quizzes', invalidQuiz, 400);
|
|
});
|
|
|
|
await test('POST /api/quizzes with question with only one option returns 400', async () => {
|
|
const invalidQuiz = {
|
|
title: 'Quiz with single option',
|
|
source: 'manual',
|
|
questions: [
|
|
{
|
|
text: 'Question with one option?',
|
|
options: [
|
|
{ text: 'Only one', isCorrect: true, shape: 'triangle', color: 'red' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
await request('POST', '/api/quizzes', invalidQuiz, 400);
|
|
});
|
|
|
|
await test('POST /api/quizzes with question with no correct answer returns 400', async () => {
|
|
const invalidQuiz = {
|
|
title: 'Quiz with no correct answer',
|
|
source: 'manual',
|
|
questions: [
|
|
{
|
|
text: 'Question with no correct?',
|
|
options: [
|
|
{ text: 'A', isCorrect: false, shape: 'triangle', color: 'red' },
|
|
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
await request('POST', '/api/quizzes', invalidQuiz, 400);
|
|
});
|
|
|
|
await test('POST /api/quizzes with invalid source type returns 400', async () => {
|
|
const invalidQuiz = {
|
|
title: 'Quiz with invalid source',
|
|
source: 'invalid_source_type',
|
|
questions: [
|
|
{
|
|
text: 'Question?',
|
|
options: [
|
|
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
|
|
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
await request('POST', '/api/quizzes', invalidQuiz, 400);
|
|
});
|
|
|
|
await test('POST /api/quizzes with null questions returns 400', async () => {
|
|
const invalidQuiz = {
|
|
title: 'Quiz with null questions',
|
|
source: 'manual',
|
|
questions: null,
|
|
};
|
|
await request('POST', '/api/quizzes', invalidQuiz, 400);
|
|
});
|
|
|
|
await test('POST /api/quizzes with question missing options returns 400', async () => {
|
|
const invalidQuiz = {
|
|
title: 'Quiz missing options',
|
|
source: 'manual',
|
|
questions: [
|
|
{
|
|
text: 'Question without options?',
|
|
},
|
|
],
|
|
};
|
|
await request('POST', '/api/quizzes', invalidQuiz, 400);
|
|
});
|
|
|
|
await test('PUT /api/quizzes/:id with invalid data returns 400', async () => {
|
|
const validQuiz = {
|
|
title: 'Valid Quiz for Update Test',
|
|
source: 'manual',
|
|
questions: [
|
|
{
|
|
text: 'Valid question?',
|
|
options: [
|
|
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
|
|
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const { data } = await request('POST', '/api/quizzes', validQuiz, 201);
|
|
const quizId = (data as { id: string }).id;
|
|
|
|
const invalidUpdate = {
|
|
title: '',
|
|
questions: [],
|
|
};
|
|
|
|
await request('PUT', `/api/quizzes/${quizId}`, invalidUpdate, 400);
|
|
await request('DELETE', `/api/quizzes/${quizId}`, undefined, 204);
|
|
});
|
|
|
|
console.log('\nPhase 6 - Malformed Request Tests:');
|
|
|
|
await test('POST /api/quizzes with malformed JSON returns 400', async () => {
|
|
const res = await fetch(`${API_URL}/api/quizzes`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Authorization: `Bearer ${TOKEN}`,
|
|
},
|
|
body: '{ invalid json }',
|
|
});
|
|
if (res.status !== 400) throw new Error(`Expected 400, got ${res.status}`);
|
|
});
|
|
|
|
await test('GET /api/quizzes/:id with very long ID returns 404', async () => {
|
|
const longId = 'a'.repeat(1000);
|
|
await request('GET', `/api/quizzes/${longId}`, undefined, 404);
|
|
});
|
|
|
|
await test('DELETE /api/quizzes/:id with SQL injection attempt returns 404', async () => {
|
|
const maliciousId = "'; DROP TABLE quizzes; --";
|
|
await request('DELETE', `/api/quizzes/${encodeURIComponent(maliciousId)}`, undefined, 404);
|
|
});
|
|
|
|
console.log('\nPhase 6 - Content Type Tests:');
|
|
|
|
await test('POST /api/quizzes without Content-Type still works with JSON body', async () => {
|
|
const quiz = {
|
|
title: 'No Content-Type Quiz',
|
|
source: 'manual',
|
|
questions: [
|
|
{
|
|
text: 'Question?',
|
|
options: [
|
|
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
|
|
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const res = await fetch(`${API_URL}/api/quizzes`, {
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Bearer ${TOKEN}`,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(quiz),
|
|
});
|
|
|
|
if (res.status !== 201) throw new Error(`Expected 201, got ${res.status}`);
|
|
const data = await res.json();
|
|
await request('DELETE', `/api/quizzes/${data.id}`, undefined, 204);
|
|
});
|
|
|
|
console.log('\nPhase 6 - Boundary Tests:');
|
|
|
|
await test('POST /api/quizzes with many questions succeeds', async () => {
|
|
const manyQuestions = Array.from({ length: 50 }, (_, i) => ({
|
|
text: `Question ${i + 1}?`,
|
|
timeLimit: 20,
|
|
options: [
|
|
{ text: `A${i}`, isCorrect: true, shape: 'triangle', color: 'red' },
|
|
{ text: `B${i}`, isCorrect: false, shape: 'diamond', color: 'blue' },
|
|
],
|
|
}));
|
|
|
|
const quiz = {
|
|
title: '50 Question Quiz',
|
|
source: 'manual',
|
|
questions: manyQuestions,
|
|
};
|
|
|
|
const { data } = await request('POST', '/api/quizzes', quiz, 201);
|
|
const quizId = (data as { id: string }).id;
|
|
|
|
const { data: getResult } = await request('GET', `/api/quizzes/${quizId}`);
|
|
const savedQuiz = getResult as { questions: unknown[] };
|
|
if (savedQuiz.questions.length !== 50) {
|
|
throw new Error(`Expected 50 questions, got ${savedQuiz.questions.length}`);
|
|
}
|
|
|
|
await request('DELETE', `/api/quizzes/${quizId}`, undefined, 204);
|
|
});
|
|
|
|
await test('POST /api/quizzes with many options per question succeeds', async () => {
|
|
const manyOptions = Array.from({ length: 10 }, (_, i) => ({
|
|
text: `Option ${i + 1}`,
|
|
isCorrect: i === 0,
|
|
shape: 'triangle',
|
|
color: 'red',
|
|
}));
|
|
|
|
const quiz = {
|
|
title: '10 Options Quiz',
|
|
source: 'manual',
|
|
questions: [
|
|
{
|
|
text: 'Question with 10 options?',
|
|
options: manyOptions,
|
|
},
|
|
],
|
|
};
|
|
|
|
const { data } = await request('POST', '/api/quizzes', quiz, 201);
|
|
const quizId = (data as { id: string }).id;
|
|
|
|
const { data: getResult } = await request('GET', `/api/quizzes/${quizId}`);
|
|
const savedQuiz = getResult as { questions: { options: unknown[] }[] };
|
|
if (savedQuiz.questions[0].options.length !== 10) {
|
|
throw new Error(`Expected 10 options, got ${savedQuiz.questions[0].options.length}`);
|
|
}
|
|
|
|
await request('DELETE', `/api/quizzes/${quizId}`, undefined, 204);
|
|
});
|
|
|
|
await test('POST /api/quizzes with unicode characters in all fields', async () => {
|
|
const unicodeQuiz = {
|
|
title: 'Emoji Quiz title test',
|
|
source: 'manual',
|
|
aiTopic: 'Japanese test',
|
|
questions: [
|
|
{
|
|
text: 'What is this character?',
|
|
timeLimit: 30,
|
|
options: [
|
|
{ text: 'Option A', isCorrect: true, shape: 'triangle', color: 'red', reason: 'Because Emoji test!' },
|
|
{ text: 'Chinese characters', isCorrect: false, shape: 'diamond', color: 'blue' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const { data } = await request('POST', '/api/quizzes', unicodeQuiz, 201);
|
|
const quizId = (data as { id: string }).id;
|
|
|
|
const { data: getResult } = await request('GET', `/api/quizzes/${quizId}`);
|
|
const savedQuiz = getResult as { title: string; questions: { text: string; options: { text: string }[] }[] };
|
|
|
|
if (savedQuiz.title !== 'Emoji Quiz title test') throw new Error('Unicode title not preserved');
|
|
if (savedQuiz.questions[0].text !== 'What is this character?') throw new Error('Unicode question not preserved');
|
|
if (savedQuiz.questions[0].options[1].text !== 'Chinese characters') throw new Error('Unicode option not preserved');
|
|
|
|
await request('DELETE', `/api/quizzes/${quizId}`, undefined, 204);
|
|
});
|
|
|
|
console.log('\nPUT Endpoint Edge Case Tests:');
|
|
|
|
await test('PUT /api/quizzes/:id with whitespace-only title returns 400', async () => {
|
|
const validQuiz = {
|
|
title: 'Quiz for PUT whitespace test',
|
|
source: 'manual',
|
|
questions: [
|
|
{
|
|
text: 'Question?',
|
|
options: [
|
|
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
|
|
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const { data } = await request('POST', '/api/quizzes', validQuiz, 201);
|
|
const quizId = (data as { id: string }).id;
|
|
|
|
const invalidUpdate = {
|
|
title: ' ',
|
|
questions: [
|
|
{
|
|
text: 'Q?',
|
|
options: [
|
|
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
|
|
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
await request('PUT', `/api/quizzes/${quizId}`, invalidUpdate, 400);
|
|
await request('DELETE', `/api/quizzes/${quizId}`, undefined, 204);
|
|
});
|
|
|
|
await test('PUT /api/quizzes/:id with question without text returns 400', async () => {
|
|
const validQuiz = {
|
|
title: 'Quiz for PUT empty question test',
|
|
source: 'manual',
|
|
questions: [
|
|
{
|
|
text: 'Original question?',
|
|
options: [
|
|
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
|
|
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const { data } = await request('POST', '/api/quizzes', validQuiz, 201);
|
|
const quizId = (data as { id: string }).id;
|
|
|
|
const invalidUpdate = {
|
|
title: 'Updated title',
|
|
questions: [
|
|
{
|
|
text: '',
|
|
options: [
|
|
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
|
|
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
await request('PUT', `/api/quizzes/${quizId}`, invalidUpdate, 400);
|
|
await request('DELETE', `/api/quizzes/${quizId}`, undefined, 204);
|
|
});
|
|
|
|
await test('PUT /api/quizzes/:id with single option returns 400', async () => {
|
|
const validQuiz = {
|
|
title: 'Quiz for PUT single option test',
|
|
source: 'manual',
|
|
questions: [
|
|
{
|
|
text: 'Original question?',
|
|
options: [
|
|
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
|
|
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const { data } = await request('POST', '/api/quizzes', validQuiz, 201);
|
|
const quizId = (data as { id: string }).id;
|
|
|
|
const invalidUpdate = {
|
|
title: 'Updated title',
|
|
questions: [
|
|
{
|
|
text: 'Question with one option?',
|
|
options: [
|
|
{ text: 'Only one', isCorrect: true, shape: 'triangle', color: 'red' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
await request('PUT', `/api/quizzes/${quizId}`, invalidUpdate, 400);
|
|
await request('DELETE', `/api/quizzes/${quizId}`, undefined, 204);
|
|
});
|
|
|
|
await test('PUT /api/quizzes/:id with no correct answer returns 400', async () => {
|
|
const validQuiz = {
|
|
title: 'Quiz for PUT no correct test',
|
|
source: 'manual',
|
|
questions: [
|
|
{
|
|
text: 'Original question?',
|
|
options: [
|
|
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
|
|
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const { data } = await request('POST', '/api/quizzes', validQuiz, 201);
|
|
const quizId = (data as { id: string }).id;
|
|
|
|
const invalidUpdate = {
|
|
title: 'Updated title',
|
|
questions: [
|
|
{
|
|
text: 'Question with no correct?',
|
|
options: [
|
|
{ text: 'A', isCorrect: false, shape: 'triangle', color: 'red' },
|
|
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
await request('PUT', `/api/quizzes/${quizId}`, invalidUpdate, 400);
|
|
await request('DELETE', `/api/quizzes/${quizId}`, undefined, 204);
|
|
});
|
|
|
|
await test('PUT /api/quizzes/:id with null questions returns 400', async () => {
|
|
const validQuiz = {
|
|
title: 'Quiz for PUT null questions test',
|
|
source: 'manual',
|
|
questions: [
|
|
{
|
|
text: 'Original question?',
|
|
options: [
|
|
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
|
|
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const { data } = await request('POST', '/api/quizzes', validQuiz, 201);
|
|
const quizId = (data as { id: string }).id;
|
|
|
|
const invalidUpdate = {
|
|
title: 'Updated title',
|
|
questions: null,
|
|
};
|
|
|
|
await request('PUT', `/api/quizzes/${quizId}`, invalidUpdate, 400);
|
|
await request('DELETE', `/api/quizzes/${quizId}`, undefined, 204);
|
|
});
|
|
|
|
await test('PUT /api/quizzes/:id preserves source and aiTopic', async () => {
|
|
const aiQuiz = {
|
|
title: 'AI Quiz for PUT preserve test',
|
|
source: 'ai_generated',
|
|
aiTopic: 'History',
|
|
questions: [
|
|
{
|
|
text: 'Original question?',
|
|
options: [
|
|
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
|
|
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const { data } = await request('POST', '/api/quizzes', aiQuiz, 201);
|
|
const quizId = (data as { id: string }).id;
|
|
|
|
const update = {
|
|
title: 'Updated AI Quiz',
|
|
questions: [
|
|
{
|
|
text: 'New question?',
|
|
options: [
|
|
{ text: 'X', isCorrect: true, shape: 'circle', color: 'yellow' },
|
|
{ text: 'Y', isCorrect: false, shape: 'square', color: 'green' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
await request('PUT', `/api/quizzes/${quizId}`, update);
|
|
|
|
const { data: getResult } = await request('GET', `/api/quizzes/${quizId}`);
|
|
const quiz = getResult as Record<string, unknown>;
|
|
|
|
if (quiz.source !== 'ai_generated') throw new Error('Source should be preserved');
|
|
if (quiz.aiTopic !== 'History') throw new Error('aiTopic should be preserved');
|
|
if (quiz.title !== 'Updated AI Quiz') throw new Error('Title should be updated');
|
|
|
|
await request('DELETE', `/api/quizzes/${quizId}`, undefined, 204);
|
|
});
|
|
|
|
await test('PUT /api/quizzes/:id on another users quiz returns 404', async () => {
|
|
const quiz = {
|
|
title: 'User isolation test',
|
|
questions: [
|
|
{
|
|
text: 'Q?',
|
|
options: [
|
|
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
|
|
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
await request('PUT', '/api/quizzes/non-existent-user-quiz-id', quiz, 404);
|
|
});
|
|
|
|
await test('PUT /api/quizzes/:id with many questions succeeds', async () => {
|
|
const validQuiz = {
|
|
title: 'Quiz for PUT many questions test',
|
|
source: 'manual',
|
|
questions: [
|
|
{
|
|
text: 'Original?',
|
|
options: [
|
|
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
|
|
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const { data } = await request('POST', '/api/quizzes', validQuiz, 201);
|
|
const quizId = (data as { id: string }).id;
|
|
|
|
const manyQuestions = Array.from({ length: 30 }, (_, i) => ({
|
|
text: `Updated question ${i + 1}?`,
|
|
timeLimit: 15,
|
|
options: [
|
|
{ text: `A${i}`, isCorrect: true, shape: 'triangle', color: 'red' },
|
|
{ text: `B${i}`, isCorrect: false, shape: 'diamond', color: 'blue' },
|
|
],
|
|
}));
|
|
|
|
const update = {
|
|
title: 'Quiz with 30 questions',
|
|
questions: manyQuestions,
|
|
};
|
|
|
|
await request('PUT', `/api/quizzes/${quizId}`, update);
|
|
|
|
const { data: getResult } = await request('GET', `/api/quizzes/${quizId}`);
|
|
const quiz = getResult as { questions: unknown[] };
|
|
if (quiz.questions.length !== 30) {
|
|
throw new Error(`Expected 30 questions, got ${quiz.questions.length}`);
|
|
}
|
|
|
|
await request('DELETE', `/api/quizzes/${quizId}`, undefined, 204);
|
|
});
|
|
|
|
await test('PUT /api/quizzes/:id preserves reason fields in options', async () => {
|
|
const validQuiz = {
|
|
title: 'Quiz for PUT reason test',
|
|
source: 'manual',
|
|
questions: [
|
|
{
|
|
text: 'Original?',
|
|
options: [
|
|
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
|
|
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const { data } = await request('POST', '/api/quizzes', validQuiz, 201);
|
|
const quizId = (data as { id: string }).id;
|
|
|
|
const updateWithReasons = {
|
|
title: 'Quiz with reasons',
|
|
questions: [
|
|
{
|
|
text: 'Why is the sky blue?',
|
|
options: [
|
|
{ text: 'Rayleigh scattering', isCorrect: true, shape: 'triangle', color: 'red', reason: 'Light scatters in atmosphere' },
|
|
{ text: 'Paint', isCorrect: false, shape: 'diamond', color: 'blue', reason: 'That is not how it works' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
await request('PUT', `/api/quizzes/${quizId}`, updateWithReasons);
|
|
|
|
const { data: getResult } = await request('GET', `/api/quizzes/${quizId}`);
|
|
const quiz = getResult as { questions: { options: { reason?: string }[] }[] };
|
|
|
|
const correctOpt = quiz.questions[0].options.find((o: any) => o.isCorrect);
|
|
if (correctOpt?.reason !== 'Light scatters in atmosphere') {
|
|
throw new Error('Reason not preserved on update');
|
|
}
|
|
|
|
await request('DELETE', `/api/quizzes/${quizId}`, undefined, 204);
|
|
});
|
|
|
|
console.log('\nPhase 6 - Duplicate/Idempotency Tests:');
|
|
|
|
await test('POST /api/quizzes with same data creates separate quizzes', async () => {
|
|
const quiz = {
|
|
title: 'Duplicate Test Quiz',
|
|
source: 'manual',
|
|
questions: [
|
|
{
|
|
text: 'Same question?',
|
|
options: [
|
|
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
|
|
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const { data: data1 } = await request('POST', '/api/quizzes', quiz, 201);
|
|
const { data: data2 } = await request('POST', '/api/quizzes', quiz, 201);
|
|
|
|
const id1 = (data1 as { id: string }).id;
|
|
const id2 = (data2 as { id: string }).id;
|
|
|
|
if (id1 === id2) throw new Error('Duplicate POST should create separate quizzes with unique IDs');
|
|
|
|
await request('DELETE', `/api/quizzes/${id1}`, undefined, 204);
|
|
await request('DELETE', `/api/quizzes/${id2}`, undefined, 204);
|
|
});
|
|
|
|
await test('DELETE /api/quizzes/:id twice returns 404 on second call', async () => {
|
|
const quiz = {
|
|
title: 'Double Delete Quiz',
|
|
source: 'manual',
|
|
questions: [
|
|
{
|
|
text: 'Q?',
|
|
options: [
|
|
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
|
|
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const { data } = await request('POST', '/api/quizzes', quiz, 201);
|
|
const quizId = (data as { id: string }).id;
|
|
|
|
await request('DELETE', `/api/quizzes/${quizId}`, undefined, 204);
|
|
await request('DELETE', `/api/quizzes/${quizId}`, undefined, 404);
|
|
});
|
|
|
|
console.log('\n=== Results ===');
|
|
const passed = results.filter((r) => r.passed).length;
|
|
const failed = results.filter((r) => !r.passed).length;
|
|
console.log(`Passed: ${passed}/${results.length}`);
|
|
console.log(`Failed: ${failed}/${results.length}`);
|
|
|
|
if (failed > 0) {
|
|
console.log('\nFailed tests:');
|
|
results
|
|
.filter((r) => !r.passed)
|
|
.forEach((r) => console.log(` - ${r.name}: ${r.error}`));
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log('\nAll tests passed!');
|
|
}
|
|
|
|
runTests().catch((err) => {
|
|
console.error('Test runner error:', err);
|
|
process.exit(1);
|
|
});
|