143 lines
5.1 KiB
Go
143 lines
5.1 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http/httptest"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"dana/internal/onec"
|
|
"dana/internal/storage"
|
|
)
|
|
|
|
func TestAutonomousHTTPHandshake(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)
|
|
}
|
|
app := &API{store: st, limiter: newRateLimiter(), onecHook: "autonomy-test"}
|
|
mux := app.Routes()
|
|
call := func(method, path string, body any, want int) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
b, _ := json.Marshal(body)
|
|
req := httptest.NewRequest(method, path, bytes.NewReader(b))
|
|
req.Header.Set("X-Api-Key", "autonomy-test")
|
|
w := httptest.NewRecorder()
|
|
mux.ServeHTTP(w, req)
|
|
if w.Code != want {
|
|
t.Fatalf("%s %s: %d, want %d: %s", method, path, w.Code, want, w.Body.String())
|
|
}
|
|
return w
|
|
}
|
|
start := time.Now().UTC().Truncate(time.Hour).Add(72 * time.Hour)
|
|
end := start.Add(30 * time.Minute)
|
|
from, to := start.Add(-time.Hour), end.Add(time.Hour)
|
|
doctor := fmt.Sprintf("http-autonomy-%d", time.Now().UnixNano())
|
|
slot := onec.Slot{DoctorID: doctor, BranchID: "branch", Start: start, End: end, Status: "free"}
|
|
payload := onecSyncPayload{From: &from, To: &to, Branches: []onec.Branch{}, Doctors: []onec.Doctor{}, Services: []onecDoctorServices{}, Slots: []onec.Slot{slot}}
|
|
call("POST", "/api/v1/onec/sync/all", payload, 200)
|
|
call("POST", "/api/v1/onec/sync/all", map[string]any{}, 400)
|
|
var doctorID int64
|
|
if err = st.Pool().QueryRow(ctx, `INSERT INTO doctors(slug,last_name,first_name,booking_provider) VALUES($1,'Тест','Врач','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,'Тест Врач',$2)`, doctor, doctorID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = st.SetBookingSettings(ctx, storage.BookingSettings{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
call("GET", "/api/v1/doctors/"+doctor+"/slots", nil, 200)
|
|
creation := appointmentInput{DoctorSlug: doctor, Start: start.Format(time.RFC3339), Name: "Тестовый пациент", Phone: "79990000000", Consent: true, RequestID: doctor, Comment: "HTTP test"}
|
|
made := call("POST", "/api/v1/appointments", creation, 201)
|
|
if !bytes.Contains(made.Body.Bytes(), []byte(`"mode":"pending"`)) {
|
|
t.Fatal("unconfirmed booking reported as booked")
|
|
}
|
|
call("POST", "/api/v1/appointments", creation, 202)
|
|
pendingRecords, err := st.PendingOnecAppointments(ctx, 50)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var rec storage.OnecPendingAppointment
|
|
for _, v := range pendingRecords {
|
|
if v.RequestID == doctor {
|
|
rec = v
|
|
}
|
|
}
|
|
if rec.ID == 0 {
|
|
t.Fatal("public booking not queued")
|
|
}
|
|
call("POST", "/api/v1/onec/sync/all", payload, 200)
|
|
free := call("GET", "/api/v1/doctors/"+doctor+"/slots", nil, 200)
|
|
var freeBody struct {
|
|
Data struct {
|
|
Enabled bool `json:"enabled"`
|
|
Slots []storage.OnecSlot `json:"slots"`
|
|
} `json:"data"`
|
|
}
|
|
if err = json.Unmarshal(free.Body.Bytes(), &freeBody); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !freeBody.Data.Enabled || len(freeBody.Data.Slots) != 0 {
|
|
t.Fatal("push reopened reserved public slot")
|
|
}
|
|
|
|
pending := call("GET", "/api/v1/onec/appointments/pending", nil, 200)
|
|
if !bytes.Contains(pending.Body.Bytes(), []byte(doctor)) || !bytes.Contains(pending.Body.Bytes(), []byte(`"commands"`)) {
|
|
t.Fatal("pending contract incomplete")
|
|
}
|
|
uuid := "00000000-0000-0000-0000-000000000002"
|
|
confirm := confirmAppointmentReq{OnecAppointmentID: uuid, End: &end}
|
|
path := fmt.Sprintf("/api/v1/onec/appointments/%d/confirm", rec.ID)
|
|
call("POST", path, confirm, 200)
|
|
call("POST", path, confirm, 200)
|
|
confirm.OnecAppointmentID = "00000000-0000-0000-0000-000000000003"
|
|
call("POST", path, confirm, 409)
|
|
call("POST", fmt.Sprintf("/api/v1/onec/appointments/%d/reject", rec.ID), rejectAppointmentReq{Reason: "late"}, 409)
|
|
call("POST", "/api/v1/onec/events", onecEvent{Event: "schedule_changed", DoctorID: doctor}, 202)
|
|
pending = call("GET", "/api/v1/onec/appointments/pending", nil, 200)
|
|
if !bytes.Contains(pending.Body.Bytes(), []byte(`"sync_requested":true`)) {
|
|
t.Fatal("snapshot request not exposed")
|
|
}
|
|
if err = st.QueueOnecCommand(ctx, rec.ID, "cancel", "", "cancelled"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
commands, err := st.PendingOnecCommands(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var id int64
|
|
for _, c := range commands {
|
|
if c.AppointmentID == uuid {
|
|
id = c.ID
|
|
}
|
|
}
|
|
if id == 0 {
|
|
t.Fatal("cancel command missing")
|
|
}
|
|
ack := fmt.Sprintf("/api/v1/onec/commands/%d/ack", id)
|
|
call("POST", ack, map[string]any{"success": true}, 200)
|
|
call("POST", ack, map[string]any{"success": true}, 200)
|
|
call("POST", "/api/v1/onec/events", onecEvent{Event: "appointment_status", AppointmentID: uuid, Status: "cancelled"}, 200)
|
|
call("POST", path, confirm, 409)
|
|
payload.Slots = []onec.Slot{}
|
|
call("POST", "/api/v1/onec/sync/all", payload, 200)
|
|
slots, err := st.FreeOnecSlots(ctx, doctor, from, to)
|
|
if err != nil || len(slots) != 0 {
|
|
t.Fatal("empty horizon did not clear slots")
|
|
}
|
|
}
|