116 lines
4.5 KiB
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")
|
|
}
|
|
}
|