Add comprehensive test suite: labs, entries, dossiers, readings
Test coverage: - lib/lib_test.go (36 test functions): - Crypto: Pack/Unpack, CryptoEncrypt/Decrypt, deterministic encryption - Tokens: Create, Parse, expiration handling, invalid token handling - Lab references: LabScale conversions, MakeRefID, LabRefLookup with sex/age matching - Categories: FromString, CategoryName, CategoryKey, Categories() list - IDs: NewID uniqueness, FormatID/ParseID roundtrip - Entry CRUD: Write, Read, Delete, filter by type, filter by date range - Trackers: Add, Query, Respond, Dismiss - RBAC: CheckAccess for own dossier, system user, granted access, no access - Dossier helpers: DossierFromEntry, SexKey - Normalize: normalizeKey test name cleaning - JSON marshaling: Tracker data, DossierPreferences - api/api_test.go (23 test functions): - Helper functions: intPtr, getString, seriesPriority - v2 helpers: deterministicID, metricLabel - Version endpoint - Error/JSON responses - Request/Response JSON marshaling: Entry, Dossier, Tracker, Parse, Readings - Method handling: token endpoint - Categories endpoint - isLocalhost detection - Schedule calculation All tests use in-memory SQLite for isolation. No external dependencies required.
This commit is contained in:
parent
93643d285b
commit
155d24ec2e
|
|
@ -0,0 +1,594 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue