diff --git a/frontend/cypress/e2e/endpoint-parity.cy.js b/frontend/cypress/e2e/endpoint-parity.cy.js new file mode 100644 index 0000000..1d72471 --- /dev/null +++ b/frontend/cypress/e2e/endpoint-parity.cy.js @@ -0,0 +1,74 @@ +// Verify that Vue frontend pages call the correct backend API endpoints. +// This test catches mismatched endpoint names (e.g. /api/bugs vs /api/bug-reports). + +const ADMIN_TOKEN = 'c86ae7afbe10fabe3c1d5e1a7fee74feaadfd5dc7be2ab62' + +describe('API Endpoint Parity', () => { + function visitAsAdmin(path) { + cy.visit(path, { + onBeforeLoad(win) { + win.localStorage.setItem('oil_auth_token', ADMIN_TOKEN) + } + }) + } + + it('search page loads recipes from /api/recipes', () => { + cy.intercept('GET', '/api/recipes').as('recipes') + visitAsAdmin('/') + cy.wait('@recipes').its('response.statusCode').should('eq', 200) + }) + + it('search page loads oils from /api/oils', () => { + cy.intercept('GET', '/api/oils').as('oils') + visitAsAdmin('/') + cy.wait('@oils').its('response.statusCode').should('eq', 200) + }) + + it('oil reference page loads oils', () => { + cy.intercept('GET', '/api/oils').as('oils') + visitAsAdmin('/oils') + cy.wait('@oils').its('response.statusCode').should('eq', 200) + }) + + it('audit log page loads from /api/audit-log', () => { + cy.intercept('GET', '/api/audit-log*').as('audit') + visitAsAdmin('/audit') + cy.wait('@audit').its('response.statusCode').should('eq', 200) + }) + + it('audit log page does NOT call /api/audit-logs (wrong endpoint)', () => { + cy.intercept('GET', '/api/audit-logs*').as('wrongAudit') + visitAsAdmin('/audit') + cy.wait(2000) + cy.get('@wrongAudit.all').should('have.length', 0) + }) + + it('bug tracker page loads from /api/bug-reports', () => { + cy.intercept('GET', '/api/bug-reports').as('bugs') + visitAsAdmin('/bugs') + cy.wait('@bugs').its('response.statusCode').should('eq', 200) + }) + + it('bug tracker page does NOT call /api/bugs (wrong endpoint)', () => { + cy.intercept('GET', '/api/bugs').as('wrongBugs') + visitAsAdmin('/bugs') + cy.wait(2000) + cy.get('@wrongBugs.all').should('have.length', 0) + }) + + it('user management page loads from /api/users', () => { + cy.intercept('GET', '/api/users').as('users') + visitAsAdmin('/users') + cy.wait('@users').its('response.statusCode').should('eq', 200) + }) + + it('categories load from /api/categories', () => { + cy.intercept('GET', '/api/categories').as('cats') + visitAsAdmin('/') + cy.wait(3000) + // Categories may or may not be fetched depending on page logic + // Just verify no /api/category-modules calls + cy.intercept('GET', '/api/category-modules').as('wrongCats') + cy.get('@wrongCats.all').should('have.length', 0) + }) +}) diff --git a/frontend/cypress/support/e2e.js b/frontend/cypress/support/e2e.js index 0f7ee48..5988293 100644 --- a/frontend/cypress/support/e2e.js +++ b/frontend/cypress/support/e2e.js @@ -1,5 +1,11 @@ -// Ignore uncaught exceptions from the app (API errors during loading, etc.) -Cypress.on('uncaught:exception', () => false) +// Log uncaught exceptions but don't swallow them blindly. +// Only ignore known non-critical errors (e.g. ResizeObserver). +Cypress.on('uncaught:exception', (err) => { + // ResizeObserver loop errors are harmless + if (err.message.includes('ResizeObserver')) return false + // Let all other errors fail the test + return true +}) // Custom commands for the oil calculator app diff --git a/frontend/src/views/AuditLog.vue b/frontend/src/views/AuditLog.vue index e4b7a6d..7ccc8d0 100644 --- a/frontend/src/views/AuditLog.vue +++ b/frontend/src/views/AuditLog.vue @@ -154,7 +154,7 @@ function formatDetail(log) { async function fetchLogs() { loading.value = true try { - const res = await api(`/api/audit-logs?offset=${page.value * pageSize}&limit=${pageSize}`) + const res = await api(`/api/audit-log?offset=${page.value * pageSize}&limit=${pageSize}`) if (res.ok) { const data = await res.json() const items = Array.isArray(data) ? data : data.logs || data.items || [] @@ -179,7 +179,7 @@ async function undoLog(log) { if (!ok) return try { const id = log._id || log.id - const res = await api(`/api/audit-logs/${id}/undo`, { method: 'POST' }) + const res = await api(`/api/audit-log/${id}/undo`, { method: 'POST' }) if (res.ok) { ui.showToast('已撤销') // Refresh diff --git a/frontend/src/views/BugTracker.vue b/frontend/src/views/BugTracker.vue index 74050f4..37050ff 100644 --- a/frontend/src/views/BugTracker.vue +++ b/frontend/src/views/BugTracker.vue @@ -188,7 +188,7 @@ function toggleComments(bug) { async function loadBugs() { try { - const res = await api('/api/bugs') + const res = await api('/api/bug-reports') if (res.ok) { bugs.value = await res.json() } @@ -200,14 +200,11 @@ async function loadBugs() { async function createBug() { if (!bugForm.title.trim()) return try { - const res = await api('/api/bugs', { + const res = await api('/api/bug-report', { method: 'POST', body: JSON.stringify({ - title: bugForm.title.trim(), - description: bugForm.description.trim(), - priority: bugForm.priority, - status: 'open', - reporter: auth.user.display_name || auth.user.username, + content: bugForm.title.trim() + (bugForm.description.trim() ? '\n' + bugForm.description.trim() : ''), + priority: bugForm.priority === 'urgent' ? 0 : bugForm.priority === 'high' ? 1 : 2, }), }) if (res.ok) { @@ -226,7 +223,7 @@ async function createBug() { async function updateStatus(bug, newStatus) { const id = bug._id || bug.id try { - const res = await api(`/api/bugs/${id}`, { + const res = await api(`/api/bug-reports/${id}`, { method: 'PUT', body: JSON.stringify({ status: newStatus }), }) @@ -244,7 +241,7 @@ async function removeBug(bug) { if (!ok) return const id = bug._id || bug.id try { - const res = await api(`/api/bugs/${id}`, { method: 'DELETE' }) + const res = await api(`/api/bug-reports/${id}`, { method: 'DELETE' }) if (res.ok) { bugs.value = bugs.value.filter(b => (b._id || b.id) !== id) ui.showToast('已删除') @@ -258,7 +255,7 @@ async function addComment(bug) { if (!newComment.value.trim()) return const id = bug._id || bug.id try { - const res = await api(`/api/bugs/${id}/comments`, { + const res = await api(`/api/bug-reports/${id}/comments`, { method: 'POST', body: JSON.stringify({ text: newComment.value.trim(), diff --git a/frontend/src/views/MyDiary.vue b/frontend/src/views/MyDiary.vue index f8ce034..e23291c 100644 --- a/frontend/src/views/MyDiary.vue +++ b/frontend/src/views/MyDiary.vue @@ -341,7 +341,7 @@ function formatDate(d) { // Brand settings async function loadBrandSettings() { try { - const res = await api('/api/brand-settings') + const res = await api('/api/brand') if (res.ok) { const data = await res.json() brandName.value = data.brand_name || '' @@ -356,7 +356,7 @@ async function loadBrandSettings() { async function saveBrandSettings() { try { - await api('/api/brand-settings', { + await api('/api/brand', { method: 'PUT', body: JSON.stringify({ brand_name: brandName.value, @@ -400,7 +400,7 @@ async function handleUpload(type, event) { // Account async function updateDisplayName() { try { - await api('/api/me/display-name', { + await api('/api/me', { method: 'PUT', body: JSON.stringify({ display_name: displayName.value }), }) diff --git a/frontend/src/views/RecipeSearch.vue b/frontend/src/views/RecipeSearch.vue index 8ab0976..0cfffa5 100644 --- a/frontend/src/views/RecipeSearch.vue +++ b/frontend/src/views/RecipeSearch.vue @@ -131,7 +131,7 @@ const catTrack = ref(null) onMounted(async () => { try { - const res = await api('/api/category-modules') + const res = await api('/api/categories') if (res.ok) { categories.value = await res.json() }