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
This commit is contained in:
curo1305
2026-06-22 08:45:50 +02:00
parent ba7f652cde
commit 9974fca2cb
2 changed files with 194 additions and 0 deletions
@@ -156,3 +156,83 @@ describe('useCloudConnectionsStore', () => {
expect(stored.startsWith('{')).toBe(false) 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')
})
})
@@ -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)
})
})