dana-medical-site/internal/api/onec_service_safety_test.go

116 lines
4.5 KiB
Go

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")
}
}