From 9974fca2cba30d0d5959da960c832cfb66c7d38d Mon Sep 17 00:00:00 2001 From: curo1305 Date: Mon, 22 Jun 2026 08:45:50 +0200 Subject: [PATCH] test(12.1-03): add RED tests for server freshness mapping - cloudConnections: setBrowseState warning/refreshing/fresh states - cloudConnections: cached items remain during warning - cloudConnections: selectConnection clears browse state - cloudConnections: does_not_use_browser_clock_as_refresh_evidence - CloudFolderView: maps_server_warning_and_last_success - CloudFolderView: http_200_does_not_imply_fresh - CloudFolderView: does_not_use_browser_clock_as_refresh_evidence - CloudFolderView: cached_items_remain_visible_during_warning 3 new failures against unconditional fresh/new Date() behavior --- .../stores/__tests__/cloudConnections.test.js | 80 ++++++++++++ .../views/__tests__/CloudFolderView.test.js | 114 ++++++++++++++++++ 2 files changed, 194 insertions(+) diff --git a/frontend/src/stores/__tests__/cloudConnections.test.js b/frontend/src/stores/__tests__/cloudConnections.test.js index bac5f65..a71567d 100644 --- a/frontend/src/stores/__tests__/cloudConnections.test.js +++ b/frontend/src/stores/__tests__/cloudConnections.test.js @@ -156,3 +156,83 @@ describe('useCloudConnectionsStore', () => { expect(stored.startsWith('{')).toBe(false) }) }) + +// ── Freshness state mapping ────────────────────────────────────────────────── + +describe('setBrowseState freshness mapping', () => { + it('setBrowseState with freshness=warning sets folderFreshness to "warning"', () => { + const store = useCloudConnectionsStore() + store.setBrowseState({ freshness: 'warning' }) + expect(store.folderFreshness).toBe('warning') + }) + + it('setBrowseState with freshness=refreshing sets folderFreshness to "refreshing"', () => { + const store = useCloudConnectionsStore() + store.setBrowseState({ freshness: 'refreshing' }) + expect(store.folderFreshness).toBe('refreshing') + }) + + it('setBrowseState with freshness=fresh sets folderFreshness to "fresh"', () => { + const store = useCloudConnectionsStore() + store.setBrowseState({ freshness: 'fresh' }) + expect(store.folderFreshness).toBe('fresh') + }) + + it('maps_server_warning_and_last_success: server last_refreshed_at is preserved exactly', () => { + const store = useCloudConnectionsStore() + const serverTimestamp = '2026-06-15T10:30:00Z' + store.setBrowseState({ + freshness: 'warning', + refreshedAt: serverTimestamp, + }) + expect(store.folderFreshness).toBe('warning') + expect(store.lastRefreshedAt).toBe(serverTimestamp) + }) + + it('selectConnection clears cloud browse state without persisting secrets', () => { + const store = useCloudConnectionsStore() + store.connections = [{ id: 'conn-1', provider: 'google_drive' }] + // Set some browse state + store.setBrowseState({ + items: [{ id: 'item-1', name: 'doc.pdf' }], + caps: { share: true }, + freshness: 'fresh', + refreshedAt: '2026-06-15T10:30:00Z', + byteAvail: 'cached', + err: null, + }) + // Select a different connection — should clear state + store.selectConnection('conn-1') + expect(store.browseItems).toHaveLength(0) + expect(store.folderFreshness).toBeNull() + expect(store.lastRefreshedAt).toBeNull() + expect(store.capabilities).toBeNull() + expect(store.byteAvailability).toBeNull() + // No tokens or credentials should be present + expect(store.browseError).toBeNull() + }) + + it('does_not_use_browser_clock_as_refresh_evidence: refreshedAt from server, not Date.now', () => { + const store = useCloudConnectionsStore() + // Store only accepts what is passed explicitly — no implicit Date() + const SERVER_TS = '2026-06-10T08:00:00.000Z' + store.setBrowseState({ freshness: 'fresh', refreshedAt: SERVER_TS }) + // lastRefreshedAt must equal exactly what was passed — not a new Date() + expect(store.lastRefreshedAt).toBe(SERVER_TS) + }) + + it('cached_items_remain_visible_during_warning: items not cleared on warning freshness', () => { + const store = useCloudConnectionsStore() + const cachedItems = [ + { id: 'item-a', provider_item_id: 'pref-a', name: 'Cached Folder', kind: 'folder' }, + ] + // First, set fresh state with items + store.setBrowseState({ items: cachedItems, freshness: 'fresh', refreshedAt: '2026-06-01T00:00:00Z' }) + expect(store.browseItems).toHaveLength(1) + // Now simulate a warning without clearing items (only freshness changes) + store.setBrowseState({ freshness: 'warning' }) + // Items must still be there — warning does not delete cached items + expect(store.browseItems).toHaveLength(1) + expect(store.browseItems[0].id).toBe('item-a') + }) +}) diff --git a/frontend/src/views/__tests__/CloudFolderView.test.js b/frontend/src/views/__tests__/CloudFolderView.test.js index e8614d5..2f096f1 100644 --- a/frontend/src/views/__tests__/CloudFolderView.test.js +++ b/frontend/src/views/__tests__/CloudFolderView.test.js @@ -306,3 +306,117 @@ describe('opaque_reference_round_trip', () => { } }) }) + +// ── Freshness mapping: server state is authoritative ───────────────────────── + +describe('maps_server_refreshing_freshness', () => { + it('setBrowseState is called with refreshing before the API resolves', async () => { + // Make the API resolve async so we can check the in-flight state + let resolveApi + api.getCloudFoldersByConnectionId.mockReturnValue(new Promise(r => { resolveApi = r })) + const w = mount(CloudFolderView, { global: { stubs: globalStubs } }) + // Before resolution: refreshing must have been set + expect(mockSetBrowseState).toHaveBeenCalledWith(expect.objectContaining({ freshness: 'refreshing' })) + // Complete the request + resolveApi({ + items: [], + capabilities: null, + freshness: { refresh_state: 'fresh', last_refreshed_at: '2026-06-22T00:00:00Z' }, + }) + await flushPromises() + }) +}) + +describe('maps_server_warning_and_last_success', () => { + it('HTTP 200 with warning freshness calls setBrowseState with warning, not fresh', async () => { + const SERVER_TS = '2026-06-10T12:00:00.000Z' + api.getCloudFoldersByConnectionId.mockResolvedValue({ + items: [FOLDER_A], + capabilities: null, + freshness: { + refresh_state: 'warning', + last_refreshed_at: SERVER_TS, + error_code: 'incomplete_listing', + error_message: 'Provider returned incomplete results.', + }, + }) + const w = mount(CloudFolderView, { global: { stubs: globalStubs } }) + await flushPromises() + // setBrowseState must be called with freshness='warning', NOT 'fresh' + const calls = mockSetBrowseState.mock.calls + const finalCall = calls.find(c => c[0].freshness === 'warning') + expect(finalCall).toBeDefined() + // fresh must NOT be set after a warning response + const freshCall = calls.find(c => c[0].freshness === 'fresh') + expect(freshCall).toBeUndefined() + }) +}) + +describe('http_200_does_not_imply_fresh', () => { + it('HTTP 200 with warning state does not result in fresh freshness being set', async () => { + api.getCloudFoldersByConnectionId.mockResolvedValue({ + items: [], + capabilities: null, + freshness: { + refresh_state: 'warning', + last_refreshed_at: null, + error_code: 'incomplete_listing', + error_message: null, + }, + }) + const w = mount(CloudFolderView, { global: { stubs: globalStubs } }) + await flushPromises() + // None of the setBrowseState calls (excluding the initial 'refreshing') should set 'fresh' + const freshAfterLoad = mockSetBrowseState.mock.calls + .filter(c => c[0].freshness === 'fresh') + expect(freshAfterLoad.length).toBe(0) + }) +}) + +describe('does_not_use_browser_clock_as_refresh_evidence', () => { + it('refreshedAt passed to setBrowseState comes from server last_refreshed_at, not new Date()', async () => { + const SERVER_TS = '2026-06-01T08:00:00.000Z' + api.getCloudFoldersByConnectionId.mockResolvedValue({ + items: [], + capabilities: null, + freshness: { + refresh_state: 'fresh', + last_refreshed_at: SERVER_TS, + }, + }) + const w = mount(CloudFolderView, { global: { stubs: globalStubs } }) + await flushPromises() + // Find the setBrowseState call that carries a refreshedAt + const calls = mockSetBrowseState.mock.calls + const callWithTs = calls.find(c => c[0].refreshedAt !== undefined && c[0].refreshedAt !== null) + expect(callWithTs).toBeDefined() + // The timestamp must be exactly what the server sent + expect(callWithTs[0].refreshedAt).toBe(SERVER_TS) + // Must NOT contain 'Z' as a result of new Date().toISOString() producing a different value + // (if the test is run quickly, these might match by coincidence — but the semantic check is: + // does the code use the server value or call new Date()?) + // We verify this by checking the value matches the exact server string, not a generated one + expect(callWithTs[0].refreshedAt).not.toMatch(/^2026-06-22/) // not today's date + }) +}) + +describe('cached_items_remain_visible_during_warning', () => { + it('on HTTP 200 warning, items from response are still passed to store', async () => { + api.getCloudFoldersByConnectionId.mockResolvedValue({ + items: [FOLDER_A, FILE_A], + capabilities: null, + freshness: { + refresh_state: 'warning', + last_refreshed_at: '2026-06-01T00:00:00Z', + error_code: 'incomplete_listing', + error_message: null, + }, + }) + const w = mount(CloudFolderView, { global: { stubs: globalStubs } }) + await flushPromises() + // Items must be passed to the store even during a warning state + const callWithItems = mockSetBrowseState.mock.calls.find(c => c[0].items !== undefined) + expect(callWithItems).toBeDefined() + expect(callWithItems[0].items.length).toBe(2) + }) +})