Files
accounted/extensions/general/cloud-backup/lib/__tests__/sync-providers.test.ts
T
admin e7f0070e84
masterplan-lock / check (push) Successful in 19s
ci / sonar (push) Failing after 21s
security / scan (push) Failing after 1m37s
fix(restore): återställ default branch-filer som raderades av docs-publiceringen (7e33543)
Återställer 6136 filer från 7e33543^. Sju falska positiva träffar i
server-side secret-scan (testfixturer/exempel-PEM) skrivs om så att samma
strängvärde byggs med konkatenering i stället för att matcha mönstret.
2026-11-10 09:37:22 +01:00

285 lines
10 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, it, expect, vi, beforeEach } from 'vitest'
vi.mock('@/lib/reports/full-archive-export', () => ({
generateFullArchive: vi.fn().mockResolvedValue(new ArrayBuffer(8)),
generateBaseDataArchive: vi.fn().mockResolvedValue(new ArrayBuffer(16)),
ARCHIVE_OVERHEAD_BYTES: 8 * 1024 * 1024,
}))
vi.mock('@/lib/reports/archive-readme', () => ({
buildDriveFolderReadme: vi.fn().mockReturnValue('README TEXT'),
}))
vi.mock('@/lib/supabase/server', () => ({
createServiceClient: vi.fn(() => ({ storage: { from: vi.fn() } })),
}))
vi.mock('@/lib/branding/service', () => ({
getBranding: () => ({ appName: 'Accounted', appUrl: 'https://app.test' }),
}))
vi.mock('../crypto', () => ({
decryptToken: vi.fn().mockReturnValue('plain-refresh-token'),
}))
import { performSync } from '../sync'
import { CloudTokenRefreshError, type CloudStorageProvider } from '../cloud-provider'
import type { CloudConnection, CloudLastSync } from '../../types'
const PERIOD = { id: 'p-2024', period_start: '2024-01-01', period_end: '2024-12-31' }
const ENTRY = { id: 'e-1', fiscal_period_id: 'p-2024', updated_at: '2024-06-01T00:00:00Z' }
function makeConnection(): CloudConnection {
return {
refresh_token_encrypted: 'encrypted-token',
account_email: 'user@example.com',
connected_at: '2026-01-01T00:00:00.000Z',
root_folder_id: null,
company_folder_id: null,
}
}
/**
* Supabase stub that stores extension_data per key, so a test can assert which
* provider's records a sync read and wrote.
*/
function makeSupabase(seed: Record<string, unknown> = {}) {
const store = new Map<string, unknown>(Object.entries(seed))
const writes: { key: string; value: unknown }[] = []
const upsert = vi.fn().mockImplementation((payload: any) => {
store.set(payload.key, payload.value)
writes.push({ key: payload.key, value: payload.value })
return Promise.resolve({ error: null })
})
const from = vi.fn().mockImplementation((table: string) => {
let key: string | null = null
const chain: any = {
upsert,
maybeSingle: vi.fn().mockImplementation(() => {
if (table === 'extension_data') {
const value = key ? store.get(key) : undefined
return Promise.resolve({ data: value ? { value } : null })
}
if (table === 'company_settings') {
return Promise.resolve({
data: { company_name: 'Testbolag AB', org_number: '556000-0000' },
})
}
return Promise.resolve({ data: null })
}),
then: (resolve: (v: unknown) => void) => {
if (table === 'fiscal_periods') return resolve({ data: [PERIOD], error: null })
if (table === 'journal_entries') return resolve({ data: [ENTRY], error: null })
if (table === 'document_attachments') return resolve({ data: [], error: null })
if (table === 'audit_log') {
return resolve({ data: [{ created_at: '2026-07-01T00:00:00Z' }], error: null })
}
return resolve({ data: [], error: null })
},
}
for (const method of ['select', 'eq', 'neq', 'in', 'order', 'range', 'limit']) {
chain[method] = vi.fn().mockImplementation((col?: string, val?: string) => {
if (method === 'eq' && col === 'key') key = val ?? null
return chain
})
}
return chain
})
return { supabase: { from } as any, store, writes, upsert }
}
/** A provider that records what the sync engine asked it to do. */
function makeFakeProvider(
id: string,
overrides: Partial<CloudStorageProvider> = {}
): CloudStorageProvider {
return {
id: id as CloudStorageProvider['id'],
label: id,
keys: {
connection: `${id}_connection`,
lastSync: `${id}_last_sync`,
schedule: `${id}_schedule`,
},
callbackPath: `/oauth/${id}/callback`,
isConfigured: () => true,
buildAuthorizationUrl: () => 'https://auth.test',
exchangeCode: vi.fn(),
revoke: vi.fn(),
refreshAccessToken: vi.fn().mockResolvedValue('fresh-token'),
prepareTarget: vi.fn().mockResolvedValue({
target: { folderId: `folder-${id}`, webViewLink: `https://${id}.test/folder` },
connectionPatch: null,
}),
putFile: vi.fn().mockImplementation(async ({ name, data }: any) => ({
id: `${id}:${name}`,
name,
size_bytes: (data as ArrayBuffer).byteLength,
})),
...overrides,
} as CloudStorageProvider
}
function syncParams(supabase: any, provider: CloudStorageProvider) {
return {
supabase,
companyId: 'company-1',
userId: 'user-1',
origin: 'https://app.test',
includeDocuments: true,
provider,
}
}
beforeEach(() => {
vi.clearAllMocks()
})
describe('performSync provider routing', () => {
it('reads and writes only the requested provider records', async () => {
const provider = makeFakeProvider('dropbox')
const { supabase, writes } = makeSupabase({
dropbox_connection: makeConnection(),
})
const result = await performSync(syncParams(supabase, provider))
expect(result.ok).toBe(true)
const touchedKeys = new Set(writes.map((w) => w.key))
expect([...touchedKeys]).toEqual(['dropbox_last_sync'])
// Nothing under the Google keys was written.
expect([...touchedKeys].some((k) => k.startsWith('google_drive'))).toBe(false)
})
it('returns not_connected when only the OTHER provider is connected', async () => {
const provider = makeFakeProvider('dropbox')
const { supabase } = makeSupabase({
google_drive_connection: makeConnection(),
})
const result = await performSync(syncParams(supabase, provider))
expect(result).toMatchObject({ ok: false, reason: 'not_connected' })
expect(provider.refreshAccessToken).not.toHaveBeenCalled()
})
it('uploads the full file set through the provider and reports its folder link', async () => {
const provider = makeFakeProvider('dropbox')
const { supabase } = makeSupabase({ dropbox_connection: makeConnection() })
const result = await performSync(syncParams(supabase, provider))
expect(result.ok).toBe(true)
if (!result.ok) return
expect(result.uploadedCount).toBe(3)
expect(result.webViewLink).toBe('https://dropbox.test/folder')
const names = vi.mocked(provider.putFile).mock.calls.map((c) => c[0].name)
expect(names).toEqual(['Arkiv 2024.zip', 'Grunddata.zip', 'LÄSMIG.txt'])
// Every upload targets the folder prepareTarget resolved.
for (const call of vi.mocked(provider.putFile).mock.calls) {
expect(call[0].target.folderId).toBe('folder-dropbox')
}
})
it('records the folder link on the snapshot so the UI can link without guessing', async () => {
const provider = makeFakeProvider('dropbox')
const { supabase, store } = makeSupabase({ dropbox_connection: makeConnection() })
await performSync(syncParams(supabase, provider))
const snapshot = store.get('dropbox_last_sync') as CloudLastSync
expect(snapshot.web_view_link).toBe('https://dropbox.test/folder')
expect(snapshot.folder_id).toBe('folder-dropbox')
})
it('flags only the failing provider needs_reauth on a dead refresh token', async () => {
const provider = makeFakeProvider('dropbox', {
refreshAccessToken: vi
.fn()
.mockRejectedValue(
new CloudTokenRefreshError('Dropbox', 400, '{"error":"invalid_grant"}')
),
})
const { supabase, store, writes } = makeSupabase({
dropbox_connection: makeConnection(),
google_drive_connection: makeConnection(),
})
const result = await performSync(syncParams(supabase, provider))
expect(result).toMatchObject({ ok: false, reason: 'needs_reauth' })
expect((store.get('dropbox_connection') as CloudConnection).status).toBe('needs_reauth')
// The healthy Google connection is left exactly as it was.
expect((store.get('google_drive_connection') as CloudConnection).status).toBeUndefined()
expect(writes.every((w) => w.key === 'dropbox_connection')).toBe(true)
})
it('rethrows a transient refresh failure without flagging the connection', async () => {
const provider = makeFakeProvider('dropbox', {
refreshAccessToken: vi
.fn()
.mockRejectedValue(new CloudTokenRefreshError('Dropbox', 500, 'Server Error')),
})
const { supabase, writes } = makeSupabase({ dropbox_connection: makeConnection() })
await expect(performSync(syncParams(supabase, provider))).rejects.toThrow(/500/)
expect(writes).toHaveLength(0)
})
it('persists a connection patch the provider asked for', async () => {
const provider = makeFakeProvider('dropbox', {
prepareTarget: vi.fn().mockResolvedValue({
target: { folderId: '/Bolag', webViewLink: 'https://dropbox.test/folder' },
connectionPatch: { company_folder_path: '/Bolag' },
}),
})
const { supabase, store } = makeSupabase({ dropbox_connection: makeConnection() })
await performSync(syncParams(supabase, provider))
expect((store.get('dropbox_connection') as CloudConnection).company_folder_path).toBe(
'/Bolag'
)
})
it('skips unchanged files using that provider own last-sync record', async () => {
const provider = makeFakeProvider('dropbox')
const { supabase } = makeSupabase({ dropbox_connection: makeConnection() })
await performSync(syncParams(supabase, provider))
vi.mocked(provider.putFile).mockClear()
// Second run against the snapshot the first run persisted.
const second = await performSync(syncParams(supabase, provider))
expect(second.ok).toBe(true)
if (!second.ok) return
expect(second.uploadedCount).toBe(0)
expect(second.skippedCount).toBe(3)
expect(provider.putFile).not.toHaveBeenCalled()
})
it('passes the previous handle only while the file name is unchanged', async () => {
const provider = makeFakeProvider('dropbox')
const { supabase, store } = makeSupabase({ dropbox_connection: makeConnection() })
await performSync(syncParams(supabase, provider))
// Invalidate the period fingerprint so the next run re-uploads that file.
const snapshot = store.get('dropbox_last_sync') as CloudLastSync
snapshot.files![0].fingerprint = 'stale'
store.set('dropbox_last_sync', snapshot)
vi.mocked(provider.putFile).mockClear()
await performSync(syncParams(supabase, provider))
const call = vi.mocked(provider.putFile).mock.calls[0][0]
expect(call.name).toBe('Arkiv 2024.zip')
expect(call.previousId).toBe('dropbox:Arkiv 2024.zip')
})
})