Critical: - Add deleted column: deleteMeasurement now soft-deletes, merge filters out deleted items based on updatedAt (was: deletions were resurrected) - Replace btoa with UTF-8-safe encodeBase64 utility for Hermes compat Important: - Preserve createdAt when overwriting duplicate-date measurements - Add 15s AbortController timeout to all WebDAV fetch calls - Revert broken date prefilling regression (MeasurementForm) - getAllMeasurements/latest/byDate now filter deleted records - Replace Dimensions with useWindowDimensions for rotation awareness - Replace silent catch with console.warn in HistoryScreen Tests: 8 new tests for deletion merge cases and soft-delete queries
102 lines
4 KiB
TypeScript
102 lines
4 KiB
TypeScript
import { mergeMeasurements } from '../src/sync/syncService'
|
|
import { Measurement } from '../src/types'
|
|
|
|
jest.mock('expo-sqlite', () => ({
|
|
openDatabaseAsync: jest.fn(),
|
|
}))
|
|
|
|
const base: Measurement = {
|
|
id: 'id-1',
|
|
weight: 75,
|
|
bmi: 22,
|
|
bodyFatPercentage: 15,
|
|
fatMass: 10,
|
|
musclePercentage: 40,
|
|
muscleMass: 30,
|
|
visceralFat: 8,
|
|
bmr: 1600,
|
|
measuredAt: '2024-06-01T08:00:00.000Z',
|
|
createdAt: '2024-06-01T08:00:00.000Z',
|
|
updatedAt: '2024-06-01T08:00:00.000Z',
|
|
syncStatus: 'local',
|
|
deleted: 0,
|
|
}
|
|
|
|
describe('mergeMeasurements', () => {
|
|
it('returns empty array when both are empty', () => {
|
|
expect(mergeMeasurements([], [])).toEqual([])
|
|
})
|
|
|
|
it('keeps local items not on remote', () => {
|
|
const result = mergeMeasurements([base], [])
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].id).toBe('id-1')
|
|
})
|
|
|
|
it('keeps remote items not on local', () => {
|
|
const remote = { ...base, syncStatus: 'synced' as const }
|
|
const result = mergeMeasurements([], [remote])
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].id).toBe('id-1')
|
|
})
|
|
|
|
it('picks the newer version when both have same id', () => {
|
|
const local = { ...base, weight: 75, updatedAt: '2024-06-02T08:00:00.000Z' }
|
|
const remote = { ...base, weight: 76, updatedAt: '2024-06-03T08:00:00.000Z' }
|
|
const result = mergeMeasurements([local], [remote])
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].weight).toBe(76)
|
|
})
|
|
|
|
it('picks local when local is newer', () => {
|
|
const local = { ...base, weight: 77, updatedAt: '2024-06-04T08:00:00.000Z' }
|
|
const remote = { ...base, weight: 76, updatedAt: '2024-06-03T08:00:00.000Z' }
|
|
const result = mergeMeasurements([local], [remote])
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].weight).toBe(77)
|
|
})
|
|
|
|
it('filters out locally deleted items even if present on remote', () => {
|
|
const local = { ...base, deleted: 1 as const, updatedAt: '2024-06-05T08:00:00.000Z' }
|
|
const remote = { ...base, deleted: 0 as const, updatedAt: '2024-06-03T08:00:00.000Z' }
|
|
const result = mergeMeasurements([local], [remote])
|
|
expect(result).toHaveLength(0)
|
|
})
|
|
|
|
it('filters out remotely deleted items when remote is newer', () => {
|
|
const local = { ...base, deleted: 0 as const, updatedAt: '2024-06-04T08:00:00.000Z' }
|
|
const remote = { ...base, deleted: 1 as const, updatedAt: '2024-06-05T08:00:00.000Z' }
|
|
const result = mergeMeasurements([local], [remote])
|
|
expect(result).toHaveLength(0)
|
|
})
|
|
|
|
it('keeps local active item when local is newer than remote deletion', () => {
|
|
const local = { ...base, deleted: 0 as const, weight: 78, updatedAt: '2024-06-06T08:00:00.000Z' }
|
|
const remote = { ...base, deleted: 1 as const, updatedAt: '2024-06-05T08:00:00.000Z' }
|
|
const result = mergeMeasurements([local], [remote])
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].weight).toBe(78)
|
|
})
|
|
|
|
it('keeps active item when local deleted is older than remote active', () => {
|
|
const local = { ...base, deleted: 1 as const, updatedAt: '2024-06-01T08:00:00.000Z' }
|
|
const remote = { ...base, deleted: 0 as const, updatedAt: '2024-06-05T08:00:00.000Z', syncStatus: 'synced' as const }
|
|
const result = mergeMeasurements([local], [remote])
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].weight).toBe(75)
|
|
})
|
|
|
|
it('merges multiple active and deleted items correctly', () => {
|
|
const localActive = { ...base, id: 'a', weight: 70, updatedAt: '2024-06-05T08:00:00.000Z' }
|
|
const localDeleted = { ...base, id: 'b', weight: 80, deleted: 1 as const, updatedAt: '2024-06-05T08:00:00.000Z' }
|
|
const remoteActive = { ...base, id: 'c', weight: 90, updatedAt: '2024-06-03T08:00:00.000Z', syncStatus: 'synced' as const }
|
|
const remoteDeleted = { ...base, id: 'd', weight: 100, deleted: 1 as const, updatedAt: '2024-06-05T08:00:00.000Z' }
|
|
|
|
const result = mergeMeasurements(
|
|
[localActive, localDeleted],
|
|
[remoteActive, remoteDeleted]
|
|
)
|
|
expect(result).toHaveLength(2)
|
|
expect(result.map(m => m.id).sort()).toEqual(['a', 'c'])
|
|
})
|
|
})
|