667 lines
24 KiB
JavaScript
667 lines
24 KiB
JavaScript
let authToken = null;
|
|
let householdId = null;
|
|
let storeId = null;
|
|
let testUserId = null;
|
|
let createdHouseholdId = null;
|
|
let secondHouseholdId = null;
|
|
let inviteCode = null;
|
|
|
|
const tests = [
|
|
{
|
|
category: "Authentication",
|
|
tests: [
|
|
{
|
|
name: "Login with valid credentials",
|
|
method: "POST",
|
|
endpoint: "/auth/login",
|
|
auth: false,
|
|
body: () => ({ username: document.getElementById('username').value, password: document.getElementById('password').value }),
|
|
expect: (res) => res.token && res.role,
|
|
expectedFields: ['token', 'username', 'role'],
|
|
onSuccess: (res) => { authToken = res.token; }
|
|
},
|
|
{
|
|
name: "Login with invalid credentials",
|
|
method: "POST",
|
|
endpoint: "/auth/login",
|
|
auth: false,
|
|
body: { username: "wronguser", password: "wrongpass" },
|
|
expectFail: true,
|
|
expect: (res, status) => status === 401,
|
|
expectedFields: ['message']
|
|
},
|
|
{
|
|
name: "Access protected route without token",
|
|
method: "GET",
|
|
endpoint: "/households",
|
|
auth: false,
|
|
expectFail: true,
|
|
expect: (res, status) => status === 401
|
|
}
|
|
]
|
|
},
|
|
{
|
|
category: "Households",
|
|
tests: [
|
|
{
|
|
name: "Get user's households",
|
|
method: "GET",
|
|
endpoint: "/households",
|
|
auth: true,
|
|
expect: (res) => Array.isArray(res),
|
|
onSuccess: (res) => { if (res.length > 0) householdId = res[0].id; }
|
|
},
|
|
{
|
|
name: "Create new household",
|
|
method: "POST",
|
|
endpoint: "/households",
|
|
auth: true,
|
|
body: { name: `Test Household ${Date.now()}` },
|
|
expect: (res) => res.household && res.household.invite_code,
|
|
expectedFields: ['message', 'household', 'household.id', 'household.name', 'household.invite_code']
|
|
},
|
|
{
|
|
name: "Get household details",
|
|
method: "GET",
|
|
endpoint: () => `/households/${householdId}`,
|
|
auth: true,
|
|
skip: () => !householdId,
|
|
expect: (res) => res.id === householdId,
|
|
expectedFields: ['id', 'name', 'invite_code', 'created_at']
|
|
},
|
|
{
|
|
name: "Update household name",
|
|
method: "PATCH",
|
|
endpoint: () => `/households/${householdId}`,
|
|
auth: true,
|
|
skip: () => !householdId,
|
|
body: { name: `Updated Household ${Date.now()}` },
|
|
expect: (res) => res.household,
|
|
expectedFields: ['message', 'household', 'household.id', 'household.name']
|
|
},
|
|
{
|
|
name: "Refresh invite code",
|
|
method: "POST",
|
|
endpoint: () => `/households/${householdId}/invite/refresh`,
|
|
auth: true,
|
|
skip: () => !householdId,
|
|
expect: (res) => res.household && res.household.invite_code,
|
|
expectedFields: ['message', 'household', 'household.invite_code']
|
|
},
|
|
{
|
|
name: "Join household with invalid code",
|
|
method: "POST",
|
|
endpoint: "/households/join/INVALID123",
|
|
auth: true,
|
|
expectFail: true,
|
|
expect: (res, status) => status === 404
|
|
},
|
|
{
|
|
name: "Create household with empty name (validation)",
|
|
method: "POST",
|
|
endpoint: "/households",
|
|
auth: true,
|
|
body: { name: "" },
|
|
expectFail: true,
|
|
expect: (res, status) => status === 400,
|
|
expectedFields: ['error']
|
|
}
|
|
]
|
|
},
|
|
{
|
|
category: "Members",
|
|
tests: [
|
|
{
|
|
name: "Get household members",
|
|
method: "GET",
|
|
endpoint: () => `/households/${householdId}/members`,
|
|
auth: true,
|
|
skip: () => !householdId,
|
|
expect: (res) => Array.isArray(res) && res.length > 0,
|
|
onSuccess: (res) => { testUserId = res[0].user_id; }
|
|
},
|
|
{
|
|
name: "Update member role (non-admin attempting)",
|
|
method: "PATCH",
|
|
endpoint: () => `/households/${householdId}/members/${testUserId}/role`,
|
|
auth: true,
|
|
skip: () => !householdId || !testUserId,
|
|
body: { role: "user" },
|
|
expectFail: true,
|
|
expect: (res, status) => status === 400 || status === 403
|
|
}
|
|
]
|
|
},
|
|
{
|
|
category: "Stores",
|
|
tests: [
|
|
{
|
|
name: "Get all stores catalog",
|
|
method: "GET",
|
|
endpoint: "/stores",
|
|
auth: true,
|
|
expect: (res) => Array.isArray(res),
|
|
onSuccess: (res) => { if (res.length > 0) storeId = res[0].id; }
|
|
},
|
|
{
|
|
name: "Get household stores",
|
|
method: "GET",
|
|
endpoint: () => `/stores/household/${householdId}`,
|
|
auth: true,
|
|
skip: () => !householdId,
|
|
expect: (res) => Array.isArray(res)
|
|
},
|
|
{
|
|
name: "Add store to household",
|
|
method: "POST",
|
|
endpoint: () => `/stores/household/${householdId}`,
|
|
auth: true,
|
|
skip: () => !householdId || !storeId,
|
|
body: () => ({ storeId: storeId, isDefault: true }),
|
|
expect: (res) => res.store,
|
|
expectedFields: ['message', 'store', 'store.id', 'store.name']
|
|
},
|
|
{
|
|
name: "Set default store",
|
|
method: "PATCH",
|
|
endpoint: () => `/stores/household/${householdId}/${storeId}/default`,
|
|
auth: true,
|
|
skip: () => !householdId || !storeId,
|
|
expect: (res) => res.message
|
|
},
|
|
{
|
|
name: "Add invalid store to household",
|
|
method: "POST",
|
|
endpoint: () => `/stores/household/${householdId}`,
|
|
auth: true,
|
|
skip: () => !householdId,
|
|
body: { storeId: 99999 },
|
|
expectFail: true,
|
|
expect: (res, status) => status === 404
|
|
}
|
|
]
|
|
},
|
|
{
|
|
category: "Advanced Household Tests",
|
|
tests: [
|
|
{
|
|
name: "Create household for complex workflows",
|
|
method: "POST",
|
|
endpoint: "/households",
|
|
auth: true,
|
|
body: { name: `Workflow Test ${Date.now()}` },
|
|
expect: (res) => res.household && res.household.id,
|
|
onSuccess: (res) => {
|
|
createdHouseholdId = res.household.id;
|
|
inviteCode = res.household.invite_code;
|
|
}
|
|
},
|
|
{
|
|
name: "Verify invite code format (7 chars)",
|
|
method: "GET",
|
|
endpoint: () => `/households/${createdHouseholdId}`,
|
|
auth: true,
|
|
skip: () => !createdHouseholdId,
|
|
expect: (res) => res.invite_code && res.invite_code.length === 7 && res.invite_code.startsWith('H')
|
|
},
|
|
{
|
|
name: "Get household with no stores added yet",
|
|
method: "GET",
|
|
endpoint: () => `/stores/household/${createdHouseholdId}`,
|
|
auth: true,
|
|
skip: () => !createdHouseholdId,
|
|
expect: (res) => Array.isArray(res) && res.length === 0
|
|
},
|
|
{
|
|
name: "Update household with very long name (validation)",
|
|
method: "PATCH",
|
|
endpoint: () => `/households/${createdHouseholdId}`,
|
|
auth: true,
|
|
skip: () => !createdHouseholdId,
|
|
body: { name: "A".repeat(101) },
|
|
expectFail: true,
|
|
expect: (res, status) => status === 400
|
|
},
|
|
{
|
|
name: "Refresh invite code changes value",
|
|
method: "POST",
|
|
endpoint: () => `/households/${createdHouseholdId}/invite/refresh`,
|
|
auth: true,
|
|
skip: () => !createdHouseholdId || !inviteCode,
|
|
expect: (res) => res.household && res.household.invite_code !== inviteCode,
|
|
onSuccess: (res) => { inviteCode = res.household.invite_code; }
|
|
},
|
|
{
|
|
name: "Join same household twice (idempotent)",
|
|
method: "POST",
|
|
endpoint: () => `/households/join/${inviteCode}`,
|
|
auth: true,
|
|
skip: () => !inviteCode,
|
|
expect: (res, status) => status === 200 && res.message.includes("already a member")
|
|
},
|
|
{
|
|
name: "Get non-existent household",
|
|
method: "GET",
|
|
endpoint: "/households/99999",
|
|
auth: true,
|
|
expectFail: true,
|
|
expect: (res, status) => status === 404
|
|
},
|
|
{
|
|
name: "Update non-existent household",
|
|
method: "PATCH",
|
|
endpoint: "/households/99999",
|
|
auth: true,
|
|
body: { name: "Test" },
|
|
expectFail: true,
|
|
expect: (res, status) => status === 403 || status === 404
|
|
}
|
|
]
|
|
},
|
|
{
|
|
category: "Member Management Edge Cases",
|
|
tests: [
|
|
{
|
|
name: "Get members for created household",
|
|
method: "GET",
|
|
endpoint: () => `/households/${createdHouseholdId}/members`,
|
|
auth: true,
|
|
skip: () => !createdHouseholdId,
|
|
expect: (res) => Array.isArray(res) && res.length >= 1 && res[0].role === 'admin'
|
|
},
|
|
{
|
|
name: "Update own role (should fail)",
|
|
method: "PATCH",
|
|
endpoint: () => `/households/${createdHouseholdId}/members/${testUserId}/role`,
|
|
auth: true,
|
|
skip: () => !createdHouseholdId || !testUserId,
|
|
body: { role: "user" },
|
|
expectFail: true,
|
|
expect: (res, status) => status === 400 && res.error && res.error.includes("own role")
|
|
},
|
|
{
|
|
name: "Update role with invalid value",
|
|
method: "PATCH",
|
|
endpoint: () => `/households/${createdHouseholdId}/members/1/role`,
|
|
auth: true,
|
|
skip: () => !createdHouseholdId,
|
|
body: { role: "superadmin" },
|
|
expectFail: true,
|
|
expect: (res, status) => status === 400
|
|
},
|
|
{
|
|
name: "Remove non-existent member",
|
|
method: "DELETE",
|
|
endpoint: () => `/households/${createdHouseholdId}/members/99999`,
|
|
auth: true,
|
|
skip: () => !createdHouseholdId,
|
|
expectFail: true,
|
|
expect: (res, status) => status === 404 || status === 500
|
|
}
|
|
]
|
|
},
|
|
{
|
|
category: "Store Management Advanced",
|
|
tests: [
|
|
{
|
|
name: "Add multiple stores to household",
|
|
method: "POST",
|
|
endpoint: () => `/stores/household/${createdHouseholdId}`,
|
|
auth: true,
|
|
skip: () => !createdHouseholdId || !storeId,
|
|
body: () => ({ storeId: storeId, isDefault: false }),
|
|
expect: (res) => res.store
|
|
},
|
|
{
|
|
name: "Add same store twice (duplicate check)",
|
|
method: "POST",
|
|
endpoint: () => `/stores/household/${createdHouseholdId}`,
|
|
auth: true,
|
|
skip: () => !createdHouseholdId || !storeId,
|
|
body: () => ({ storeId: storeId, isDefault: false }),
|
|
expectFail: true,
|
|
expect: (res, status) => status === 400 || status === 409 || status === 500
|
|
},
|
|
{
|
|
name: "Set default store for household",
|
|
method: "PATCH",
|
|
endpoint: () => `/stores/household/${createdHouseholdId}/${storeId}/default`,
|
|
auth: true,
|
|
skip: () => !createdHouseholdId || !storeId,
|
|
expect: (res) => res.message
|
|
},
|
|
{
|
|
name: "Verify default store is first in list",
|
|
method: "GET",
|
|
endpoint: () => `/stores/household/${createdHouseholdId}`,
|
|
auth: true,
|
|
skip: () => !createdHouseholdId || !storeId,
|
|
expect: (res) => Array.isArray(res) && res.length > 0 && res[0].is_default === true
|
|
},
|
|
{
|
|
name: "Set non-existent store as default",
|
|
method: "PATCH",
|
|
endpoint: () => `/stores/household/${createdHouseholdId}/99999/default`,
|
|
auth: true,
|
|
skip: () => !createdHouseholdId,
|
|
expectFail: true,
|
|
expect: (res, status) => status === 404 || status === 500
|
|
},
|
|
{
|
|
name: "Remove store from household",
|
|
method: "DELETE",
|
|
endpoint: () => `/stores/household/${createdHouseholdId}/${storeId}`,
|
|
auth: true,
|
|
skip: () => !createdHouseholdId || !storeId,
|
|
expect: (res) => res.message
|
|
},
|
|
{
|
|
name: "Verify store removed from household",
|
|
method: "GET",
|
|
endpoint: () => `/stores/household/${createdHouseholdId}`,
|
|
auth: true,
|
|
skip: () => !createdHouseholdId,
|
|
expect: (res) => Array.isArray(res) && res.length === 0
|
|
}
|
|
]
|
|
},
|
|
{
|
|
category: "Data Integrity & Cleanup",
|
|
tests: [
|
|
{
|
|
name: "Create second household for testing",
|
|
method: "POST",
|
|
endpoint: "/households",
|
|
auth: true,
|
|
body: { name: `Second Test ${Date.now()}` },
|
|
expect: (res) => res.household && res.household.id,
|
|
onSuccess: (res) => { secondHouseholdId = res.household.id; }
|
|
},
|
|
{
|
|
name: "Verify user belongs to multiple households",
|
|
method: "GET",
|
|
endpoint: "/households",
|
|
auth: true,
|
|
expect: (res) => Array.isArray(res) && res.length >= 3
|
|
},
|
|
{
|
|
name: "Delete created test household",
|
|
method: "DELETE",
|
|
endpoint: () => `/households/${createdHouseholdId}`,
|
|
auth: true,
|
|
skip: () => !createdHouseholdId,
|
|
expect: (res) => res.message
|
|
},
|
|
{
|
|
name: "Verify deleted household is gone",
|
|
method: "GET",
|
|
endpoint: () => `/households/${createdHouseholdId}`,
|
|
auth: true,
|
|
skip: () => !createdHouseholdId,
|
|
expectFail: true,
|
|
expect: (res, status) => status === 404 || status === 403
|
|
},
|
|
{
|
|
name: "Delete second test household",
|
|
method: "DELETE",
|
|
endpoint: () => `/households/${secondHouseholdId}`,
|
|
auth: true,
|
|
skip: () => !secondHouseholdId,
|
|
expect: (res) => res.message
|
|
},
|
|
{
|
|
name: "Verify households list updated",
|
|
method: "GET",
|
|
endpoint: "/households",
|
|
auth: true,
|
|
expect: (res) => Array.isArray(res)
|
|
}
|
|
]
|
|
}
|
|
];
|
|
|
|
async function makeRequest(test) {
|
|
const apiUrl = document.getElementById('apiUrl').value;
|
|
const endpoint = typeof test.endpoint === 'function' ? test.endpoint() : test.endpoint;
|
|
const url = `${apiUrl}${endpoint}`;
|
|
|
|
const options = {
|
|
method: test.method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
}
|
|
};
|
|
|
|
if (test.auth && authToken) {
|
|
options.headers['Authorization'] = `Bearer ${authToken}`;
|
|
}
|
|
|
|
if (test.body) {
|
|
options.body = JSON.stringify(typeof test.body === 'function' ? test.body() : test.body);
|
|
}
|
|
|
|
const response = await fetch(url, options);
|
|
const data = await response.json().catch(() => ({}));
|
|
|
|
return { data, status: response.status };
|
|
}
|
|
|
|
async function runTest(categoryIdx, testIdx) {
|
|
const test = tests[categoryIdx].tests[testIdx];
|
|
const testId = `test-${categoryIdx}-${testIdx}`;
|
|
const testEl = document.getElementById(testId);
|
|
const contentEl = document.getElementById(`${testId}-content`);
|
|
const toggleEl = document.getElementById(`${testId}-toggle`);
|
|
const resultEl = testEl.querySelector('.test-result');
|
|
|
|
// Auto-expand when running
|
|
contentEl.classList.add('expanded');
|
|
toggleEl.classList.add('expanded');
|
|
resultEl.style.display = 'block';
|
|
resultEl.className = 'test-result';
|
|
resultEl.innerHTML = '⚠️ Prerequisites not met';
|
|
return 'skip';
|
|
}
|
|
|
|
testEl.className = 'test-case running';
|
|
testEl.querySelector('.test-status').textContent = 'RUNNING';
|
|
testEl.querySelector('.test-status').className = 'test-status running';
|
|
resultEl.style.display = 'none';
|
|
|
|
try {
|
|
const { data, status } = await makeRequest(test);
|
|
|
|
const expectFail = test.expectFail || false;
|
|
const passed = test.expect(data, status);
|
|
|
|
const success = expectFail ? !passed || status >= 400 : passed;
|
|
|
|
testEl.className = success ? 'test-case pass' : 'test-case fail';
|
|
testEl.querySelector('.test-status').textContent = success ? 'PASS' : 'FAIL';
|
|
testEl.querySelector('.test-status').className = `test-status ${success ? 'pass' : 'fail'}`;
|
|
|
|
// Determine status code class
|
|
let statusClass = 'status-5xx';
|
|
if (status >= 200 && status < 300) statusClass = 'status-2xx';
|
|
else if (status >= 300 && status < 400) statusClass = 'status-3xx';
|
|
else if (status >= 400 && status < 500) statusClass = 'status-4xx';
|
|
|
|
resultEl.style.display = 'block';
|
|
resultEl.className = 'test-result';
|
|
|
|
// Check expected fields if defined
|
|
let expectedFieldsHTML = '';
|
|
if (test.expectedFields) {
|
|
const fieldChecks = test.expectedFields.map(field => {
|
|
const exists = field.split('.').reduce((obj, key) => obj?.[key], data) !== undefined;
|
|
const icon = exists ? '✓' : '✗';
|
|
const className = exists ? 'pass' : 'fail';
|
|
return `<div class="field-check ${className}">${icon} ${field}</div>`;
|
|
}).join('');
|
|
|
|
expectedFieldsHTML = `
|
|
<div class="expected-section">
|
|
<div class="expected-label">Expected Fields:</div>
|
|
${fieldChecks}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
resultEl.innerHTML = `
|
|
<div style="margin-bottom: 8px;">
|
|
<span class="response-status ${statusClass}">HTTP ${status}</span>
|
|
<span style="color: #666;">${success ? '✓ Test passed' : '✗ Test failed'}</span>
|
|
</div>
|
|
${expectedFieldsHTML}
|
|
<div style="color: #666; font-size: 12px; margin-bottom: 4px;">Response:</div>
|
|
<div>${JSON.stringify(data, null, 2)}</div>
|
|
`;
|
|
|
|
if (success && test.onSuccess) {
|
|
test.onSuccess(data);
|
|
}
|
|
|
|
return success ? 'pass' : 'fail';
|
|
} catch (error) {
|
|
testEl.className = 'test-case fail';
|
|
testEl.querySelector('.test-status').textContent = 'ERROR';
|
|
testEl.querySelector('.test-status').className = 'test-status fail';
|
|
|
|
resultEl.style.display = 'block';
|
|
resultEl.className = 'test-error';
|
|
resultEl.innerHTML = `
|
|
<div style="font-weight: bold; margin-bottom: 8px;">❌ Network/Request Error</div>
|
|
<div>${error.message}</div>
|
|
${error.stack ? `<div style="margin-top: 8px; font-size: 11px; opacity: 0.7;">${error.stack}</div>` : ''}
|
|
`;
|
|
return 'fail';
|
|
}
|
|
}
|
|
|
|
async function runAllTests(event) {
|
|
authToken = null;
|
|
householdId = null;
|
|
storeId = null;
|
|
testUserId = null;
|
|
createdHouseholdId = null;
|
|
secondHouseholdId = null;
|
|
inviteCode = null;
|
|
|
|
const button = event.target;
|
|
button.disabled = true;
|
|
button.textContent = '⏳ Running Tests...';
|
|
|
|
let totalTests = 0;
|
|
let passedTests = 0;
|
|
let failedTests = 0;
|
|
|
|
for (let i = 0; i < tests.length; i++) {
|
|
for (let j = 0; j < tests[i].tests.length; j++) {
|
|
const result = await runTest(i, j);
|
|
if (result !== 'skip') {
|
|
totalTests++;
|
|
if (result === 'pass') passedTests++;
|
|
if (result === 'fail') failedTests++;
|
|
}
|
|
}
|
|
}
|
|
|
|
document.getElementById('summary').style.display = 'flex';
|
|
document.getElementById('totalTests').textContent = totalTests;
|
|
document.getElementById('passedTests').textContent = passedTests;
|
|
document.getElementById('failedTests').textContent = failedTests;
|
|
|
|
button.disabled = false;
|
|
button.textContent = '▶ Run All Tests';
|
|
}
|
|
|
|
function toggleTest(testId) {
|
|
const content = document.getElementById(`${testId}-content`);
|
|
const toggle = document.getElementById(`${testId}-toggle`);
|
|
|
|
if (content.classList.contains('expanded')) {
|
|
content.classList.remove('expanded');
|
|
toggle.classList.remove('expanded');
|
|
} else {
|
|
content.classList.add('expanded');
|
|
toggle.classList.add('expanded');
|
|
}
|
|
}
|
|
|
|
function expandAllTests() {
|
|
document.querySelectorAll('.test-content').forEach(content => {
|
|
content.classList.add('expanded');
|
|
});
|
|
document.querySelectorAll('.toggle-icon').forEach(icon => {
|
|
icon.classList.add('expanded');
|
|
});
|
|
}
|
|
|
|
function collapseAllTests() {
|
|
document.querySelectorAll('.test-content').forEach(content => {
|
|
content.classList.remove('expanded');
|
|
});
|
|
document.querySelectorAll('.toggle-icon').forEach(icon => {
|
|
icon.classList.remove('expanded');
|
|
});
|
|
}
|
|
|
|
function clearResults() {
|
|
renderTests();
|
|
document.getElementById('summary').style.display = 'none';
|
|
authToken = null;
|
|
householdId = null;
|
|
storeId = null;
|
|
testUserId = null;
|
|
createdHouseholdId = null;
|
|
secondHouseholdId = null;
|
|
inviteCode = null;
|
|
}
|
|
|
|
function renderTests() {
|
|
const container = document.getElementById('testResults');
|
|
container.innerHTML = '';
|
|
|
|
tests.forEach((category, catIdx) => {
|
|
const categoryDiv = document.createElement('div');
|
|
categoryDiv.className = 'test-category';
|
|
|
|
const categoryHeader = document.createElement('h2');
|
|
categoryHeader.textContent = category.category;
|
|
categoryDiv.appendChild(categoryHeader);
|
|
|
|
category.tests.forEach((test, testIdx) => {
|
|
const testDiv = document.createElement('div');
|
|
testDiv.className = 'test-case';
|
|
testDiv.id = `test-${catIdx}-${testIdx}`;
|
|
|
|
const endpoint = typeof test.endpoint === 'function' ? test.endpoint() : test.endpoint;
|
|
|
|
testDiv.innerHTML = `
|
|
<div class="test-header" onclick="toggleTest('${testDiv.id}')">
|
|
<div class="test-name">
|
|
<span class="toggle-icon" id="${testDiv.id}-toggle">▶</span>
|
|
${test.name}
|
|
</div>
|
|
<div class="test-status pending">PENDING</div>
|
|
</div>
|
|
<div class="test-content" id="${testDiv.id}-content">
|
|
<div class="test-details">
|
|
<strong>${test.method}</strong> ${endpoint}
|
|
${test.expectFail ? ' <span style="color: #dc3545; font-weight: 600;">(Expected to fail)</span>' : ''}
|
|
${test.auth ? ' <span style="color: #0066cc; font-weight: 600;">🔒 Requires Auth</span>' : ''}
|
|
</div>
|
|
<div class="test-result" style="display: none;"></div>
|
|
</div>
|
|
`;
|
|
|
|
categoryDiv.appendChild(testDiv);
|
|
});
|
|
|
|
container.appendChild(categoryDiv);
|
|
});
|
|
}
|
|
|
|
// Initialize
|
|
renderTests();
|