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:
Eddy de Vink 2026-08-05 20:55:14 +02:00
commit 0513db46ac
10 changed files with 207 additions and 58 deletions

View file

@ -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')
)
})
})