package web import ( "context" "net/http" "net/http/httptest" "strings" "testing" "yovision/bell/internal/alert" ) type fakeRepository struct { limit int actor string } func (*fakeRepository) AlertReady(context.Context) error { return nil } func (*fakeRepository) PublishRule(context.Context, alert.RuleSpec) (bool, error) { return true, nil } func (*fakeRepository) EvaluateNext(context.Context) (bool, error) { return false, nil } func (f *fakeRepository) ListAlerts(_ context.Context, _, _ int64, _ string, limit int, _ string) (alert.Page, error) { f.limit = limit return alert.Page{Items: []alert.Summary{}}, nil } func (*fakeRepository) GetAlert(context.Context, int64, int64, string) (alert.Detail, error) { return alert.Detail{}, alert.ErrNotFound } func (f *fakeRepository) Command(_ context.Context, _, _ int64, id, command, key, actor string, _ *string) (int, alert.CommandResponse, error) { f.actor = actor return 200, alert.CommandResponse{AlertID: id, State: "acknowledged", ActorRef: actor}, nil } func testMux(t *testing.T) (*http.ServeMux, *fakeRepository) { t.Helper() repository := &fakeRepository{} handler, err := NewHandler(repository, Config{Token: "12345678901234567890123456789012", TenantID: 1, SiteID: 2, ActorRef: "operator:local"}) if err != nil { t.Fatal(err) } mux := http.NewServeMux() handler.Register(mux) return mux, repository } func TestPageIsNoStoreAndDoesNotEmbedToken(t *testing.T) { mux, _ := testMux(t) request := httptest.NewRequest(http.MethodGet, "/bell-console/", nil) response := httptest.NewRecorder() mux.ServeHTTP(response, request) if response.Code != 200 || response.Header().Get("Cache-Control") != "no-store" { t.Fatalf("page response: %d %#v", response.Code, response.Header()) } if body := response.Body.String(); body == "" || strings.Contains(body, "12345678901234567890123456789012") { t.Fatal("page missing or leaked console token") } } func TestAPIRequiresBearerAndDefaultsToSixteen(t *testing.T) { mux, repository := testMux(t) request := httptest.NewRequest(http.MethodGet, "/bell-console/api/v1/alerts", nil) response := httptest.NewRecorder() mux.ServeHTTP(response, request) if response.Code != http.StatusUnauthorized { t.Fatalf("unauthorized status %d", response.Code) } request = httptest.NewRequest(http.MethodGet, "/bell-console/api/v1/alerts", nil) request.Header.Set("Authorization", "Bearer 12345678901234567890123456789012") response = httptest.NewRecorder() mux.ServeHTTP(response, request) if response.Code != http.StatusOK || repository.limit != alert.DefaultPageSize { t.Fatalf("authorized list: status=%d limit=%d", response.Code, repository.limit) } } func TestCommandUsesServerActorAndRequiresIdempotencyKey(t *testing.T) { mux, repository := testMux(t) path := "/bell-console/api/v1/alerts/alt_01J8XQ2K7M3P5R9T0V4W6Y8Z2C:ack" request := httptest.NewRequest(http.MethodPost, path, strings.NewReader(`{"note":null}`)) request.Header.Set("Authorization", "Bearer 12345678901234567890123456789012") response := httptest.NewRecorder() mux.ServeHTTP(response, request) if response.Code != http.StatusBadRequest { t.Fatalf("missing idempotency status %d", response.Code) } request = httptest.NewRequest(http.MethodPost, path, strings.NewReader(`{"note":null}`)) request.Header.Set("Authorization", "Bearer 12345678901234567890123456789012") request.Header.Set("Idempotency-Key", "command-0001") response = httptest.NewRecorder() mux.ServeHTTP(response, request) if response.Code != http.StatusOK || repository.actor != "operator:local" { t.Fatalf("command status=%d actor=%q", response.Code, repository.actor) } }