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
128 lines
4 KiB
TypeScript
128 lines
4 KiB
TypeScript
import { Measurement } from '../src/types'
|
|
import * as SQLite from 'expo-sqlite'
|
|
|
|
jest.mock('expo-sqlite', () => ({
|
|
openDatabaseAsync: jest.fn(),
|
|
}))
|
|
|
|
const mockMeasurement: Measurement = {
|
|
id: '550e8400-e29b-41d4-a716-446655440000',
|
|
weight: 75.5,
|
|
bmi: 22.3,
|
|
bodyFatPercentage: 15.2,
|
|
fatMass: 11.5,
|
|
musclePercentage: 40.1,
|
|
muscleMass: 30.3,
|
|
visceralFat: 8,
|
|
bmr: 1650,
|
|
measuredAt: '2024-01-01T08:00:00.000Z',
|
|
createdAt: '2024-01-01T08:00:00.000Z',
|
|
updatedAt: '2024-01-01T08:00:00.000Z',
|
|
syncStatus: 'local',
|
|
deleted: 0,
|
|
}
|
|
|
|
describe('database', () => {
|
|
let db: any
|
|
let initDatabase: any
|
|
let insertMeasurement: any
|
|
let getAllMeasurements: any
|
|
let getLatestMeasurement: any
|
|
let updateMeasurement: any
|
|
let deleteMeasurement: any
|
|
let getAllMeasurementsIncludingDeleted: any
|
|
|
|
beforeEach(async () => {
|
|
jest.resetModules()
|
|
jest.doMock('expo-sqlite', () => ({
|
|
openDatabaseAsync: jest.fn(),
|
|
}))
|
|
db = {
|
|
execAsync: jest.fn(() => Promise.resolve()),
|
|
runAsync: jest.fn(() => Promise.resolve()),
|
|
getAllAsync: jest.fn(),
|
|
}
|
|
const sqlite = require('expo-sqlite')
|
|
;(sqlite.openDatabaseAsync as jest.Mock).mockResolvedValue(db)
|
|
const mod = require('../src/database/db')
|
|
initDatabase = mod.initDatabase
|
|
insertMeasurement = mod.insertMeasurement
|
|
getAllMeasurements = mod.getAllMeasurements
|
|
getLatestMeasurement = mod.getLatestMeasurement
|
|
updateMeasurement = mod.updateMeasurement
|
|
deleteMeasurement = mod.deleteMeasurement
|
|
getAllMeasurementsIncludingDeleted = mod.getAllMeasurementsIncludingDeleted
|
|
})
|
|
|
|
it('initDatabase creates the measurements table', async () => {
|
|
await initDatabase()
|
|
expect(db.execAsync).toHaveBeenCalledWith(
|
|
expect.stringContaining('CREATE TABLE IF NOT EXISTS measurements')
|
|
)
|
|
})
|
|
|
|
it('insertMeasurement inserts a row', async () => {
|
|
db.runAsync.mockResolvedValue(undefined)
|
|
await initDatabase()
|
|
await insertMeasurement(mockMeasurement)
|
|
expect(db.runAsync).toHaveBeenCalledWith(
|
|
expect.stringContaining('INSERT INTO measurements'),
|
|
expect.arrayContaining([mockMeasurement.id])
|
|
)
|
|
})
|
|
|
|
it('getAllMeasurements returns all measurements ordered by measuredAt desc', async () => {
|
|
db.getAllAsync.mockResolvedValue([{ ...mockMeasurement }])
|
|
await initDatabase()
|
|
const result = await getAllMeasurements()
|
|
expect(result).toHaveLength(1)
|
|
expect(result[0].id).toBe(mockMeasurement.id)
|
|
})
|
|
|
|
it('getLatestMeasurement returns the most recent', async () => {
|
|
db.getAllAsync.mockResolvedValue([{ ...mockMeasurement }])
|
|
await initDatabase()
|
|
const result = await getLatestMeasurement()
|
|
expect(result).not.toBeNull()
|
|
expect(result!.id).toBe(mockMeasurement.id)
|
|
})
|
|
|
|
it('updateMeasurement updates syncStatus', async () => {
|
|
db.runAsync.mockResolvedValue(undefined)
|
|
await initDatabase()
|
|
await updateMeasurement(mockMeasurement.id, 'synced')
|
|
expect(db.runAsync).toHaveBeenCalledWith(
|
|
expect.stringContaining('UPDATE measurements'),
|
|
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')
|
|
)
|
|
})
|
|
})
|