Files
tiny-forge/internal/store/workload_notifications_test.go
alexei.dolgolyov 410a131cec feat(apps): stepped creation wizard, branch previews, and app-creation fixes
This session (frontend focus):
- Rebuild /apps/new as a 4-step wizard (Basics → Configure → Trigger → Review):
  WizardRail, SourceKindPicker card grid, AppManifest review, per-step validation,
  ConfirmDialog-based unsaved-changes guard.
- Extract lib/workload/sourceForms.ts (single source of truth for source_config)
  + {Image,Compose,Static,Dockerfile}SourceForm + StaticDiscoveryWizard; fold the
  /apps/[id] edit form onto the same components (removes the duplication). Add
  vitest + sourceForms unit tests.
- Branch preview environments UI: /chain is_preview/preview_branch + a Preview
  environments panel on /apps/[id] (per-branch URLs, ConfirmDialog teardown, armed
  state); RegistryImagePicker on the registry trigger and the image source.
- Fixes: image-inspect 404 -> admin-gated POST /api/discovery/image/inspect;
  conflict-panel blur flicker; friendly localized discovery errors; CPU/Memory
  label hints; dashboard + /apps "Total workloads" count only source_kind workloads
  (drop stale trigger_kind gate); NPM cert/access-list name cache; EntityPicker
  empty-list guard.
- Update CLAUDE.md frontend conventions + add a Build & Test section.

Also captures pre-existing in-progress platform work (not from this session):
workload notifications, Prometheus metrics export, store lockfile, health probes,
backup hardening, and related store/webhook/scheduler changes.
2026-05-29 02:09:54 +03:00

171 lines
5.0 KiB
Go

package store
import (
"errors"
"testing"
)
// seedWorkloadForNotifications creates a minimal workload row so the FK
// constraint on workload_notifications is satisfied. Returns the new
// workload's ID for tests to reference.
func seedWorkloadForNotifications(t *testing.T, s *Store, name string) string {
t.Helper()
w, err := s.CreateWorkload(Workload{
Kind: string(WorkloadKindProject),
Name: name,
SourceKind: "image",
})
if err != nil {
t.Fatalf("seed workload: %v", err)
}
return w.ID
}
func TestCreateWorkloadNotification_RoundTrip(t *testing.T) {
s := newTestStore(t)
wlID := seedWorkloadForNotifications(t, s, "app1")
created, err := s.CreateWorkloadNotification(WorkloadNotification{
WorkloadID: wlID,
Name: "Slack alerts",
URL: "https://hooks.slack.test/x",
Secret: "shh",
EventTypes: "deploy_failure,build_failure",
Enabled: true,
})
if err != nil {
t.Fatalf("CreateWorkloadNotification: %v", err)
}
if created.ID == "" {
t.Fatal("expected ID to be assigned")
}
got, err := s.GetWorkloadNotification(created.ID)
if err != nil {
t.Fatalf("Get: %v", err)
}
if got.URL != "https://hooks.slack.test/x" || got.Name != "Slack alerts" {
t.Errorf("row mismatch: %+v", got)
}
if !got.Enabled {
t.Error("expected Enabled=true")
}
if got.EventTypes != "deploy_failure,build_failure" {
t.Errorf("event_types = %q", got.EventTypes)
}
}
func TestCreateWorkloadNotification_RejectsMissingURL(t *testing.T) {
s := newTestStore(t)
wlID := seedWorkloadForNotifications(t, s, "app1")
_, err := s.CreateWorkloadNotification(WorkloadNotification{
WorkloadID: wlID,
Name: "broken",
URL: "",
})
if err == nil {
t.Fatal("expected URL validation error")
}
}
func TestListWorkloadNotifications_SortedByOrder(t *testing.T) {
s := newTestStore(t)
wlID := seedWorkloadForNotifications(t, s, "app1")
// Insert out of order; ListWorkloadNotifications should return
// them sorted by SortOrder ascending.
_, _ = s.CreateWorkloadNotification(WorkloadNotification{
WorkloadID: wlID, Name: "C", URL: "https://c.test", SortOrder: 30,
})
_, _ = s.CreateWorkloadNotification(WorkloadNotification{
WorkloadID: wlID, Name: "A", URL: "https://a.test", SortOrder: 10,
})
_, _ = s.CreateWorkloadNotification(WorkloadNotification{
WorkloadID: wlID, Name: "B", URL: "https://b.test", SortOrder: 20,
})
rows, err := s.ListWorkloadNotifications(wlID)
if err != nil {
t.Fatalf("list: %v", err)
}
if len(rows) != 3 {
t.Fatalf("len = %d, want 3", len(rows))
}
if rows[0].Name != "A" || rows[1].Name != "B" || rows[2].Name != "C" {
t.Errorf("sort order wrong: %q %q %q", rows[0].Name, rows[1].Name, rows[2].Name)
}
}
func TestUpdateWorkloadNotification_PersistsChanges(t *testing.T) {
s := newTestStore(t)
wlID := seedWorkloadForNotifications(t, s, "app1")
n, _ := s.CreateWorkloadNotification(WorkloadNotification{
WorkloadID: wlID, Name: "old", URL: "https://old.test", Enabled: true,
})
n.Name = "new"
n.URL = "https://new.test"
n.Enabled = false
n.EventTypes = "deploy_success"
if err := s.UpdateWorkloadNotification(n); err != nil {
t.Fatalf("update: %v", err)
}
got, _ := s.GetWorkloadNotification(n.ID)
if got.Name != "new" || got.URL != "https://new.test" || got.Enabled {
t.Errorf("update did not persist: %+v", got)
}
}
func TestDeleteWorkloadNotification_ReturnsNotFoundForMissing(t *testing.T) {
s := newTestStore(t)
err := s.DeleteWorkloadNotification("nope")
if !errors.Is(err, ErrNotFound) {
t.Errorf("expected ErrNotFound, got %v", err)
}
}
func TestDeleteWorkloadNotification_CascadesFromWorkload(t *testing.T) {
s := newTestStore(t)
wlID := seedWorkloadForNotifications(t, s, "app1")
_, _ = s.CreateWorkloadNotification(WorkloadNotification{
WorkloadID: wlID, Name: "x", URL: "https://x.test",
})
if err := s.DeleteWorkload(wlID); err != nil {
t.Fatalf("delete workload: %v", err)
}
rows, err := s.ListWorkloadNotifications(wlID)
if err != nil {
t.Fatalf("list after cascade: %v", err)
}
if len(rows) != 0 {
t.Errorf("expected cascade delete to remove rows, got %d", len(rows))
}
}
func TestMatchesEventType_AllowList(t *testing.T) {
cases := []struct {
eventTypes string
probe string
want bool
}{
{"", "deploy_success", true}, // empty = all
{"deploy_success,deploy_failure", "deploy_success", true},
{"deploy_success,deploy_failure", "build_failure", false},
{"build_failure", "build_failure", true},
{" deploy_success , build_failure ", "build_failure", true}, // whitespace tolerated
}
for _, c := range cases {
n := WorkloadNotification{Enabled: true, EventTypes: c.eventTypes}
got := n.MatchesEventType(c.probe)
if got != c.want {
t.Errorf("MatchesEventType(%q, %q) = %v, want %v", c.eventTypes, c.probe, got, c.want)
}
}
}
func TestMatchesEventType_DisabledNeverMatches(t *testing.T) {
n := WorkloadNotification{Enabled: false, EventTypes: ""}
if n.MatchesEventType("any") {
t.Error("disabled row should never match")
}
}