fix(backup): don't let a stale export pause a reconnected backup

- An export now pauses a revoked integration only if its row is
  unchanged since the export read it, using the trigger-maintained
  updatedAt column as the row version. If the user reconnected in the
  meantime, the stale failure no longer disables the fresh credentials
  or asks the user to reconnect again. updateExportStatus now reports
  whether a row was written.
- The backup status store ignores a response overtaken by a newer
  refresh, or by the status being flagged, cleared or set directly, so
  a slow response can't overwrite newer state.
- Unit tests cover the whole integration repository, the guarded pause
  and stale status responses. Coverage thresholds are raised to the
  new baseline.
This commit is contained in:
Josh Creek
2026-09-14 19:03:29 +01:00
parent fb95b43b30
commit 7786d46078
8 changed files with 398 additions and 26 deletions
+58
View File
@@ -104,6 +104,64 @@ describe('refreshBackupStatus', () => {
expect(get(backupsNeedingReconnect)).toEqual(['google_drive']);
});
function deferredResponse() {
let resolve!: (response: Response) => void;
const promise = new Promise<Response>((r) => (resolve = r));
return { promise, resolve };
}
const json = (body: unknown) => new Response(JSON.stringify(body));
it('ignores a response overtaken by a newer refresh', async () => {
const slow = deferredResponse();
fetchMock
.mockReturnValueOnce(slow.promise)
.mockResolvedValueOnce(json([{ provider: 'google_drive', enabled: true }]));
const first = refreshBackupStatus();
await refreshBackupStatus();
slow.resolve(json([{ provider: 'google_drive', enabled: false }]));
await first;
expect(get(backupsNeedingReconnect)).toEqual([]);
});
it('ignores a response that arrives after the status was flagged directly', async () => {
const slow = deferredResponse();
fetchMock.mockReturnValueOnce(slow.promise);
const pending = refreshBackupStatus();
markReconnectNeeded(['dropbox']);
slow.resolve(json([{ provider: 'dropbox', enabled: true }]));
await pending;
expect(get(backupsNeedingReconnect)).toEqual(['dropbox']);
});
it('ignores a response that arrives after the status was cleared', async () => {
const slow = deferredResponse();
fetchMock.mockReturnValueOnce(slow.promise);
const pending = refreshBackupStatus();
clearBackupStatus();
slow.resolve(json([{ provider: 'google_drive', enabled: false }]));
await pending;
expect(get(backupsNeedingReconnect)).toEqual([]);
});
it('ignores a response that arrives after the status was set from another source', async () => {
const slow = deferredResponse();
fetchMock.mockReturnValueOnce(slow.promise);
const pending = refreshBackupStatus();
setBackupStatus([{ provider: 'google_drive', enabled: true }]);
slow.resolve(json([{ provider: 'google_drive', enabled: false }]));
await pending;
expect(get(backupsNeedingReconnect)).toEqual([]);
});
it('keeps the last known status when the request fails', async () => {
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined);
markReconnectNeeded(['dropbox']);