Skip to content
quarry Artifact [4a8d3c667f]
Login

Artifact [4a8d3c667f]

Artifact 4a8d3c667ff2e9d1d70f1c9a7df4f38ff174df167395e46b0381d99563600fc8:


package notify

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"
)

// recorder is a minimal fake notify server: it remembers every request
// it saw and answers with whatever statuses the test primes.
type recorder struct {
	reqs     []recordedReq
	statuses map[string]int // method+" "+path -> status to answer once, then 200
}

type recordedReq struct {
	method, path string
	body         map[string]any
}

func (r *recorder) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	var body map[string]any
	json.NewDecoder(req.Body).Decode(&body)
	r.reqs = append(r.reqs, recordedReq{method: req.Method, path: req.URL.Path, body: body})
	key := req.Method + " " + req.URL.Path
	if status, ok := r.statuses[key]; ok {
		w.WriteHeader(status)
		return
	}
	w.WriteHeader(http.StatusOK)
}

func serverWith(t *testing.T, handler http.HandlerFunc) (*httptest.Server, *recorder) {
	t.Helper()
	rec := &recorder{statuses: map[string]int{}}
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		rec.ServeHTTP(w, req)
		if handler != nil {
			handler(w, req)
		}
	}))
	t.Cleanup(srv.Close)
	return srv, rec
}

func TestEnsureSubscriber(t *testing.T) {
	srv, rec := serverWith(t, nil)
	c := New(srv.URL, "k")
	if err := c.EnsureSubscriber(context.Background(), "alice"); err != nil {
		t.Fatal(err)
	}
	if len(rec.reqs) != 1 || rec.reqs[0].path != "/v1/subscribers" {
		t.Fatalf("requests = %+v", rec.reqs)
	}
	if rec.reqs[0].body["subscriberId"] != "alice" {
		t.Fatalf("body = %+v", rec.reqs[0].body)
	}
}

func TestEnsureSubscriberNilClient(t *testing.T) {
	var c *Client
	if err := c.EnsureSubscriber(context.Background(), "alice"); err != nil {
		t.Fatalf("nil client must no-op: %v", err)
	}
	c = New("", "k")
	if err := c.EnsureSubscriber(context.Background(), "alice"); err != nil {
		t.Fatalf("disabled client must no-op: %v", err)
	}
}

func TestEnsureSubscriberAlreadyExists(t *testing.T) {
	rec := &recorder{statuses: map[string]int{"POST /v1/subscribers": http.StatusConflict}}
	srv := httptest.NewServer(rec)
	t.Cleanup(srv.Close)
	c := New(srv.URL, "k")
	if err := c.EnsureSubscriber(context.Background(), "alice"); err != nil {
		t.Fatalf("409 must be treated as success: %v", err)
	}
}

func TestEnsureSubscriberRealFailure(t *testing.T) {
	rec := &recorder{statuses: map[string]int{"POST /v1/subscribers": http.StatusInternalServerError}}
	srv := httptest.NewServer(rec)
	t.Cleanup(srv.Close)
	c := New(srv.URL, "k")
	if err := c.EnsureSubscriber(context.Background(), "alice"); err == nil {
		t.Fatal("a real 500 must be reported")
	}
}

func TestEnsureTopic(t *testing.T) {
	srv, rec := serverWith(t, nil)
	c := New(srv.URL, "k")
	if err := c.EnsureTopic(context.Background(), "repo-quarry", "quarry activity"); err != nil {
		t.Fatal(err)
	}
	if len(rec.reqs) != 1 || rec.reqs[0].path != "/v1/topics" ||
		rec.reqs[0].body["key"] != "repo-quarry" || rec.reqs[0].body["name"] != "quarry activity" {
		t.Fatalf("requests = %+v", rec.reqs)
	}
}

func TestAddTopicMember(t *testing.T) {
	srv, rec := serverWith(t, nil)
	c := New(srv.URL, "k")
	if err := c.AddTopicMember(context.Background(), "repo-quarry", "alice"); err != nil {
		t.Fatal(err)
	}
	if len(rec.reqs) != 1 || rec.reqs[0].path != "/v1/topics/repo-quarry/subscribers" ||
		rec.reqs[0].body["subscriberId"] != "alice" {
		t.Fatalf("requests = %+v", rec.reqs)
	}
}

func TestAddTopicMemberAlreadyExists(t *testing.T) {
	rec := &recorder{statuses: map[string]int{"POST /v1/topics/repo-quarry/subscribers": http.StatusConflict}}
	srv := httptest.NewServer(rec)
	t.Cleanup(srv.Close)
	c := New(srv.URL, "k")
	if err := c.AddTopicMember(context.Background(), "repo-quarry", "alice"); err != nil {
		t.Fatalf("409 must be treated as success: %v", err)
	}
}

