485 lines
17 KiB
JavaScript
485 lines
17 KiB
JavaScript
/**
|
|
* 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);
|
|
});
|