package api import ( "bytes" "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "os" "strings" "testing" "time" "dana/internal/onec" "dana/internal/storage" ) func TestAppointmentRejectsUnverifiedService(t *testing.T) { a := &API{limiter: newRateLimiter()} in := appointmentInput{DoctorSlug: "fictional-dentist", Start: time.Now().Add(48 * time.Hour).Format(time.RFC3339), Name: "Fictional patient", Phone: "79990000000", Consent: true, RequestID: "fictional-request", ServiceID: "unverified-service"} body, _ := json.Marshal(in) w := httptest.NewRecorder() a.createAppointment(w, httptest.NewRequest(http.MethodPost, "/api/v1/appointments", bytes.NewReader(body))) if w.Code != http.StatusUnprocessableEntity || !strings.Contains(w.Body.String(), `"code":"service_unavailable"`) { t.Fatalf("unverified service was not rejected before storage access: %d %s", w.Code, w.Body.String()) } in.ServiceID = "" body, _ = json.Marshal(in) w = httptest.NewRecorder() a.createAppointment(w, httptest.NewRequest(http.MethodPost, "/api/v1/appointments", bytes.NewReader(body))) if w.Code != http.StatusServiceUnavailable || !strings.Contains(w.Body.String(), `"code":"onec_disabled"`) { t.Fatalf("booking without service must continue to normal availability checks: %d %s", w.Code, w.Body.String()) } } // Uses only a disposable database, never production or a real 1C server. func TestDoctorServicesQuarantine(t *testing.T) { dsn := os.Getenv("DANA_AUTONOMY_TEST_DSN") if dsn == "" { t.Skip("requires disposable autonomy DB") } ctx := context.Background() st, err := storage.New(ctx, dsn) if err != nil { t.Fatal(err) } defer st.Close() if err = st.Migrate(ctx); err != nil { t.Fatal(err) } doctor := fmt.Sprintf("service-guard-%d", time.Now().UnixNano()) var doctorID int64 if err = st.Pool().QueryRow(ctx, `INSERT INTO doctors(slug,last_name,first_name,booking_provider) VALUES($1,'Test','Dentist','native') RETURNING id`, doctor).Scan(&doctorID); err != nil { t.Fatal(err) } if _, err = st.Pool().Exec(ctx, `INSERT INTO onec_doctors(onec_id,full_name,doctor_id) VALUES($1,'Test Dentist',$2)`, doctor, doctorID); err != nil { t.Fatal(err) } if err = st.SaveOnecServices(ctx, doctor, []onec.Service{{ID: "unverified-lab", Name: "Fictional unrelated lab", DurationMin: 30, Price: 100}}); err != nil { t.Fatal(err) } if err = st.SetBookingSettings(ctx, storage.BookingSettings{}); err != nil { t.Fatal(err) } start := time.Now().UTC().Truncate(time.Hour).Add(72 * time.Hour) end := start.Add(30 * time.Minute) if err = st.AcceptOnecSlots(ctx, start.Add(-time.Hour), end.Add(time.Hour), []onec.Slot{{DoctorID: doctor, BranchID: "branch", Start: start, End: end, Status: "free"}}); err != nil { t.Fatal(err) } app := &API{store: st, limiter: newRateLimiter(), onecHook: "fictional-service-guard"} mux := app.Routes() w := httptest.NewRecorder() mux.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/api/v1/doctors/"+doctor+"/slots", nil)) var result struct { Data struct { Enabled bool `json:"enabled"` Slots []storage.OnecSlot `json:"slots"` Services []onec.Service `json:"services"` } `json:"data"` } if err = json.Unmarshal(w.Body.Bytes(), &result); err != nil { t.Fatal(err) } if w.Code != 200 || !result.Data.Enabled || len(result.Data.Slots) != 1 || result.Data.Services == nil || len(result.Data.Services) != 0 { t.Fatalf("unverified catalog leaked or real slot disappeared: %d %s", w.Code, w.Body.String()) } raw, err := st.OnecServices(ctx, doctor) if err != nil || len(raw) != 1 { t.Fatalf("raw import must remain intact for reconciliation: %+v %v", raw, err) } in := appointmentInput{DoctorSlug: doctor, Start: start.Format(time.RFC3339), Name: "Fictional patient", Phone: "79990000000", Consent: true, RequestID: doctor, BranchID: "branch"} body, _ := json.Marshal(in) w = httptest.NewRecorder() mux.ServeHTTP(w, httptest.NewRequest(http.MethodPost, "/api/v1/appointments", bytes.NewReader(body))) if w.Code != http.StatusCreated || !bytes.Contains(w.Body.Bytes(), []byte(`"mode":"pending"`)) { t.Fatalf("actual slot cannot be booked without service: %d %s", w.Code, w.Body.String()) } pending, err := st.PendingOnecAppointments(ctx, 50) if err != nil { t.Fatal(err) } found := false for _, record := range pending { if record.RequestID == doctor { found = true if record.ServiceID != "" { t.Fatal("unverified service was attached to pending appointment") } } } if !found { t.Fatal("booking missing from test pending queue") } }