func TestRemoveTopicMember(t *testing.T) {
	rec := &recorder{statuses: map[string]int{}}
	srv := httptest.NewServer(rec)
	t.Cleanup(srv.Close)
	c := New(srv.URL, "k")
	if err := c.RemoveTopicMember(context.Background(), "repo-quarry", "alice"); err != nil {
		t.Fatal(err)
	}
	if len(rec.reqs) != 1 || rec.reqs[0].method != "DELETE" ||
		rec.reqs[0].path != "/v1/topics/repo-quarry/subscribers/alice" {
		t.Fatalf("requests = %+v", rec.reqs)
	}
}

func TestRemoveTopicMemberAbsentIsSuccess(t *testing.T) {
	rec := &recorder{statuses: map[string]int{"DELETE /v1/topics/repo-quarry/subscribers/alice": http.StatusNotFound}}
	srv := httptest.NewServer(rec)
	t.Cleanup(srv.Close)
	c := New(srv.URL, "k")
	if err := c.RemoveTopicMember(context.Background(), "repo-quarry", "alice"); err != nil {
		t.Fatalf("404 on remove must be treated as success: %v", err)
	}
}

func TestEnsureWorkflowCreatesWhenAbsent(t *testing.T) {
	rec := &recorder{statuses: map[string]int{"GET /v1/workflows/quarry-mention": http.StatusNotFound}}
	srv := httptest.NewServer(rec)
	t.Cleanup(srv.Close)
	c := New(srv.URL, "k")
	def := WorkflowDef{Name: "quarry-mention", TriggerIdentifier: "quarry-mention",
		Steps: []WorkflowStep{{StepID: "S1", Type: "in_app", ControlValues: map[string]string{"subject": "s", "body": "b"}}}}
	if err := c.EnsureWorkflow(context.Background(), def); err != nil {
		t.Fatal(err)
	}
	var gets, posts int
	for _, r := range rec.reqs {
		switch {
		case r.method == "GET" && r.path == "/v1/workflows/quarry-mention":
			gets++
		case r.method == "POST" && r.path == "/v1/workflows":
			posts++
		}
	}
	if gets != 1 || posts != 1 {
		t.Fatalf("expected one GET then one POST, got requests = %+v", rec.reqs)
	}
}

func TestEnsureWorkflowLeavesPresentAlone(t *testing.T) {
	rec := &recorder{statuses: map[string]int{"GET /v1/workflows/quarry-mention": http.StatusOK}}
	srv := httptest.NewServer(rec)
	t.Cleanup(srv.Close)
	c := New(srv.URL, "k")
	def := WorkflowDef{Name: "quarry-mention", TriggerIdentifier: "quarry-mention"}
	if err := c.EnsureWorkflow(context.Background(), def); err != nil {
		t.Fatal(err)
	}
	for _, r := range rec.reqs {
		if r.method == "POST" {
			t.Fatalf("a present workflow must never be POSTed (or PUT) over: %+v", rec.reqs)
		}
	}
	if len(rec.reqs) != 1 {
		t.Fatalf("expected exactly the one GET, got %+v", rec.reqs)
	}
}

func TestEnsureWorkflowNilClient(t *testing.T) {
	var c *Client
	if err := c.EnsureWorkflow(context.Background(), WorkflowDef{TriggerIdentifier: "x"}); err != nil {
		t.Fatalf("nil client must no-op: %v", err)
	}
}

func TestWorkflowsCoverTheFiveNames(t *testing.T) {
	want := map[string]bool{
		"quarry-mention": false, "quarry-forum-reply": false, "quarry-activity": false,
		"quarry-landing": false, "quarry-error": false, "quarry-slow": false,
	}
	for _, wf := range Workflows() {
		if _, ok := want[wf.Name]; !ok {
			t.Fatalf("unexpected workflow %q", wf.Name)
		}
		want[wf.Name] = true
		if wf.TriggerIdentifier != wf.Name {
			t.Fatalf("%s: triggerIdentifier %q must match name", wf.Name, wf.TriggerIdentifier)
		}
		if len(wf.Steps) != 1 || wf.Steps[0].Type != "in_app" {
			t.Fatalf("%s: expected exactly one in_app step, got %+v", wf.Name, wf.Steps)
		}
	}
	for name, seen := range want {
		if !seen {
			t.Fatalf("workflow %q missing from Workflows()", name)
		}
	}
}