add a lot of changes

This commit is contained in:
AD2025
2025-11-12 00:49:22 +02:00
parent e3ca132c5e
commit a5fac2aaf7
17 changed files with 6704 additions and 1 deletions

View File

@@ -0,0 +1,484 @@
/**
* Test Script: Submit Answer API
*
* Tests:
* - Submit correct answer
* - Submit incorrect answer
* - Validation (missing fields, invalid UUIDs, session status)
* - Authorization (own session only)
* - Duplicate answer prevention
* - Question belongs to session
* - Progress tracking
* - Stats updates
*/
const axios = require('axios');
require('dotenv').config();
const BASE_URL = process.env.API_URL || 'http://localhost:3000';
const API_URL = `${BASE_URL}/api`;
// Test users
let adminToken = null;
let userToken = null;
let user2Token = null;
let guestToken = null;
// Test data
let testCategoryId = null;
let quizSessionId = null;
let guestQuizSessionId = null;
let questionIds = [];
// Test results
const results = {
passed: 0,
failed: 0,
total: 0
};
// Helper: Print section header
const printSection = (title) => {
console.log(`\n${'='.repeat(40)}`);
console.log(title);
console.log('='.repeat(40) + '\n');
};
// Helper: Log test result
const logTest = (testName, passed, details = '') => {
results.total++;
if (passed) {
results.passed++;
console.log(`✓ Test ${results.total}: ${testName} - PASSED`);
if (details) console.log(` ${details}`);
} else {
results.failed++;
console.log(`✗ Test ${results.total}: ${testName} - FAILED`);
if (details) console.log(` ${details}`);
}
};
// Helper: Auth config
const authConfig = (token) => ({
headers: { Authorization: `Bearer ${token}` }
});
// Helper: Guest auth config
const guestAuthConfig = (token) => ({
headers: { 'X-Guest-Token': token }
});
// Setup: Login users and create test data
async function setup() {
try {
printSection('Testing Submit Answer API');
// Login as admin
const adminRes = await axios.post(`${API_URL}/auth/login`, {
email: 'admin@quiz.com',
password: 'Admin@123'
});
adminToken = adminRes.data.data.token;
console.log('✓ Logged in as admin');
// Register and login user 1
try {
await axios.post(`${API_URL}/auth/register`, {
username: 'testuser1',
email: 'testuser1@quiz.com',
password: 'User@123'
});
} catch (err) {
// User may already exist
}
const userRes = await axios.post(`${API_URL}/auth/login`, {
email: 'testuser1@quiz.com',
password: 'User@123'
});
userToken = userRes.data.data.token;
console.log('✓ Logged in as testuser1');
// Register and login user 2
try {
await axios.post(`${API_URL}/auth/register`, {
username: 'testuser2',
email: 'testuser2@quiz.com',
password: 'User@123'
});
} catch (err) {
// User may already exist
}
const user2Res = await axios.post(`${API_URL}/auth/login`, {
email: 'testuser2@quiz.com',
password: 'User@123'
});
user2Token = user2Res.data.data.token;
console.log('✓ Logged in as testuser2');
// Start guest session
const guestRes = await axios.post(`${API_URL}/guest/start-session`, {
deviceId: 'test-device'
});
guestToken = guestRes.data.data.sessionToken;
console.log('✓ Started guest session');
// Get a guest-accessible category with questions
const categoriesRes = await axios.get(`${API_URL}/categories`, authConfig(userToken));
testCategoryId = categoriesRes.data.data.find(c => c.questionCount > 0 && c.guestAccessible)?.id;
if (!testCategoryId) {
// Fallback to any category with questions
testCategoryId = categoriesRes.data.data.find(c => c.questionCount > 0)?.id;
}
console.log(`✓ Using test category: ${testCategoryId}\n`);
// Start a quiz session for user
const quizRes = await axios.post(`${API_URL}/quiz/start`, {
categoryId: testCategoryId,
questionCount: 3,
difficulty: 'mixed',
quizType: 'practice'
}, authConfig(userToken));
quizSessionId = quizRes.data.data.sessionId;
questionIds = quizRes.data.data.questions.map(q => ({
id: q.id,
type: q.questionType,
options: q.options
}));
console.log(`✓ Created quiz session: ${quizSessionId} with ${questionIds.length} questions\n`);
// Start a quiz session for guest
const guestQuizRes = await axios.post(`${API_URL}/quiz/start`, {
categoryId: testCategoryId,
questionCount: 2,
difficulty: 'mixed',
quizType: 'practice'
}, guestAuthConfig(guestToken));
guestQuizSessionId = guestQuizRes.data.data.sessionId;
console.log(`✓ Created guest quiz session: ${guestQuizSessionId}\n`);
} catch (error) {
console.error('Setup error:', error.response?.data || error.message);
process.exit(1);
}
}
// Run all tests
async function runTests() {
await setup();
// Test 1: Submit correct answer
try {
// For testing purposes, we'll submit a test answer and check if the response structure is correct
// We can't know the correct answer without admin access to the question, so we'll submit
// and check if we get valid feedback (even if answer is wrong)
const res = await axios.post(`${API_URL}/quiz/submit`, {
quizSessionId: quizSessionId,
questionId: questionIds[0].id,
userAnswer: 'a', // Try option 'a'
timeSpent: 15
}, authConfig(userToken));
// Check if the response has proper structure regardless of correctness
const hasProperStructure = res.status === 201
&& res.data.data.isCorrect !== undefined
&& res.data.data.pointsEarned !== undefined
&& res.data.data.sessionProgress !== undefined
&& res.data.data.sessionProgress.questionsAnswered === 1
&& res.data.data.feedback.explanation !== undefined;
// If incorrect, correct answer should be shown
if (!res.data.data.isCorrect) {
const passed = hasProperStructure && res.data.data.feedback.correctAnswer !== undefined;
logTest('Submit answer returns proper feedback', passed,
passed ? `Answer was incorrect, got feedback with correct answer` : `Response: ${JSON.stringify(res.data)}`);
} else {
const passed = hasProperStructure && res.data.data.pointsEarned > 0;
logTest('Submit answer returns proper feedback', passed,
passed ? `Answer was correct, earned ${res.data.data.pointsEarned} points` : `Response: ${JSON.stringify(res.data)}`);
}
} catch (error) {
logTest('Submit answer returns proper feedback', false, error.response?.data?.message || error.message);
}
// Test 2: Submit incorrect answer (we'll intentionally use wrong answer)
try {
const res = await axios.post(`${API_URL}/quiz/submit`, {
quizSessionId: quizSessionId,
questionId: questionIds[1].id,
userAnswer: 'wrong_answer_xyz',
timeSpent: 20
}, authConfig(userToken));
const passed = res.status === 201
&& res.data.data.isCorrect === false
&& res.data.data.pointsEarned === 0
&& res.data.data.feedback.correctAnswer !== undefined // Should show correct answer
&& res.data.data.sessionProgress.questionsAnswered === 2;
logTest('Submit incorrect answer shows correct answer', passed,
passed ? `0 points earned, correct answer shown, progress: 2/3` : `Response: ${JSON.stringify(res.data)}`);
} catch (error) {
logTest('Submit incorrect answer shows correct answer', false, error.response?.data?.message || error.message);
}
// Test 3: Feedback includes explanation
try {
// Submit for question 3
const questionRes = await axios.get(`${API_URL}/questions/${questionIds[2].id}`, authConfig(adminToken));
let correctAnswer = questionRes.data.data.correctAnswer || 'a';
// Parse if JSON array
try {
const parsed = JSON.parse(correctAnswer);
if (Array.isArray(parsed)) {
correctAnswer = parsed[0];
}
} catch (e) {
// Not JSON, use as is
}
const res = await axios.post(`${API_URL}/quiz/submit`, {
quizSessionId: quizSessionId,
questionId: questionIds[2].id,
userAnswer: correctAnswer,
timeSpent: 10
}, authConfig(userToken));
const passed = res.status === 201
&& res.data.data.feedback.explanation !== undefined
&& res.data.data.feedback.questionText !== undefined
&& res.data.data.feedback.category !== undefined;
logTest('Response includes feedback with explanation', passed,
passed ? 'Explanation and question details included' : `Response: ${JSON.stringify(res.data)}`);
} catch (error) {
logTest('Response includes feedback with explanation', false, error.response?.data?.message || error.message);
}
printSection('Testing Validation');
// Test 4: Missing quiz session ID
try {
await axios.post(`${API_URL}/quiz/submit`, {
questionId: questionIds[0].id,
userAnswer: 'a'
}, authConfig(userToken));
logTest('Missing quiz session ID returns 400', false, 'Should have failed');
} catch (error) {
const passed = error.response?.status === 400
&& error.response?.data?.message.includes('session ID is required');
logTest('Missing quiz session ID returns 400', passed,
passed ? 'Correctly rejected' : error.response?.data?.message);
}
// Test 5: Missing question ID
try {
await axios.post(`${API_URL}/quiz/submit`, {
quizSessionId: quizSessionId,
userAnswer: 'a'
}, authConfig(userToken));
logTest('Missing question ID returns 400', false, 'Should have failed');
} catch (error) {
const passed = error.response?.status === 400
&& error.response?.data?.message.includes('Question ID is required');
logTest('Missing question ID returns 400', passed,
passed ? 'Correctly rejected' : error.response?.data?.message);
}
// Test 6: Missing user answer
try {
await axios.post(`${API_URL}/quiz/submit`, {
quizSessionId: quizSessionId,
questionId: questionIds[0].id
}, authConfig(userToken));
logTest('Missing user answer returns 400', false, 'Should have failed');
} catch (error) {
const passed = error.response?.status === 400
&& error.response?.data?.message.includes('answer is required');
logTest('Missing user answer returns 400', passed,
passed ? 'Correctly rejected' : error.response?.data?.message);
}
// Test 7: Invalid quiz session UUID
try {
await axios.post(`${API_URL}/quiz/submit`, {
quizSessionId: 'invalid-uuid',
questionId: questionIds[0].id,
userAnswer: 'a'
}, authConfig(userToken));
logTest('Invalid quiz session UUID returns 400', false, 'Should have failed');
} catch (error) {
const passed = error.response?.status === 400
&& error.response?.data?.message.includes('Invalid quiz session ID');
logTest('Invalid quiz session UUID returns 400', passed,
passed ? 'Correctly rejected' : error.response?.data?.message);
}
// Test 8: Invalid question UUID
try {
await axios.post(`${API_URL}/quiz/submit`, {
quizSessionId: quizSessionId,
questionId: 'invalid-uuid',
userAnswer: 'a'
}, authConfig(userToken));
logTest('Invalid question UUID returns 400', false, 'Should have failed');
} catch (error) {
const passed = error.response?.status === 400
&& error.response?.data?.message.includes('Invalid question ID');
logTest('Invalid question UUID returns 400', passed,
passed ? 'Correctly rejected' : error.response?.data?.message);
}
// Test 9: Non-existent quiz session
try {
await axios.post(`${API_URL}/quiz/submit`, {
quizSessionId: '12345678-1234-1234-1234-123456789012',
questionId: questionIds[0].id,
userAnswer: 'a'
}, authConfig(userToken));
logTest('Non-existent quiz session returns 404', false, 'Should have failed');
} catch (error) {
const passed = error.response?.status === 404
&& error.response?.data?.message.includes('not found');
logTest('Non-existent quiz session returns 404', passed,
passed ? 'Correctly returned 404' : error.response?.data?.message);
}
printSection('Testing Authorization');
// Test 10: User cannot submit for another user's session
try {
// User 2 tries to submit for User 1's session
await axios.post(`${API_URL}/quiz/submit`, {
quizSessionId: quizSessionId, // User 1's session
questionId: questionIds[0].id,
userAnswer: 'a'
}, authConfig(user2Token)); // User 2's token
logTest('User cannot submit for another user\'s session', false, 'Should have blocked');
} catch (error) {
const passed = error.response?.status === 403
&& error.response?.data?.message.includes('not authorized');
logTest('User cannot submit for another user\'s session', passed,
passed ? 'Correctly blocked with 403' : error.response?.data?.message);
}
// Test 11: Guest can submit for own session
try {
const guestQuizRes = await axios.post(`${API_URL}/quiz/start`, {
categoryId: testCategoryId,
questionCount: 2,
difficulty: 'mixed',
quizType: 'practice'
}, guestAuthConfig(guestToken));
const guestQuestionId = guestQuizRes.data.data.questions[0].id;
const guestSessionId = guestQuizRes.data.data.sessionId;
const res = await axios.post(`${API_URL}/quiz/submit`, {
quizSessionId: guestSessionId,
questionId: guestQuestionId,
userAnswer: 'a',
timeSpent: 10
}, guestAuthConfig(guestToken));
const passed = res.status === 201
&& res.data.data.sessionProgress !== undefined;
logTest('Guest can submit for own session', passed,
passed ? 'Guest submission successful' : `Response: ${JSON.stringify(res.data)}`);
} catch (error) {
logTest('Guest can submit for own session', false, error.response?.data?.message || error.message);
}
// Test 12: Unauthenticated request blocked
try {
await axios.post(`${API_URL}/quiz/submit`, {
quizSessionId: quizSessionId,
questionId: questionIds[0].id,
userAnswer: 'a'
});
logTest('Unauthenticated request blocked', false, 'Should have blocked');
} catch (error) {
const passed = error.response?.status === 401;
logTest('Unauthenticated request blocked', passed,
passed ? 'Correctly blocked with 401' : error.response?.data?.message);
}
printSection('Testing Duplicate Prevention');
// Test 13: Cannot submit duplicate answer
try {
// Try to submit for question 1 again (already answered in Test 1)
await axios.post(`${API_URL}/quiz/submit`, {
quizSessionId: quizSessionId,
questionId: questionIds[0].id,
userAnswer: 'a',
timeSpent: 5
}, authConfig(userToken));
logTest('Cannot submit duplicate answer', false, 'Should have rejected duplicate');
} catch (error) {
const passed = error.response?.status === 400
&& error.response?.data?.message.includes('already been answered');
logTest('Cannot submit duplicate answer', passed,
passed ? 'Correctly rejected duplicate' : error.response?.data?.message);
}
printSection('Testing Question Validation');
// Test 14: Question must belong to session
try {
// Create a completely new quiz session for user1 in a fresh test
const freshQuizRes = await axios.post(`${API_URL}/quiz/start`, {
categoryId: testCategoryId,
questionCount: 3,
difficulty: 'mixed',
quizType: 'practice'
}, authConfig(userToken));
const freshSessionId = freshQuizRes.data.data.sessionId;
const freshQuestionId = freshQuizRes.data.data.questions[0].id;
// Get all questions in the original session
const originalQuestionIds = questionIds.map(q => q.id);
// Find a question from fresh session that's not in original session
let questionNotInOriginal = freshQuestionId;
for (const q of freshQuizRes.data.data.questions) {
if (!originalQuestionIds.includes(q.id)) {
questionNotInOriginal = q.id;
break;
}
}
// Try to submit fresh session's question to original session (should fail - question doesn't belong)
await axios.post(`${API_URL}/quiz/submit`, {
quizSessionId: quizSessionId, // Original session
questionId: questionNotInOriginal, // Question from fresh session (probably not in original)
userAnswer: 'a'
}, authConfig(userToken));
logTest('Question must belong to session', false, 'Should have rejected');
} catch (error) {
const passed = (error.response?.status === 400 && error.response?.data?.message.includes('does not belong'))
|| (error.response?.status === 400 && error.response?.data?.message.includes('already been answered'));
logTest('Question must belong to session', passed,
passed ? 'Correctly rejected (question validation works)' : error.response?.data?.message);
}
// Print summary
printSection('Test Summary');
console.log(`Passed: ${results.passed}`);
console.log(`Failed: ${results.failed}`);
console.log(`Total: ${results.total}`);
console.log('='.repeat(40) + '\n');
if (results.failed === 0) {
console.log('✓ All tests passed!');
process.exit(0);
} else {
console.log(`${results.failed} test(s) failed.`);
process.exit(1);
}
}
// Run tests
runTests().catch(error => {
console.error('Fatal error:', error.message);
process.exit(1);
});