fix: sync deletion propagation, add base64 util, timeout, and merge improvements
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
This commit is contained in:
parent
c499f7df13
commit
0513db46ac
10 changed files with 207 additions and 58 deletions
|
|
@ -19,6 +19,7 @@ const mockMeasurement: Measurement = {
|
|||
createdAt: '2024-01-01T08:00:00.000Z',
|
||||
updatedAt: '2024-01-01T08:00:00.000Z',
|
||||
syncStatus: 'local',
|
||||
deleted: 0,
|
||||
}
|
||||
|
||||
describe('database', () => {
|
||||
|
|
@ -28,6 +29,8 @@ describe('database', () => {
|
|||
let getAllMeasurements: any
|
||||
let getLatestMeasurement: any
|
||||
let updateMeasurement: any
|
||||
let deleteMeasurement: any
|
||||
let getAllMeasurementsIncludingDeleted: any
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.resetModules()
|
||||
|
|
@ -35,8 +38,8 @@ describe('database', () => {
|
|||
openDatabaseAsync: jest.fn(),
|
||||
}))
|
||||
db = {
|
||||
execAsync: jest.fn(),
|
||||
runAsync: jest.fn(),
|
||||
execAsync: jest.fn(() => Promise.resolve()),
|
||||
runAsync: jest.fn(() => Promise.resolve()),
|
||||
getAllAsync: jest.fn(),
|
||||
}
|
||||
const sqlite = require('expo-sqlite')
|
||||
|
|
@ -47,6 +50,8 @@ describe('database', () => {
|
|||
getAllMeasurements = mod.getAllMeasurements
|
||||
getLatestMeasurement = mod.getLatestMeasurement
|
||||
updateMeasurement = mod.updateMeasurement
|
||||
deleteMeasurement = mod.deleteMeasurement
|
||||
getAllMeasurementsIncludingDeleted = mod.getAllMeasurementsIncludingDeleted
|
||||
})
|
||||
|
||||
it('initDatabase creates the measurements table', async () => {
|
||||
|
|
@ -91,4 +96,33 @@ describe('database', () => {
|
|||
expect.arrayContaining(['synced', mockMeasurement.id])
|
||||
)
|
||||
})
|
||||
|
||||
it('deleteMeasurement marks measurement as deleted', async () => {
|
||||
db.runAsync.mockResolvedValue(undefined)
|
||||
await initDatabase()
|
||||
await deleteMeasurement(mockMeasurement.id)
|
||||
expect(db.runAsync).toHaveBeenCalledWith(
|
||||
expect.stringContaining('UPDATE measurements SET deleted = 1'),
|
||||
expect.arrayContaining([mockMeasurement.id])
|
||||
)
|
||||
})
|
||||
|
||||
it('getAllMeasurements filters out deleted records', async () => {
|
||||
await initDatabase()
|
||||
await getAllMeasurements()
|
||||
expect(db.getAllAsync).toHaveBeenCalledWith(
|
||||
expect.stringContaining('deleted = 0')
|
||||
)
|
||||
})
|
||||
|
||||
it('getAllMeasurementsIncludingDeleted includes deleted records', async () => {
|
||||
await initDatabase()
|
||||
await getAllMeasurementsIncludingDeleted()
|
||||
expect(db.getAllAsync).toHaveBeenCalledWith(
|
||||
expect.stringContaining('SELECT * FROM measurements')
|
||||
)
|
||||
expect(db.getAllAsync).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('deleted = 0')
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ const base: Measurement = {
|
|||
createdAt: '2024-06-01T08:00:00.000Z',
|
||||
updatedAt: '2024-06-01T08:00:00.000Z',
|
||||
syncStatus: 'local',
|
||||
deleted: 0,
|
||||
}
|
||||
|
||||
describe('mergeMeasurements', () => {
|
||||
|
|
@ -54,4 +55,48 @@ describe('mergeMeasurements', () => {
|
|||
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'])
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue