mirror of
https://github.com/jcreek/LivingDexTracker.git
synced 2026-09-16 02:22:17 +00:00
fb95b43b30
Google answers a revoked or expired refresh token with invalid_grant. Every save then retried the dead token, and reconnecting never cleared the old error, so it kept showing on Backup Settings afterwards. - The Google Drive and Dropbox OAuth callbacks clear lastError when a provider is reconnected. - An invalid_grant, or a missing refresh token, now pauses the integration with a readable "reconnect" message instead of retrying it on every catch update. Other failures still retry as before. - A banner on every page and an alert on the Pokédex page point to Backup Settings, which shows a "Reconnect needed" badge. The Pokédex page re-checks backup status after each export, because saving a catch record also exports on the server and may pause a provider first. - Offline sync status and the "Save all artwork" link move from every page to a new /offline-guide page, linked from the user menu and the home and welcome pages. Only the offline read-only banner stays sitewide. - Unit tests cover every export path and the backup status store. BDD covers revocation and reconnecting for both providers, and the offline guide. The mock provider can now reject token refreshes, and mock control calls fail loudly if a stale mock is reused. Coverage thresholds are raised to the new baseline.
118 lines
3.2 KiB
TypeScript
118 lines
3.2 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { get } from 'svelte/store';
|
|
import {
|
|
backupsNeedingReconnect,
|
|
clearBackupStatus,
|
|
markReconnectNeeded,
|
|
refreshBackupStatus,
|
|
setBackupStatus
|
|
} from '$lib/stores/backupStatus';
|
|
|
|
describe('backup reconnect status', () => {
|
|
beforeEach(() => clearBackupStatus());
|
|
|
|
it('lists only the providers that exports switched off', () => {
|
|
setBackupStatus([
|
|
{ provider: 'google_drive', enabled: false },
|
|
{ provider: 'dropbox', enabled: true }
|
|
]);
|
|
expect(get(backupsNeedingReconnect)).toEqual(['google_drive']);
|
|
});
|
|
|
|
it('adds newly revoked providers without duplicating known ones', () => {
|
|
markReconnectNeeded(['google_drive']);
|
|
markReconnectNeeded(['google_drive', 'dropbox']);
|
|
expect(get(backupsNeedingReconnect)).toEqual(['google_drive', 'dropbox']);
|
|
});
|
|
|
|
it('clears everything, e.g. on sign-out', () => {
|
|
markReconnectNeeded(['dropbox']);
|
|
clearBackupStatus();
|
|
expect(get(backupsNeedingReconnect)).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('refreshBackupStatus', () => {
|
|
const fetchMock = vi.fn();
|
|
|
|
beforeEach(() => {
|
|
clearBackupStatus();
|
|
fetchMock.mockReset();
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
vi.stubGlobal('window', {});
|
|
vi.stubGlobal('navigator', { onLine: true });
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('loads the paused providers from the integrations API', async () => {
|
|
fetchMock.mockResolvedValue(
|
|
new Response(
|
|
JSON.stringify([
|
|
{ provider: 'google_drive', enabled: true },
|
|
{ provider: 'dropbox', enabled: false }
|
|
])
|
|
)
|
|
);
|
|
|
|
await refreshBackupStatus();
|
|
|
|
expect(fetchMock).toHaveBeenCalledWith('/api/export-integrations', {
|
|
credentials: 'include'
|
|
});
|
|
expect(get(backupsNeedingReconnect)).toEqual(['dropbox']);
|
|
});
|
|
|
|
it('clears a stale warning once the provider has been reconnected', async () => {
|
|
markReconnectNeeded(['google_drive']);
|
|
fetchMock.mockResolvedValue(
|
|
new Response(JSON.stringify([{ provider: 'google_drive', enabled: true }]))
|
|
);
|
|
|
|
await refreshBackupStatus();
|
|
|
|
expect(get(backupsNeedingReconnect)).toEqual([]);
|
|
});
|
|
|
|
it('does nothing while offline', async () => {
|
|
vi.stubGlobal('navigator', { onLine: false });
|
|
markReconnectNeeded(['google_drive']);
|
|
|
|
await refreshBackupStatus();
|
|
|
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
expect(get(backupsNeedingReconnect)).toEqual(['google_drive']);
|
|
});
|
|
|
|
it('does nothing during server rendering', async () => {
|
|
vi.stubGlobal('window', undefined);
|
|
|
|
await refreshBackupStatus();
|
|
|
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('keeps the last known status when the API rejects the request', async () => {
|
|
markReconnectNeeded(['google_drive']);
|
|
fetchMock.mockResolvedValue(new Response('Unauthorized', { status: 401 }));
|
|
|
|
await refreshBackupStatus();
|
|
|
|
expect(get(backupsNeedingReconnect)).toEqual(['google_drive']);
|
|
});
|
|
|
|
it('keeps the last known status when the request fails', async () => {
|
|
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
markReconnectNeeded(['dropbox']);
|
|
fetchMock.mockRejectedValue(new TypeError('Failed to fetch'));
|
|
|
|
await refreshBackupStatus();
|
|
|
|
expect(get(backupsNeedingReconnect)).toEqual(['dropbox']);
|
|
expect(consoleError).toHaveBeenCalled();
|
|
});
|
|
});
|