package handler import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "strings" "testing" "chis_osi/pipeline" "chis_osi/source" ) func TestHealthRecordUpsertReturnsStructuredOutcome(t *testing.T) { tests := []struct { name string outcome pipeline.Outcome wantStatus int }{ {name: "create", outcome: pipeline.Outcome{Status: pipeline.StatusDone, Action: pipeline.ActionCreate, ServiceID: "JKDA00001", ResponseCode: "01", PHRIDHint: "****0001"}, wantStatus: http.StatusOK}, {name: "update", outcome: pipeline.Outcome{Status: pipeline.StatusDone, Action: pipeline.ActionUpdate, ServiceID: "JKDA00003", ResponseCode: "01", PHRIDHint: "****0001"}, wantStatus: http.StatusOK}, {name: "manual review", outcome: pipeline.Outcome{Status: pipeline.StatusManualReview, Action: pipeline.ActionNone, Reason: "multiple CHIS health records found"}, wantStatus: http.StatusConflict}, {name: "retry", outcome: pipeline.Outcome{Status: pipeline.StatusRetry, Action: pipeline.ActionNone, Reason: "CHIS query failed"}, wantStatus: http.StatusServiceUnavailable}, {name: "validation failed", outcome: pipeline.Outcome{Status: pipeline.StatusFailed, Action: pipeline.ActionNone, Reason: "mapping validation failed: idCard"}, wantStatus: http.StatusUnprocessableEntity}, {name: "upstream failed", outcome: pipeline.Outcome{Status: pipeline.StatusFailed, Action: pipeline.ActionCreate, Reason: "CHIS create failed", ServiceID: "JKDA00001"}, wantStatus: http.StatusBadGateway}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { upserter := &fakeHealthRecordUpserter{outcome: tt.outcome} h := NewHealthRecordUpsertHandler(upserter) req := httptest.NewRequest(http.MethodPost, "/api/health-record/upsert", strings.NewReader(testPHISEnvelope())) rec := httptest.NewRecorder() h.Upsert(rec, req) if rec.Code != tt.wantStatus { t.Fatalf("status = %d, want %d; body=%s", rec.Code, tt.wantStatus, rec.Body.String()) } var got pipeline.Outcome if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil { t.Fatalf("decode outcome: %v; body=%s", err, rec.Body.String()) } if got.Status != tt.outcome.Status || got.Action != tt.outcome.Action || upserter.task.ArchID != "ARCH-HTTP-1" { t.Fatalf("outcome=%#v task=%#v", got, upserter.task) } }) } } func TestHealthRecordUpsertHidesInternalError(t *testing.T) { h := NewHealthRecordUpsertHandler(&fakeHealthRecordUpserter{err: errors.New("proxy secret detail")}) req := httptest.NewRequest(http.MethodPost, "/api/health-record/upsert", strings.NewReader(testPHISEnvelope())) rec := httptest.NewRecorder() h.Upsert(rec, req) if rec.Code != http.StatusBadGateway || strings.Contains(rec.Body.String(), "secret") { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } } func TestHealthRecordUpsertPreservesDoneOutcomeOnFinalizationError(t *testing.T) { h := NewHealthRecordUpsertHandler(&fakeHealthRecordUpserter{ outcome: pipeline.Outcome{Status: pipeline.StatusDone, Action: pipeline.ActionCreate, ServiceID: "JKDA00001", ResponseCode: "01"}, err: errors.New("idempotency complete failed"), }) req := httptest.NewRequest(http.MethodPost, "/api/health-record/upsert", strings.NewReader(testPHISEnvelope())) rec := httptest.NewRecorder() h.Upsert(rec, req) if rec.Code != http.StatusBadGateway || !strings.Contains(rec.Body.String(), `"retrySafe":false`) || !strings.Contains(rec.Body.String(), `"status":"done"`) { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } } func TestHealthRecordUpsertRejectsInvalidEnvelopeAndMethod(t *testing.T) { t.Run("invalid envelope", func(t *testing.T) { h := NewHealthRecordUpsertHandler(&fakeHealthRecordUpserter{}) req := httptest.NewRequest(http.MethodPost, "/api/health-record/upsert", strings.NewReader(`{"code":500,"msg":"failed"}`)) rec := httptest.NewRecorder() h.Upsert(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } }) t.Run("method", func(t *testing.T) { h := NewHealthRecordUpsertHandler(nil) req := httptest.NewRequest(http.MethodGet, "/api/health-record/upsert", nil) rec := httptest.NewRecorder() h.Upsert(rec, req) if rec.Code != http.StatusMethodNotAllowed || rec.Header().Get("Allow") != http.MethodPost { t.Fatalf("status=%d allow=%q", rec.Code, rec.Header().Get("Allow")) } }) } type fakeHealthRecordUpserter struct { outcome pipeline.Outcome err error task source.HealthRecordTask } func (f *fakeHealthRecordUpserter) Upsert(_ context.Context, task source.HealthRecordTask) (pipeline.Outcome, error) { f.task = task return f.outcome, f.err } func testPHISEnvelope() string { return `{"code":200,"msg":"ok","compress":false,"data":{"archId":"ARCH-HTTP-1","businessId":"BUS-HTTP-1","record":{"idCard":"440000********1234"}}}` }