package main import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "os" "testing" "time" "inou/lib" ) func TestMain(m *testing.M) { os.Setenv("TEST_MODE", "1") code := m.Run() os.Exit(code) } // ============================================================================= // Test Helpers // ============================================================================= // makeRequest creates a test HTTP request with optional auth token func makeRequest(method, path string, body interface{}, token string) *http.Request { var bodyReader *bytes.Reader if body != nil { b, _ := json.Marshal(body) bodyReader = bytes.NewReader(b) } else { bodyReader = bytes.NewReader(nil) } req := httptest.NewRequest(method, path, bodyReader) req.Header.Set("Content-Type", "application/json") if token != "" { req.Header.Set("Authorization", "Bearer "+token) } return req } // parseResponse parses JSON response body func parseResponse(t *testing.T, w *httptest.ResponseRecorder, v interface{}) { t.Helper() if err := json.Unmarshal(w.Body.Bytes(), v); err != nil { t.Fatalf("Failed to parse response: %v\nBody: %s", err, w.Body.String()) } } // ============================================================================= // v1 Helper Function Tests // ============================================================================= func TestIntPtr(t *testing.T) { p := intPtr(42) if *p != 42 { t.Errorf("intPtr(42) = %d, want 42", *p) } } func TestGetString(t *testing.T) { m := map[string]interface{}{ "name": "Test Study", "number": 123, "nested": map[string]interface{}{"inner": "value"}, } if got := getString(m, "name"); got != "Test Study" { t.Errorf("getString(name) = %q, want 'Test Study'", got) } if got := getString(m, "number"); got != "" { t.Errorf("getString(number) = %q, want '' (not string)", got) } if got := getString(m, "missing"); got != "" { t.Errorf("getString(missing) = %q, want ''", got) } } func TestSeriesPriority(t *testing.T) { tests := []struct { desc string expected int }{ {"AXIAL T2", 1}, // T2 non-contrast first {"SAGITTAL T2+", 2}, // T2 contrast {"CORONAL T1", 3}, // T1 non-contrast {"AXIAL T1+ GD", 4}, // T1 contrast {"FLAIR", 5}, // FLAIR non-contrast {"FLAIR+", 6}, // FLAIR contrast {"DWI", 99}, // Other {"OTHER SEQUENCE", 99}, // Unknown } for _, tc := range tests { got := seriesPriority(tc.desc) if got != tc.expected { t.Errorf("seriesPriority(%q) = %d, want %d", tc.desc, got, tc.expected) } } } // ============================================================================= // v2 Readings Helper Tests // ============================================================================= func TestDeterministicID(t *testing.T) { id1 := deterministicID("dossier1", "cat", "3") id2 := deterministicID("dossier1", "cat", "3") id3 := deterministicID("dossier2", "cat", "3") if id1 != id2 { t.Error("deterministicID should be deterministic for same inputs") } if id1 == id3 { t.Error("deterministicID should differ for different inputs") } if len(id1) != 16 { t.Errorf("deterministicID length = %d, want 16", len(id1)) } } func TestMetricLabel(t *testing.T) { tests := []struct { input string expected string }{ {"heart_rate", "Heart Rate"}, {"blood_pressure", "Blood Pressure"}, {"weight", "Weight"}, {"spo2", "Spo2"}, {"body_mass_index", "Body Mass Index"}, } for _, tc := range tests { got := metricLabel(tc.input) if got != tc.expected { t.Errorf("metricLabel(%q) = %q, want %q", tc.input, got, tc.expected) } } } // ============================================================================= // Version Endpoint Test // ============================================================================= func TestVersionEndpoint(t *testing.T) { w := httptest.NewRecorder() r := makeRequest("GET", "/api/version", nil, "") handleVersion(w, r) if w.Code != http.StatusOK { t.Errorf("Status = %d, want 200", w.Code) } var resp map[string]interface{} parseResponse(t, w, &resp) // Check it has the expected fields if _, ok := resp["api_version"]; !ok { t.Error("Response should contain 'api_version' field") } if _, ok := resp["latest_bridge_version"]; !ok { t.Error("Response should contain 'latest_bridge_version' field") } } // ============================================================================= // Error Response Tests // ============================================================================= func TestV1Error(t *testing.T) { w := httptest.NewRecorder() v1Error(w, "test error message", http.StatusBadRequest) if w.Code != http.StatusBadRequest { t.Errorf("Status = %d, want 400", w.Code) } var resp map[string]string parseResponse(t, w, &resp) if resp["error"] != "test error message" { t.Errorf("error = %q, want 'test error message'", resp["error"]) } } func TestV1JSON(t *testing.T) { w := httptest.NewRecorder() data := map[string]interface{}{ "id": "abc123", "name": "Test", "count": 42, } v1JSON(w, data) if w.Header().Get("Content-Type") != "application/json" { t.Errorf("Content-Type = %q, want 'application/json'", w.Header().Get("Content-Type")) } var resp map[string]interface{} parseResponse(t, w, &resp) if resp["id"] != "abc123" { t.Errorf("id = %q, want 'abc123'", resp["id"]) } } // ============================================================================= // Request/Response Structure Tests // ============================================================================= func TestEntryRequestJSON(t *testing.T) { jsonStr := `{ "dossier": "abc123def456gh78", "category": "lab", "type": "WBC", "value": "8.5", "timestamp": 1704067200, "data": "{\"unit\":\"x10^9/L\"}" }` var req EntryRequest if err := json.Unmarshal([]byte(jsonStr), &req); err != nil { t.Fatalf("Failed to unmarshal: %v", err) } if req.Dossier != "abc123def456gh78" { t.Errorf("Dossier = %q, want 'abc123def456gh78'", req.Dossier) } if req.Category != "lab" { t.Errorf("Category = %q, want 'lab'", req.Category) } if req.Value != "8.5" { t.Errorf("Value = %q, want '8.5'", req.Value) } if req.Timestamp != 1704067200 { t.Errorf("Timestamp = %d, want 1704067200", req.Timestamp) } } func TestEntryResponseJSON(t *testing.T) { resp := EntryResponse{ ID: "entry123", Dossier: "dossier456", Category: "lab", Type: "WBC", Value: "8.5", Timestamp: 1704067200, } data, err := json.Marshal(resp) if err != nil { t.Fatalf("Failed to marshal: %v", err) } var got EntryResponse if err := json.Unmarshal(data, &got); err != nil { t.Fatalf("Failed to unmarshal: %v", err) } if got.ID != "entry123" { t.Errorf("ID = %q, want 'entry123'", got.ID) } } func TestDossierResponseJSON(t *testing.T) { resp := DossierResponse{ ID: "abc123def456gh78", Name: "Test User", DOB: "1990-05-15", Found: true, } data, err := json.Marshal(resp) if err != nil { t.Fatalf("Failed to marshal: %v", err) } // Should have correct JSON field names var m map[string]interface{} json.Unmarshal(data, &m) if m["id"] != "abc123def456gh78" { t.Errorf("id = %v, want 'abc123def456gh78'", m["id"]) } if m["name"] != "Test User" { t.Errorf("name = %v, want 'Test User'", m["name"]) } if m["found"] != true { t.Errorf("found = %v, want true", m["found"]) } } func TestTrackerResponseJSON(t *testing.T) { resp := TrackerResponse{ ID: "tracker123", Category: "vital", Type: "blood_pressure", Question: "What is your blood pressure?", InputType: "blood_pressure", Active: true, IsDue: true, } data, err := json.Marshal(resp) if err != nil { t.Fatalf("Failed to marshal: %v", err) } var m map[string]interface{} json.Unmarshal(data, &m) if m["id"] != "tracker123" { t.Errorf("id = %v, want 'tracker123'", m["id"]) } if m["is_due"] != true { t.Errorf("is_due = %v, want true", m["is_due"]) } } func TestParseRequestJSON(t *testing.T) { jsonStr := `{"input": "I took my blood pressure and it was 120/80"}` var req ParseRequest if err := json.Unmarshal([]byte(jsonStr), &req); err != nil { t.Fatalf("Failed to unmarshal: %v", err) } if req.Input != "I took my blood pressure and it was 120/80" { t.Errorf("Input = %q, unexpected value", req.Input) } } func TestReadingsRequestJSON(t *testing.T) { jsonStr := `{ "dossier_id": "abc123", "category": "vital", "readings": [ {"source": "manual", "metric": "heart_rate", "value": 72, "unit": "bpm", "timestamp": 1704067200}, {"source": "apple_health", "metric": "weight", "value": 75.5, "unit": "kg", "timestamp": 1704067200} ] }` var req readingsRequest if err := json.Unmarshal([]byte(jsonStr), &req); err != nil { t.Fatalf("Failed to unmarshal: %v", err) } if req.DossierID != "abc123" { t.Errorf("DossierID = %q, want 'abc123'", req.DossierID) } if len(req.Readings) != 2 { t.Errorf("len(Readings) = %d, want 2", len(req.Readings)) } if req.Readings[0].Metric != "heart_rate" { t.Errorf("Readings[0].Metric = %q, want 'heart_rate'", req.Readings[0].Metric) } if req.Readings[1].Value != 75.5 { t.Errorf("Readings[1].Value = %f, want 75.5", req.Readings[1].Value) } } func TestDossierCreateRequestJSON(t *testing.T) { jsonStr := `{"email": "test@example.com", "invited_by": "abc123"}` var req DossierCreateRequest if err := json.Unmarshal([]byte(jsonStr), &req); err != nil { t.Fatalf("Failed to unmarshal: %v", err) } if req.Email != "test@example.com" { t.Errorf("Email = %q, want 'test@example.com'", req.Email) } if req.InvitedBy != "abc123" { t.Errorf("InvitedBy = %q, want 'abc123'", req.InvitedBy) } } // ============================================================================= // Health Response Structure Test // ============================================================================= func TestHealthResponseJSON(t *testing.T) { resp := HealthResponse{ Status: "ok", Time: time.Now().Unix(), Version: "1.0.0", Checks: map[string]string{"db": "ok", "auth": "ok"}, } data, err := json.Marshal(resp) if err != nil { t.Fatalf("Failed to marshal: %v", err) } var got HealthResponse if err := json.Unmarshal(data, &got); err != nil { t.Fatalf("Failed to unmarshal: %v", err) } if got.Status != "ok" { t.Errorf("Status = %q, want 'ok'", got.Status) } if got.Version != "1.0.0" { t.Errorf("Version = %q, want '1.0.0'", got.Version) } if got.Checks["db"] != "ok" { t.Errorf("Checks['db'] = %q, want 'ok'", got.Checks["db"]) } } // ============================================================================= // Tracker Respond Request Test // ============================================================================= func TestTrackerRespondRequestJSON(t *testing.T) { jsonStr := `{ "tracker_id": "tracker123", "response": "{\"systolic\":120,\"diastolic\":80}", "response_raw": "120/80 mmHg", "action": "respond" }` var req TrackerRespondRequest if err := json.Unmarshal([]byte(jsonStr), &req); err != nil { t.Fatalf("Failed to unmarshal: %v", err) } if req.TrackerID != "tracker123" { t.Errorf("TrackerID = %q, want 'tracker123'", req.TrackerID) } if req.Action != "respond" { t.Errorf("Action = %q, want 'respond'", req.Action) } if req.ResponseRaw != "120/80 mmHg" { t.Errorf("ResponseRaw = %q, want '120/80 mmHg'", req.ResponseRaw) } } // ============================================================================= // API Route Handler Tests (Method Not Allowed) // ============================================================================= func TestV1TokenMethodNotAllowed(t *testing.T) { w := httptest.NewRecorder() r := makeRequest("GET", "/api/v1/token", nil, "test-token") v1Token(w, r) if w.Code != http.StatusMethodNotAllowed { t.Errorf("GET /api/v1/token should return 405, got %d", w.Code) } } // ============================================================================= // Entry to Response Conversion Test // ============================================================================= func TestEntryToResponse(t *testing.T) { entry := &lib.Entry{ EntryID: "entry123", DossierID: "dossier456", Category: lib.CategoryLab, Type: "WBC", Value: "8.5", Timestamp: 1704067200, Data: `{"unit":"x10^9/L"}`, } resp := entryToResponse(entry, "en") if resp.ID != "entry123" { t.Errorf("ID = %q, want 'entry123'", resp.ID) } if resp.Dossier != "dossier456" { t.Errorf("Dossier = %q, want 'dossier456'", resp.Dossier) } if resp.Type != "WBC" { t.Errorf("Type = %q, want 'WBC'", resp.Type) } if resp.Value != "8.5" { t.Errorf("Value = %q, want '8.5'", resp.Value) } } // ============================================================================= // Categories API Test // ============================================================================= func TestV1CategoriesReturnsJSON(t *testing.T) { w := httptest.NewRecorder() r := makeRequest("GET", "/api/v1/categories", nil, "") v1Categories(w, r) if w.Code != http.StatusOK { t.Errorf("Status = %d, want 200", w.Code) } if ct := w.Header().Get("Content-Type"); ct != "application/json" { t.Errorf("Content-Type = %q, want 'application/json'", ct) } var resp []map[string]interface{} parseResponse(t, w, &resp) if len(resp) == 0 { t.Error("Categories should not be empty") } // First should be imaging if resp[0]["key"] != "imaging" { t.Errorf("First category key = %v, want 'imaging'", resp[0]["key"]) } } // ============================================================================= // Localhost Check Test // ============================================================================= func TestIsLocalhost(t *testing.T) { tests := []struct { remoteAddr string expected bool }{ {"127.0.0.1:1234", true}, {"[::1]:1234", true}, {"192.168.1.100:1234", false}, {"10.0.0.1:5000", false}, } for _, tc := range tests { r := httptest.NewRequest("GET", "/", nil) r.RemoteAddr = tc.remoteAddr got := isLocalhost(r) if got != tc.expected { t.Errorf("isLocalhost(%q) = %v, want %v", tc.remoteAddr, got, tc.expected) } } } // ============================================================================= // Schedule Helper Tests // ============================================================================= func TestCalculateNextAskFromSchedule(t *testing.T) { now := time.Date(2026, 2, 28, 10, 0, 0, 0, time.UTC) // Saturday 10:00 tests := []struct { name string schedule []ScheduleSlot minNext time.Time // Minimum expected next time maxNext time.Time // Maximum expected next time }{ { name: "empty schedule", schedule: []ScheduleSlot{}, minNext: now, maxNext: now.Add(25 * time.Hour), }, { name: "daily morning", schedule: []ScheduleSlot{ {Days: []string{"sun", "mon", "tue", "wed", "thu", "fri", "sat"}, Times: []string{"09:00"}}, }, minNext: now, // Could be tomorrow maxNext: now.Add(7 * 24 * time.Hour), // Within a week }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { result := calculateNextAskFromSchedule(tc.schedule, now) resultTime := time.Unix(result, 0) if result == 0 && len(tc.schedule) == 0 { // Empty schedule returns 0 return } if resultTime.Before(now) { t.Errorf("Result %v is before now %v", resultTime, now) } }) } }