Phase 6 complete

This commit is contained in:
Joey Yakimowich-Payne 2026-01-13 16:52:57 -07:00
commit 3a22b42492
No known key found for this signature in database
GPG key ID: 6BFE655FA5ABD1E1
11 changed files with 735 additions and 103 deletions

View file

@ -223,7 +223,10 @@ async function runTests() {
questions: [
{
text: 'Q?',
options: [{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' }],
options: [
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
],
},
],
};
@ -356,6 +359,7 @@ async function runTests() {
timeLimit: 10,
options: [
{ text: 'Solo', isCorrect: true, shape: 'triangle', color: 'red' },
{ text: 'Duo', isCorrect: false, shape: 'diamond', color: 'blue' },
],
},
],
@ -383,7 +387,10 @@ async function runTests() {
questions: [
{
text: 'Timestamp Q?',
options: [{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' }],
options: [
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
],
},
],
};
@ -410,7 +417,10 @@ async function runTests() {
questions: [
{
text: 'Updated Q?',
options: [{ text: 'B', isCorrect: true, shape: 'diamond', color: 'blue' }],
options: [
{ text: 'B', isCorrect: true, shape: 'diamond', color: 'blue' },
{ text: 'C', isCorrect: false, shape: 'circle', color: 'yellow' },
],
},
],
});
@ -469,7 +479,10 @@ async function runTests() {
questions: [
{
text: 'Test?',
options: [{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' }],
options: [
{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' },
{ text: 'B', isCorrect: false, shape: 'diamond', color: 'blue' },
],
},
],
};
@ -580,13 +593,13 @@ async function runTests() {
const quiz1 = {
title: 'Concurrent Quiz 1',
source: 'manual',
questions: [{ text: 'Q1?', options: [{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' }] }],
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: 'B', isCorrect: true, shape: 'diamond', color: 'blue' }] }],
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([
@ -621,7 +634,7 @@ async function runTests() {
const quiz = {
title: longTitle,
source: 'manual',
questions: [{ text: 'Q?', options: [{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' }] }],
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);
@ -640,7 +653,7 @@ async function runTests() {
const quiz = {
title: specialTitle,
source: 'manual',
questions: [{ text: 'Q?', options: [{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' }] }],
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);
@ -658,12 +671,327 @@ async function runTests() {
const quiz = {
title: ' ',
source: 'manual',
questions: [{ text: 'Q?', options: [{ text: 'A', isCorrect: true, shape: 'triangle', color: 'red' }] }],
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('\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;