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 `