package main import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "sync" "testing" "time" "git.cin.su/m/Assistant/adapters/notificationexecutor" commitcontract "git.cin.su/m/Assistant/contracts/commit" ) func delivery(t *testing.T, intent, message string) notificationexecutor.DeliveryRequest { t.Helper() payload := notificationexecutor.Payload{ActionIntentID: commitcontract.ActionIntentID(intent), TemplateCode: commitcontract.ProspectiveDeliveredTemplateV1, ProspectiveItemID: "item-1", ExpectedRevision: 1, Title: "Домовой", Message: message} digest, err := notificationexecutor.Digest(payload) if err != nil { t.Fatal(err) } return notificationexecutor.DeliveryRequest{Payload: payload, RequestDigest: digest} } func request(t *testing.T, handler http.Handler, method, path, token string, value any) *httptest.ResponseRecorder { t.Helper() var body bytes.Buffer if value != nil { if err := json.NewEncoder(&body).Encode(value); err != nil { t.Fatal(err) } } r := httptest.NewRequest(method, path, &body) if token != "" { r.Header.Set("Authorization", "Bearer "+token) } w := httptest.NewRecorder() handler.ServeHTTP(w, r) return w } func TestReceiverIdempotencyConflictAndRestart(t *testing.T) { directory := t.TempDir() clock := func() time.Time { return time.UnixMilli(100) } server, err := newReceiverServer(directory, "write-secret", "read-secret", clock) if err != nil { t.Fatal(err) } first := delivery(t, "intent-1", "message one") w := request(t, server.routes(), http.MethodPut, "/delivery/v1/deliveries/intent-1", "write-secret", first) if w.Code != http.StatusCreated { t.Fatalf("create status %d: %s", w.Code, w.Body.String()) } w = request(t, server.routes(), http.MethodPut, "/delivery/v1/deliveries/intent-1", "write-secret", first) if w.Code != http.StatusOK { t.Fatalf("repeat status %d", w.Code) } conflict := delivery(t, "intent-1", "different") w = request(t, server.routes(), http.MethodPut, "/delivery/v1/deliveries/intent-1", "write-secret", conflict) if w.Code != http.StatusConflict { t.Fatalf("conflict status %d", w.Code) } restarted, err := newReceiverServer(directory, "write-secret", "read-secret", clock) if err != nil { t.Fatal(err) } w = request(t, restarted.routes(), http.MethodGet, "/delivery/v1/receipts/intent-1", "write-secret", nil) if w.Code != http.StatusOK { t.Fatalf("restart get status %d", w.Code) } w = request(t, restarted.routes(), http.MethodGet, "/delivery/v1/receipts?limit=10", "read-secret", nil) if w.Code != http.StatusOK { t.Fatalf("list status %d", w.Code) } } func TestReceiverSeparatesReadAndWriteAuthority(t *testing.T) { server, err := newReceiverServer(t.TempDir(), "write-secret", "read-secret", time.Now) if err != nil { t.Fatal(err) } handler := server.routes() value := delivery(t, "intent-1", "message") if w := request(t, handler, http.MethodPut, "/delivery/v1/deliveries/intent-1", "read-secret", value); w.Code != http.StatusUnauthorized { t.Fatalf("reader wrote: %d", w.Code) } if w := request(t, handler, http.MethodGet, "/delivery/v1/receipts", "write-secret", nil); w.Code != http.StatusUnauthorized { t.Fatalf("writer listed: %d", w.Code) } } func TestConcurrentDuplicateCreatesOneReceipt(t *testing.T) { server, err := newReceiverServer(t.TempDir(), "write-secret", "read-secret", time.Now) if err != nil { t.Fatal(err) } value := delivery(t, "intent-concurrent", "message") handler := server.routes() codes := make(chan int, 8) var group sync.WaitGroup for range 8 { group.Add(1) go func() { defer group.Done() codes <- request(t, handler, http.MethodPut, "/delivery/v1/deliveries/intent-concurrent", "write-secret", value).Code }() } group.Wait() close(codes) created := 0 for code := range codes { if code == http.StatusCreated { created++ } else if code != http.StatusOK { t.Fatalf("unexpected status %d", code) } } if created != 1 { t.Fatalf("created=%d", created) } }