mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
test(import): pin the CSV import before moving its writer
This commit is contained in:
1 parent
5d9a9107fa
commit
94ad53756e
7 files changed
+1209
No files matched your search
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"status": 200,
|
||||
"body": {
|
||||
"data": {
|
||||
"inserted": 7,
|
||||
"updated": 0,
|
||||
"skipped": 1,
|
||||
"total": 8,
|
||||
"dryRun": true,
|
||||
"rows": [
|
||||
{
|
||||
"line": 2,
|
||||
"status": "inserted"
|
||||
},
|
||||
{
|
||||
"line": 3,
|
||||
"status": "inserted"
|
||||
},
|
||||
{
|
||||
"line": 4,
|
||||
"status": "inserted"
|
||||
},
|
||||
{
|
||||
"line": 5,
|
||||
"status": "inserted"
|
||||
},
|
||||
{
|
||||
"line": 6,
|
||||
"status": "inserted"
|
||||
},
|
||||
{
|
||||
"line": 7,
|
||||
"status": "inserted"
|
||||
},
|
||||
{
|
||||
"line": 8,
|
||||
"status": "inserted"
|
||||
},
|
||||
{
|
||||
"line": 9,
|
||||
"status": "skipped",
|
||||
"reason": "invalid_value"
|
||||
}
|
||||
]
|
||||
},
|
||||
"error": null
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"status": 200,
|
||||
"action": {
|
||||
"name": "import.csv.upload"
|
||||
},
|
||||
"meta": {
|
||||
"import_csv_inserted": 7,
|
||||
"import_csv_updated": 0,
|
||||
"import_csv_skipped": 1,
|
||||
"import_csv_dry_run": true
|
||||
},
|
||||
"warnings": null,
|
||||
"error": null
|
||||
}
|
||||
],
|
||||
"effects": [],
|
||||
"stored": {
|
||||
"measurements": [],
|
||||
"rollups": [],
|
||||
"audit": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
{
|
||||
"status": 200,
|
||||
"body": {
|
||||
"data": {
|
||||
"inserted": 5,
|
||||
"updated": 1,
|
||||
"skipped": 2,
|
||||
"total": 8,
|
||||
"dryRun": false,
|
||||
"rows": [
|
||||
{
|
||||
"line": 2,
|
||||
"status": "inserted"
|
||||
},
|
||||
{
|
||||
"line": 3,
|
||||
"status": "inserted"
|
||||
},
|
||||
{
|
||||
"line": 4,
|
||||
"status": "updated"
|
||||
},
|
||||
{
|
||||
"line": 5,
|
||||
"status": "inserted"
|
||||
},
|
||||
{
|
||||
"line": 6,
|
||||
"status": "skipped",
|
||||
"reason": "duplicate"
|
||||
},
|
||||
{
|
||||
"line": 7,
|
||||
"status": "inserted"
|
||||
},
|
||||
{
|
||||
"line": 8,
|
||||
"status": "inserted"
|
||||
},
|
||||
{
|
||||
"line": 9,
|
||||
"status": "skipped",
|
||||
"reason": "invalid_value"
|
||||
}
|
||||
]
|
||||
},
|
||||
"error": null
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"status": 200,
|
||||
"action": {
|
||||
"name": "import.csv.upload"
|
||||
},
|
||||
"meta": {
|
||||
"import_csv_inserted": 5,
|
||||
"import_csv_updated": 1,
|
||||
"import_csv_skipped": 2,
|
||||
"import_csv_dry_run": false
|
||||
},
|
||||
"warnings": null,
|
||||
"error": null
|
||||
}
|
||||
],
|
||||
"effects": [
|
||||
{
|
||||
"effect": "emitDataArrival",
|
||||
"args": [
|
||||
{
|
||||
"userId": "<id:1>",
|
||||
"kind": "weight",
|
||||
"newestSampleAt": "2026-10-11T06:30:00.000Z",
|
||||
"insertedCount": 2,
|
||||
"refId": "<id:2>",
|
||||
"source": "csv_import"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"effect": "maybeEnqueueMorningRefresh",
|
||||
"args": [
|
||||
"<id:1>",
|
||||
[
|
||||
"2026-10-12T06:00:00.000Z"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"effect": "invalidateStatusInsightsForTypes",
|
||||
"args": [
|
||||
"<id:1>",
|
||||
[
|
||||
"WEIGHT",
|
||||
"SLEEP_DURATION",
|
||||
"BLOOD_GLUCOSE",
|
||||
"PULSE"
|
||||
],
|
||||
{
|
||||
"source": "manual"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"stored": {
|
||||
"measurements": [
|
||||
{
|
||||
"type": "WEIGHT",
|
||||
"value": 81.4,
|
||||
"unit": "kg",
|
||||
"source": "IMPORT",
|
||||
"externalId": "exp-w-1",
|
||||
"measuredAt": "2026-10-10T06:30:00.000Z",
|
||||
"notes": null,
|
||||
"notesEncrypted": "<bytes:59>",
|
||||
"glucoseContext": null,
|
||||
"deletedAt": null
|
||||
},
|
||||
{
|
||||
"type": "WEIGHT",
|
||||
"value": 82,
|
||||
"unit": "kg",
|
||||
"source": "IMPORT",
|
||||
"externalId": "exp-w-2",
|
||||
"measuredAt": "2026-10-11T06:30:00.000Z",
|
||||
"notes": null,
|
||||
"notesEncrypted": null,
|
||||
"glucoseContext": null,
|
||||
"deletedAt": null
|
||||
},
|
||||
{
|
||||
"type": "PULSE",
|
||||
"value": 64,
|
||||
"unit": "bpm",
|
||||
"source": "IMPORT",
|
||||
"externalId": null,
|
||||
"measuredAt": "2026-10-12T07:00:00.000Z",
|
||||
"notes": null,
|
||||
"notesEncrypted": null,
|
||||
"glucoseContext": null,
|
||||
"deletedAt": null
|
||||
},
|
||||
{
|
||||
"type": "SLEEP_DURATION",
|
||||
"value": 412,
|
||||
"unit": "minutes",
|
||||
"source": "IMPORT",
|
||||
"externalId": "exp-s-1",
|
||||
"measuredAt": "2026-10-12T06:00:00.000Z",
|
||||
"notes": null,
|
||||
"notesEncrypted": null,
|
||||
"glucoseContext": null,
|
||||
"deletedAt": null
|
||||
},
|
||||
{
|
||||
"type": "BLOOD_GLUCOSE",
|
||||
"value": 104,
|
||||
"unit": "mg/dL",
|
||||
"source": "IMPORT",
|
||||
"externalId": null,
|
||||
"measuredAt": "2026-10-11T08:00:00.000Z",
|
||||
"notes": null,
|
||||
"notesEncrypted": null,
|
||||
"glucoseContext": "FASTING",
|
||||
"deletedAt": null
|
||||
}
|
||||
],
|
||||
"rollups": [
|
||||
{
|
||||
"type": "WEIGHT",
|
||||
"bucketStart": "2026-10-10T00:00:00.000Z",
|
||||
"count": 1,
|
||||
"mean": 81.4
|
||||
},
|
||||
{
|
||||
"type": "WEIGHT",
|
||||
"bucketStart": "2026-10-11T00:00:00.000Z",
|
||||
"count": 1,
|
||||
"mean": 82
|
||||
},
|
||||
{
|
||||
"type": "PULSE",
|
||||
"bucketStart": "2026-10-12T00:00:00.000Z",
|
||||
"count": 1,
|
||||
"mean": 64
|
||||
},
|
||||
{
|
||||
"type": "SLEEP_DURATION",
|
||||
"bucketStart": "2026-10-12T00:00:00.000Z",
|
||||
"count": 1,
|
||||
"mean": 412
|
||||
},
|
||||
{
|
||||
"type": "BLOOD_GLUCOSE",
|
||||
"bucketStart": "2026-10-11T00:00:00.000Z",
|
||||
"count": 1,
|
||||
"mean": 104
|
||||
}
|
||||
],
|
||||
"audit": [
|
||||
{
|
||||
"action": "import.csv.upload",
|
||||
"details": "{\"inserted\":5,\"updated\":1,\"skipped\":2}"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
{
|
||||
"status": 200,
|
||||
"body": {
|
||||
"data": {
|
||||
"inserted": 0,
|
||||
"updated": 2,
|
||||
"skipped": 6,
|
||||
"total": 8,
|
||||
"dryRun": false,
|
||||
"rows": [
|
||||
{
|
||||
"line": 2,
|
||||
"status": "skipped",
|
||||
"reason": "duplicate"
|
||||
},
|
||||
{
|
||||
"line": 3,
|
||||
"status": "updated"
|
||||
},
|
||||
{
|
||||
"line": 4,
|
||||
"status": "updated"
|
||||
},
|
||||
{
|
||||
"line": 5,
|
||||
"status": "skipped",
|
||||
"reason": "duplicate"
|
||||
},
|
||||
{
|
||||
"line": 6,
|
||||
"status": "skipped",
|
||||
"reason": "duplicate"
|
||||
},
|
||||
{
|
||||
"line": 7,
|
||||
"status": "skipped",
|
||||
"reason": "duplicate"
|
||||
},
|
||||
{
|
||||
"line": 8,
|
||||
"status": "skipped",
|
||||
"reason": "duplicate"
|
||||
},
|
||||
{
|
||||
"line": 9,
|
||||
"status": "skipped",
|
||||
"reason": "invalid_value"
|
||||
}
|
||||
]
|
||||
},
|
||||
"error": null
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"status": 200,
|
||||
"action": {
|
||||
"name": "import.csv.upload"
|
||||
},
|
||||
"meta": {
|
||||
"import_csv_inserted": 0,
|
||||
"import_csv_updated": 2,
|
||||
"import_csv_skipped": 6,
|
||||
"import_csv_dry_run": false
|
||||
},
|
||||
"warnings": null,
|
||||
"error": null
|
||||
}
|
||||
],
|
||||
"effects": [
|
||||
{
|
||||
"effect": "maybeEnqueueMorningRefresh",
|
||||
"args": [
|
||||
"<id:1>",
|
||||
[]
|
||||
]
|
||||
},
|
||||
{
|
||||
"effect": "invalidateStatusInsightsForTypes",
|
||||
"args": [
|
||||
"<id:1>",
|
||||
[
|
||||
"WEIGHT"
|
||||
],
|
||||
{
|
||||
"source": "manual"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"stored": {
|
||||
"measurements": [
|
||||
{
|
||||
"type": "WEIGHT",
|
||||
"value": 81.4,
|
||||
"unit": "kg",
|
||||
"source": "IMPORT",
|
||||
"externalId": "exp-w-1",
|
||||
"measuredAt": "2026-10-10T06:30:00.000Z",
|
||||
"notes": null,
|
||||
"notesEncrypted": "<bytes:59>",
|
||||
"glucoseContext": null,
|
||||
"deletedAt": null
|
||||
},
|
||||
{
|
||||
"type": "WEIGHT",
|
||||
"value": 82,
|
||||
"unit": "kg",
|
||||
"source": "IMPORT",
|
||||
"externalId": "exp-w-2",
|
||||
"measuredAt": "2026-10-11T06:30:00.000Z",
|
||||
"notes": null,
|
||||
"notesEncrypted": null,
|
||||
"glucoseContext": null,
|
||||
"deletedAt": null
|
||||
},
|
||||
{
|
||||
"type": "PULSE",
|
||||
"value": 64,
|
||||
"unit": "bpm",
|
||||
"source": "IMPORT",
|
||||
"externalId": null,
|
||||
"measuredAt": "2026-10-12T07:00:00.000Z",
|
||||
"notes": null,
|
||||
"notesEncrypted": null,
|
||||
"glucoseContext": null,
|
||||
"deletedAt": null
|
||||
},
|
||||
{
|
||||
"type": "SLEEP_DURATION",
|
||||
"value": 412,
|
||||
"unit": "minutes",
|
||||
"source": "IMPORT",
|
||||
"externalId": "exp-s-1",
|
||||
"measuredAt": "2026-10-12T06:00:00.000Z",
|
||||
"notes": null,
|
||||
"notesEncrypted": null,
|
||||
"glucoseContext": null,
|
||||
"deletedAt": null
|
||||
},
|
||||
{
|
||||
"type": "BLOOD_GLUCOSE",
|
||||
"value": 104,
|
||||
"unit": "mg/dL",
|
||||
"source": "IMPORT",
|
||||
"externalId": null,
|
||||
"measuredAt": "2026-10-11T08:00:00.000Z",
|
||||
"notes": null,
|
||||
"notesEncrypted": null,
|
||||
"glucoseContext": "FASTING",
|
||||
"deletedAt": null
|
||||
}
|
||||
],
|
||||
"rollups": [
|
||||
{
|
||||
"type": "WEIGHT",
|
||||
"bucketStart": "2026-10-10T00:00:00.000Z",
|
||||
"count": 1,
|
||||
"mean": 81.4
|
||||
},
|
||||
{
|
||||
"type": "WEIGHT",
|
||||
"bucketStart": "2026-10-11T00:00:00.000Z",
|
||||
"count": 1,
|
||||
"mean": 82
|
||||
},
|
||||
{
|
||||
"type": "PULSE",
|
||||
"bucketStart": "2026-10-12T00:00:00.000Z",
|
||||
"count": 1,
|
||||
"mean": 64
|
||||
},
|
||||
{
|
||||
"type": "SLEEP_DURATION",
|
||||
"bucketStart": "2026-10-12T00:00:00.000Z",
|
||||
"count": 1,
|
||||
"mean": 412
|
||||
},
|
||||
{
|
||||
"type": "BLOOD_GLUCOSE",
|
||||
"bucketStart": "2026-10-11T00:00:00.000Z",
|
||||
"count": 1,
|
||||
"mean": 104
|
||||
}
|
||||
],
|
||||
"audit": [
|
||||
{
|
||||
"action": "import.csv.upload",
|
||||
"details": "{\"inserted\":5,\"updated\":1,\"skipped\":2}"
|
||||
},
|
||||
{
|
||||
"action": "import.csv.upload",
|
||||
"details": "{\"inserted\":0,\"updated\":2,\"skipped\":6}"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
{
|
||||
"status": 200,
|
||||
"body": {
|
||||
"data": {
|
||||
"inserted": 1,
|
||||
"updated": 2,
|
||||
"skipped": 1,
|
||||
"total": 4,
|
||||
"dryRun": false,
|
||||
"rows": [
|
||||
{
|
||||
"line": 2,
|
||||
"status": "skipped",
|
||||
"reason": "duplicate"
|
||||
},
|
||||
{
|
||||
"line": 3,
|
||||
"status": "updated"
|
||||
},
|
||||
{
|
||||
"line": 4,
|
||||
"status": "updated"
|
||||
},
|
||||
{
|
||||
"line": 5,
|
||||
"status": "inserted"
|
||||
}
|
||||
]
|
||||
},
|
||||
"error": null
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"status": 200,
|
||||
"action": {
|
||||
"name": "import.csv.upload"
|
||||
},
|
||||
"meta": {
|
||||
"import_csv_inserted": 1,
|
||||
"import_csv_updated": 2,
|
||||
"import_csv_skipped": 1,
|
||||
"import_csv_dry_run": false
|
||||
},
|
||||
"warnings": null,
|
||||
"error": null
|
||||
}
|
||||
],
|
||||
"effects": [
|
||||
{
|
||||
"effect": "maybeEnqueueMorningRefresh",
|
||||
"args": [
|
||||
"<id:1>",
|
||||
[]
|
||||
]
|
||||
},
|
||||
{
|
||||
"effect": "invalidateStatusInsightsForTypes",
|
||||
"args": [
|
||||
"<id:1>",
|
||||
[
|
||||
"WEIGHT",
|
||||
"SLEEP_DURATION",
|
||||
"PULSE"
|
||||
],
|
||||
{
|
||||
"source": "manual"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"stored": {
|
||||
"measurements": [
|
||||
{
|
||||
"type": "WEIGHT",
|
||||
"value": 82,
|
||||
"unit": "kg",
|
||||
"source": "IMPORT",
|
||||
"externalId": "exp-w-2",
|
||||
"measuredAt": "2026-10-09T06:30:00.000Z",
|
||||
"notes": null,
|
||||
"notesEncrypted": null,
|
||||
"glucoseContext": null,
|
||||
"deletedAt": null
|
||||
},
|
||||
{
|
||||
"type": "WEIGHT",
|
||||
"value": 81.4,
|
||||
"unit": "kg",
|
||||
"source": "IMPORT",
|
||||
"externalId": "exp-w-1",
|
||||
"measuredAt": "2026-10-10T06:30:00.000Z",
|
||||
"notes": null,
|
||||
"notesEncrypted": "<bytes:59>",
|
||||
"glucoseContext": null,
|
||||
"deletedAt": null
|
||||
},
|
||||
{
|
||||
"type": "PULSE",
|
||||
"value": 64,
|
||||
"unit": "bpm",
|
||||
"source": "IMPORT",
|
||||
"externalId": null,
|
||||
"measuredAt": "2026-10-12T07:00:00.000Z",
|
||||
"notes": null,
|
||||
"notesEncrypted": null,
|
||||
"glucoseContext": null,
|
||||
"deletedAt": null
|
||||
},
|
||||
{
|
||||
"type": "PULSE",
|
||||
"value": 66,
|
||||
"unit": "bpm",
|
||||
"source": "IMPORT",
|
||||
"externalId": null,
|
||||
"measuredAt": "2026-10-13T07:00:00.000Z",
|
||||
"notes": null,
|
||||
"notesEncrypted": null,
|
||||
"glucoseContext": null,
|
||||
"deletedAt": null
|
||||
},
|
||||
{
|
||||
"type": "SLEEP_DURATION",
|
||||
"value": 430,
|
||||
"unit": "minutes",
|
||||
"source": "IMPORT",
|
||||
"externalId": "exp-s-1",
|
||||
"measuredAt": "2026-10-12T06:00:00.000Z",
|
||||
"notes": null,
|
||||
"notesEncrypted": null,
|
||||
"glucoseContext": null,
|
||||
"deletedAt": null
|
||||
},
|
||||
{
|
||||
"type": "BLOOD_GLUCOSE",
|
||||
"value": 104,
|
||||
"unit": "mg/dL",
|
||||
"source": "IMPORT",
|
||||
"externalId": null,
|
||||
"measuredAt": "2026-10-11T08:00:00.000Z",
|
||||
"notes": null,
|
||||
"notesEncrypted": null,
|
||||
"glucoseContext": "FASTING",
|
||||
"deletedAt": null
|
||||
}
|
||||
],
|
||||
"rollups": [
|
||||
{
|
||||
"type": "WEIGHT",
|
||||
"bucketStart": "2026-10-09T00:00:00.000Z",
|
||||
"count": 1,
|
||||
"mean": 82
|
||||
},
|
||||
{
|
||||
"type": "WEIGHT",
|
||||
"bucketStart": "2026-10-10T00:00:00.000Z",
|
||||
"count": 1,
|
||||
"mean": 81.4
|
||||
},
|
||||
{
|
||||
"type": "PULSE",
|
||||
"bucketStart": "2026-10-12T00:00:00.000Z",
|
||||
"count": 1,
|
||||
"mean": 64
|
||||
},
|
||||
{
|
||||
"type": "PULSE",
|
||||
"bucketStart": "2026-10-13T00:00:00.000Z",
|
||||
"count": 1,
|
||||
"mean": 66
|
||||
},
|
||||
{
|
||||
"type": "SLEEP_DURATION",
|
||||
"bucketStart": "2026-10-12T00:00:00.000Z",
|
||||
"count": 1,
|
||||
"mean": 430
|
||||
},
|
||||
{
|
||||
"type": "BLOOD_GLUCOSE",
|
||||
"bucketStart": "2026-10-11T00:00:00.000Z",
|
||||
"count": 1,
|
||||
"mean": 104
|
||||
}
|
||||
],
|
||||
"audit": [
|
||||
{
|
||||
"action": "import.csv.upload",
|
||||
"details": "{\"inserted\":5,\"updated\":1,\"skipped\":2}"
|
||||
},
|
||||
{
|
||||
"action": "import.csv.upload",
|
||||
"details": "{\"inserted\":0,\"updated\":2,\"skipped\":6}"
|
||||
},
|
||||
{
|
||||
"action": "import.csv.upload",
|
||||
"details": "{\"inserted\":1,\"updated\":2,\"skipped\":1}"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
{
|
||||
"status": 422,
|
||||
"body": {
|
||||
"data": null,
|
||||
"error": "Validation failed",
|
||||
"details": {
|
||||
"issues": [
|
||||
{
|
||||
"path": "",
|
||||
"code": "unrecognized_keys",
|
||||
"message": "Unrecognized key: \"dryrun\"",
|
||||
"keys": [
|
||||
"dryrun"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"meta": {
|
||||
"errorCode": "http.invalid",
|
||||
"requestId": "<requestId>"
|
||||
}
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"status": 422,
|
||||
"action": {
|
||||
"name": "import.csv.upload"
|
||||
},
|
||||
"meta": null,
|
||||
"warnings": null,
|
||||
"error": null
|
||||
}
|
||||
],
|
||||
"effects": [],
|
||||
"stored": {
|
||||
"measurements": [
|
||||
{
|
||||
"type": "WEIGHT",
|
||||
"value": 82,
|
||||
"unit": "kg",
|
||||
"source": "IMPORT",
|
||||
"externalId": "exp-w-2",
|
||||
"measuredAt": "2026-10-09T06:30:00.000Z",
|
||||
"notes": null,
|
||||
"notesEncrypted": null,
|
||||
"glucoseContext": null,
|
||||
"deletedAt": null
|
||||
},
|
||||
{
|
||||
"type": "WEIGHT",
|
||||
"value": 81.4,
|
||||
"unit": "kg",
|
||||
"source": "IMPORT",
|
||||
"externalId": "exp-w-1",
|
||||
"measuredAt": "2026-10-10T06:30:00.000Z",
|
||||
"notes": null,
|
||||
"notesEncrypted": "<bytes:59>",
|
||||
"glucoseContext": null,
|
||||
"deletedAt": null
|
||||
},
|
||||
{
|
||||
"type": "PULSE",
|
||||
"value": 64,
|
||||
"unit": "bpm",
|
||||
"source": "IMPORT",
|
||||
"externalId": null,
|
||||
"measuredAt": "2026-10-12T07:00:00.000Z",
|
||||
"notes": null,
|
||||
"notesEncrypted": null,
|
||||
"glucoseContext": null,
|
||||
"deletedAt": null
|
||||
},
|
||||
{
|
||||
"type": "PULSE",
|
||||
"value": 66,
|
||||
"unit": "bpm",
|
||||
"source": "IMPORT",
|
||||
"externalId": null,
|
||||
"measuredAt": "2026-10-13T07:00:00.000Z",
|
||||
"notes": null,
|
||||
"notesEncrypted": null,
|
||||
"glucoseContext": null,
|
||||
"deletedAt": null
|
||||
},
|
||||
{
|
||||
"type": "SLEEP_DURATION",
|
||||
"value": 430,
|
||||
"unit": "minutes",
|
||||
"source": "IMPORT",
|
||||
"externalId": "exp-s-1",
|
||||
"measuredAt": "2026-10-12T06:00:00.000Z",
|
||||
"notes": null,
|
||||
"notesEncrypted": null,
|
||||
"glucoseContext": null,
|
||||
"deletedAt": null
|
||||
},
|
||||
{
|
||||
"type": "BLOOD_GLUCOSE",
|
||||
"value": 104,
|
||||
"unit": "mg/dL",
|
||||
"source": "IMPORT",
|
||||
"externalId": null,
|
||||
"measuredAt": "2026-10-11T08:00:00.000Z",
|
||||
"notes": null,
|
||||
"notesEncrypted": null,
|
||||
"glucoseContext": "FASTING",
|
||||
"deletedAt": null
|
||||
}
|
||||
],
|
||||
"rollups": [
|
||||
{
|
||||
"type": "WEIGHT",
|
||||
"bucketStart": "2026-10-09T00:00:00.000Z",
|
||||
"count": 1,
|
||||
"mean": 82
|
||||
},
|
||||
{
|
||||
"type": "WEIGHT",
|
||||
"bucketStart": "2026-10-10T00:00:00.000Z",
|
||||
"count": 1,
|
||||
"mean": 81.4
|
||||
},
|
||||
{
|
||||
"type": "PULSE",
|
||||
"bucketStart": "2026-10-12T00:00:00.000Z",
|
||||
"count": 1,
|
||||
"mean": 64
|
||||
},
|
||||
{
|
||||
"type": "PULSE",
|
||||
"bucketStart": "2026-10-13T00:00:00.000Z",
|
||||
"count": 1,
|
||||
"mean": 66
|
||||
},
|
||||
{
|
||||
"type": "SLEEP_DURATION",
|
||||
"bucketStart": "2026-10-12T00:00:00.000Z",
|
||||
"count": 1,
|
||||
"mean": 430
|
||||
},
|
||||
{
|
||||
"type": "BLOOD_GLUCOSE",
|
||||
"bucketStart": "2026-10-11T00:00:00.000Z",
|
||||
"count": 1,
|
||||
"mean": 104
|
||||
}
|
||||
],
|
||||
"audit": [
|
||||
{
|
||||
"action": "import.csv.upload",
|
||||
"details": "{\"inserted\":5,\"updated\":1,\"skipped\":2}"
|
||||
},
|
||||
{
|
||||
"action": "import.csv.upload",
|
||||
"details": "{\"inserted\":0,\"updated\":2,\"skipped\":6}"
|
||||
},
|
||||
{
|
||||
"action": "import.csv.upload",
|
||||
"details": "{\"inserted\":1,\"updated\":2,\"skipped\":1}"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
{
|
||||
"status": 422,
|
||||
"body": {
|
||||
"data": null,
|
||||
"error": "Missing required column(s): type, value, unit, measuredat. Required header: type,value,unit,measuredat[,glucosecontext,notes,externalid].",
|
||||
"meta": {
|
||||
"errorCode": "http.invalid",
|
||||
"requestId": "<requestId>"
|
||||
}
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"status": 422,
|
||||
"action": {
|
||||
"name": "import.csv.upload"
|
||||
},
|
||||
"meta": null,
|
||||
"warnings": null,
|
||||
"error": null
|
||||
}
|
||||
],
|
||||
"effects": [],
|
||||
"stored": {
|
||||
"measurements": [
|
||||
{
|
||||
"type": "WEIGHT",
|
||||
"value": 82,
|
||||
"unit": "kg",
|
||||
"source": "IMPORT",
|
||||
"externalId": "exp-w-2",
|
||||
"measuredAt": "2026-10-09T06:30:00.000Z",
|
||||
"notes": null,
|
||||
"notesEncrypted": null,
|
||||
"glucoseContext": null,
|
||||
"deletedAt": null
|
||||
},
|
||||
{
|
||||
"type": "WEIGHT",
|
||||
"value": 81.4,
|
||||
"unit": "kg",
|
||||
"source": "IMPORT",
|
||||
"externalId": "exp-w-1",
|
||||
"measuredAt": "2026-10-10T06:30:00.000Z",
|
||||
"notes": null,
|
||||
"notesEncrypted": "<bytes:59>",
|
||||
"glucoseContext": null,
|
||||
"deletedAt": null
|
||||
},
|
||||
{
|
||||
"type": "PULSE",
|
||||
"value": 64,
|
||||
"unit": "bpm",
|
||||
"source": "IMPORT",
|
||||
"externalId": null,
|
||||
"measuredAt": "2026-10-12T07:00:00.000Z",
|
||||
"notes": null,
|
||||
"notesEncrypted": null,
|
||||
"glucoseContext": null,
|
||||
"deletedAt": null
|
||||
},
|
||||
{
|
||||
"type": "PULSE",
|
||||
"value": 66,
|
||||
"unit": "bpm",
|
||||
"source": "IMPORT",
|
||||
"externalId": null,
|
||||
"measuredAt": "2026-10-13T07:00:00.000Z",
|
||||
"notes": null,
|
||||
"notesEncrypted": null,
|
||||
"glucoseContext": null,
|
||||
"deletedAt": null
|
||||
},
|
||||
{
|
||||
"type": "SLEEP_DURATION",
|
||||
"value": 430,
|
||||
"unit": "minutes",
|
||||
"source": "IMPORT",
|
||||
"externalId": "exp-s-1",
|
||||
"measuredAt": "2026-10-12T06:00:00.000Z",
|
||||
"notes": null,
|
||||
"notesEncrypted": null,
|
||||
"glucoseContext": null,
|
||||
"deletedAt": null
|
||||
},
|
||||
{
|
||||
"type": "BLOOD_GLUCOSE",
|
||||
"value": 104,
|
||||
"unit": "mg/dL",
|
||||
"source": "IMPORT",
|
||||
"externalId": null,
|
||||
"measuredAt": "2026-10-11T08:00:00.000Z",
|
||||
"notes": null,
|
||||
"notesEncrypted": null,
|
||||
"glucoseContext": "FASTING",
|
||||
"deletedAt": null
|
||||
}
|
||||
],
|
||||
"rollups": [
|
||||
{
|
||||
"type": "WEIGHT",
|
||||
"bucketStart": "2026-10-09T00:00:00.000Z",
|
||||
"count": 1,
|
||||
"mean": 82
|
||||
},
|
||||
{
|
||||
"type": "WEIGHT",
|
||||
"bucketStart": "2026-10-10T00:00:00.000Z",
|
||||
"count": 1,
|
||||
"mean": 81.4
|
||||
},
|
||||
{
|
||||
"type": "PULSE",
|
||||
"bucketStart": "2026-10-12T00:00:00.000Z",
|
||||
"count": 1,
|
||||
"mean": 64
|
||||
},
|
||||
{
|
||||
"type": "PULSE",
|
||||
"bucketStart": "2026-10-13T00:00:00.000Z",
|
||||
"count": 1,
|
||||
"mean": 66
|
||||
},
|
||||
{
|
||||
"type": "SLEEP_DURATION",
|
||||
"bucketStart": "2026-10-12T00:00:00.000Z",
|
||||
"count": 1,
|
||||
"mean": 430
|
||||
},
|
||||
{
|
||||
"type": "BLOOD_GLUCOSE",
|
||||
"bucketStart": "2026-10-11T00:00:00.000Z",
|
||||
"count": 1,
|
||||
"mean": 104
|
||||
}
|
||||
],
|
||||
"audit": [
|
||||
{
|
||||
"action": "import.csv.upload",
|
||||
"details": "{\"inserted\":5,\"updated\":1,\"skipped\":2}"
|
||||
},
|
||||
{
|
||||
"action": "import.csv.upload",
|
||||
"details": "{\"inserted\":0,\"updated\":2,\"skipped\":6}"
|
||||
},
|
||||
{
|
||||
"action": "import.csv.upload",
|
||||
"details": "{\"inserted\":1,\"updated\":2,\"skipped\":1}"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* Parity for `POST /api/import/csv` while its batched writer moves into
|
||||
* `src/lib/import/`.
|
||||
*
|
||||
* A dry run of a mixed file (rows with and without an external id, a key
|
||||
* repeated inside the file, a natural-key twin, a row the parser refuses),
|
||||
* the same file for real, the same file again, a changed file that moves a
|
||||
* reading to another day and re-imports a deleted row, and the refusals (an
|
||||
* unknown parameter, a file without a usable header). After each request
|
||||
* the snapshot holds the response, the wide event, the recorded arrival and
|
||||
* refresh tails, and the stored measurements, rollups and audit rows. The
|
||||
* shared import bucket is emptied before each request so the walk never
|
||||
* meets its own rate limit.
|
||||
*/
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { POST } from "@/app/api/import/csv/route";
|
||||
|
||||
import { callRoute, type RouteAuth } from "../../../kit/call-route";
|
||||
import { makeSession, makeUser } from "../../../kit/factories";
|
||||
import {
|
||||
effectRecorder,
|
||||
eventFacts,
|
||||
expectParity,
|
||||
stable,
|
||||
wideEventRecorder,
|
||||
} from "../../../kit/parity";
|
||||
import { getPrismaClient, truncateAllTables } from "../../setup";
|
||||
|
||||
vi.mock("@/lib/logging/transports", async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import("@/lib/logging/transports")>();
|
||||
const { wideEventRecorder: recorder } = await import("../../../kit/parity");
|
||||
return {
|
||||
...actual,
|
||||
emitIfSampled: (event: (typeof recorder.events)[number]) => {
|
||||
recorder.events.push(event);
|
||||
},
|
||||
};
|
||||
});
|
||||
vi.mock("@/lib/arrivals/emit-shared", async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import("@/lib/arrivals/emit-shared")>();
|
||||
const { effectRecorder } = await import("../../../kit/parity");
|
||||
return {
|
||||
...actual,
|
||||
emitDataArrival: async (...args: unknown[]) => {
|
||||
effectRecorder.record("emitDataArrival", args);
|
||||
},
|
||||
};
|
||||
});
|
||||
vi.mock("@/lib/daily/morning-refresh-trigger", async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<
|
||||
typeof import("@/lib/daily/morning-refresh-trigger")
|
||||
>();
|
||||
const { effectRecorder } = await import("../../../kit/parity");
|
||||
return {
|
||||
...actual,
|
||||
maybeEnqueueMorningRefresh: async (...args: unknown[]) => {
|
||||
effectRecorder.record("maybeEnqueueMorningRefresh", args);
|
||||
},
|
||||
};
|
||||
});
|
||||
vi.mock("@/lib/jobs/pr-detection", async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import("@/lib/jobs/pr-detection")>();
|
||||
const { effectRecorder } = await import("../../../kit/parity");
|
||||
return {
|
||||
...actual,
|
||||
enqueuePrDetection: async (...args: unknown[]) => {
|
||||
effectRecorder.record("enqueuePrDetection", args);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/lib/insights/status-invalidation", async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import("@/lib/insights/status-invalidation")>();
|
||||
const { effectRecorder } = await import("../../../kit/parity");
|
||||
return {
|
||||
...actual,
|
||||
// Detached from the request but inside its context: run for real, its
|
||||
// annotate can land on the request's wide event or miss it, depending on
|
||||
// timing. Recorded instead, like the other tails.
|
||||
invalidateStatusInsightsForTypes: async (...args: unknown[]) => {
|
||||
effectRecorder.record("invalidateStatusInsightsForTypes", args);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const prisma = getPrismaClient();
|
||||
const NOW = new Date("2026-10-15T12:00:00.000Z");
|
||||
const HEADER = "type,value,unit,measuredAt,glucoseContext,notes,externalId";
|
||||
|
||||
let userId: string;
|
||||
let owner: RouteAuth;
|
||||
|
||||
async function stored() {
|
||||
const [measurements, rollups, audit] = await Promise.all([
|
||||
prisma.measurement.findMany({
|
||||
where: { userId },
|
||||
orderBy: [{ type: "asc" }, { measuredAt: "asc" }, { value: "asc" }],
|
||||
select: {
|
||||
type: true,
|
||||
value: true,
|
||||
unit: true,
|
||||
source: true,
|
||||
externalId: true,
|
||||
measuredAt: true,
|
||||
notes: true,
|
||||
notesEncrypted: true,
|
||||
glucoseContext: true,
|
||||
deletedAt: true,
|
||||
},
|
||||
}),
|
||||
prisma.measurementRollup.findMany({
|
||||
where: { userId, granularity: "DAY" },
|
||||
orderBy: [{ type: "asc" }, { bucketStart: "asc" }],
|
||||
select: { type: true, bucketStart: true, count: true, mean: true },
|
||||
}),
|
||||
prisma.auditLog.findMany({
|
||||
where: { userId },
|
||||
orderBy: { createdAt: "asc" },
|
||||
select: { action: true, details: true },
|
||||
}),
|
||||
]);
|
||||
return { measurements, rollups, audit };
|
||||
}
|
||||
|
||||
async function step(
|
||||
name: string,
|
||||
lines: string[],
|
||||
query: Record<string, string> = {},
|
||||
): Promise<void> {
|
||||
await prisma.rateLimit.deleteMany({ where: { key: `import:${userId}` } });
|
||||
wideEventRecorder.take();
|
||||
effectRecorder.take();
|
||||
const res = await callRoute(POST, {
|
||||
method: "POST",
|
||||
route: "/api/import/csv",
|
||||
query,
|
||||
text: lines.join("\n"),
|
||||
headers: { "content-type": "text/csv" },
|
||||
auth: owner,
|
||||
});
|
||||
const events = wideEventRecorder.take();
|
||||
expect(events.length).toBeGreaterThan(0);
|
||||
await expectParity(
|
||||
`import-csv.${name}`,
|
||||
stable(
|
||||
{
|
||||
status: res.status,
|
||||
body: res.body,
|
||||
events: events.map(eventFacts),
|
||||
effects: effectRecorder.take(),
|
||||
stored: await stored(),
|
||||
},
|
||||
{ since: NOW },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const FILE = [
|
||||
HEADER,
|
||||
"WEIGHT,81.4,kg,2026-10-10T06:30:00.000Z,,after the run,exp-w-1",
|
||||
"WEIGHT,81.9,kg,2026-10-11T06:30:00.000Z,,,exp-w-2",
|
||||
// The same key again later in the file: the last value wins.
|
||||
"WEIGHT,82.0,kg,2026-10-11T06:30:00.000Z,,,exp-w-2",
|
||||
"BLOOD_GLUCOSE,104,mg/dL,2026-10-11T08:00:00.000Z,FASTING,,",
|
||||
// A natural-key twin of the line above.
|
||||
"BLOOD_GLUCOSE,104,mg/dL,2026-10-11T08:00:00.000Z,FASTING,,",
|
||||
"SLEEP_DURATION,412,minutes,2026-10-12T06:00:00.000Z,,,exp-s-1",
|
||||
"PULSE,64,bpm,2026-10-12T07:00:00.000Z,,,",
|
||||
// Refused by the parser.
|
||||
"WEIGHT,not-a-number,kg,2026-10-12T06:30:00.000Z,,,",
|
||||
];
|
||||
|
||||
const CHANGED = [
|
||||
HEADER,
|
||||
// Unchanged.
|
||||
"WEIGHT,81.4,kg,2026-10-10T06:30:00.000Z,,after the run,exp-w-1",
|
||||
// Moved to another day.
|
||||
"WEIGHT,82.0,kg,2026-10-09T06:30:00.000Z,,,exp-w-2",
|
||||
// Deleted since the last import; re-importing brings it back.
|
||||
"SLEEP_DURATION,430,minutes,2026-10-12T06:00:00.000Z,,,exp-s-1",
|
||||
"PULSE,66,bpm,2026-10-13T07:00:00.000Z,,,",
|
||||
];
|
||||
|
||||
beforeAll(async () => {
|
||||
vi.useFakeTimers({ toFake: ["Date"], now: NOW });
|
||||
await truncateAllTables(prisma);
|
||||
const user = await makeUser({ timezone: "Europe/Berlin" });
|
||||
userId = user.id;
|
||||
owner = await makeSession(user);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe("POST /api/import/csv parity", () => {
|
||||
it("01 a dry run", async () => {
|
||||
await step("01-dry-run", FILE, { dryRun: "1" });
|
||||
});
|
||||
|
||||
it("02 the file for real, then again", async () => {
|
||||
await step("02-first-import", FILE);
|
||||
await step("03-same-file", FILE);
|
||||
});
|
||||
|
||||
it("04 a changed file over a deleted row", async () => {
|
||||
await prisma.measurement.updateMany({
|
||||
where: { userId, externalId: "exp-s-1" },
|
||||
data: { deletedAt: new Date("2026-10-13T08:00:00.000Z") },
|
||||
});
|
||||
await step("04-changed-file", CHANGED);
|
||||
});
|
||||
|
||||
it("05 the refusals", async () => {
|
||||
await step("05-unknown-parameter", FILE, { dryrun: "1" });
|
||||
await step("06-no-header", ["just,some,words"]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user