Files
cloud-ip-validator/internal/dashboard/handlers_test.go
T

196 lines
7.5 KiB
Go
Raw Normal View History

2026-08-23 20:39:22 +03:00
package dashboard
import (
"strings"
"testing"
"time"
)
func TestOverviewFragment(t *testing.T) {
fake, caURL := newFakeControlAPI(t)
now := time.Now()
older := now.Add(-time.Hour)
fake.validators = []validatorDTO{{ValidatorID: "v1", State: "idle"}}
fake.ips = []ipQueueItem{
{IPAddress: "1.1.1.1", State: "checking", Sequence: 1, AssignedAt: &now, UpdatedAt: now, CreatedAt: now},
{IPAddress: "2.2.2.2", State: "done", OverallResult: "pass", AggregatedAt: &now, UpdatedAt: now, CreatedAt: now},
{IPAddress: "3.3.3.3", State: "failed", OverallResult: "cancelled", AggregatedAt: &older, UpdatedAt: now, CreatedAt: now},
}
ts := newTestServer(t, caURL)
body := get(t, ts, "/overview/fragment")
if !strings.Contains(body, "1.1.1.1") {
t.Fatalf("expected in-progress ip 1.1.1.1 in current-checking section, got:\n%s", body)
}
if strings.Contains(body, `href="/ips/1.1.1.1"`) {
// currently-checking rows link to /ips detail too; that's fine, just
// make sure the done one shows up in the "last completed" table.
}
if !strings.Contains(body, "2.2.2.2") || !strings.Contains(body, "3.3.3.3") {
t.Fatalf("expected both completed ips in last-completed section, got:\n%s", body)
}
if !strings.Contains(body, "pass: 1") || !strings.Contains(body, "cancelled: 1") {
t.Fatalf("expected breakdown pass:1 cancelled:1, got:\n%s", body)
}
}
func TestIPsSubmitAddAndForceRecheck(t *testing.T) {
fake, caURL := newFakeControlAPI(t)
now := time.Now()
fake.ips = []ipQueueItem{
{IPAddress: "9.9.9.9", State: "done", OverallResult: "pass", AttemptNumber: 1, AggregatedAt: &now, UpdatedAt: now, CreatedAt: now},
}
ts := newTestServer(t, caURL)
// Add a brand-new address.
body := postForm(t, ts, "POST", "/ips", map[string][]string{"addresses": {"1.2.3.4"}})
if !strings.Contains(body, "1.2.3.4") {
t.Fatalf("expected new address in re-rendered table, got:\n%s", body)
}
// Force-recheck the already-done address via the same endpoint.
body = postForm(t, ts, "POST", "/ips", map[string][]string{"addresses": {"9.9.9.9"}})
if !strings.Contains(body, "9.9.9.9") {
t.Fatalf("expected requeued address in table, got:\n%s", body)
}
if fake.ips[0].State != "queued" || fake.ips[0].AttemptNumber != 2 {
t.Fatalf("expected fake control-api state requeued with attempt_number=2, got %+v", fake.ips[0])
}
// Empty submission is a client error, surfaced via the banner, not a 500.
body = postForm(t, ts, "POST", "/ips", map[string][]string{"addresses": {""}})
2026-08-23 21:15:31 +03:00
if !strings.Contains(body, "alert-warning") {
2026-08-23 20:39:22 +03:00
t.Fatalf("expected client error banner for empty submission, got:\n%s", body)
}
}
func TestIPRecheckSkippedInProgress(t *testing.T) {
fake, caURL := newFakeControlAPI(t)
now := time.Now()
fake.ips = []ipQueueItem{{IPAddress: "5.5.5.5", State: "checking", UpdatedAt: now, CreatedAt: now}}
ts := newTestServer(t, caURL)
body := postForm(t, ts, "POST", "/ips/5.5.5.5/recheck", nil)
// SubmitIPs succeeds (200) but the address itself lands in
// skipped_in_progress — the dashboard shouldn't claim success without
// qualification; the table must still show it untouched (still checking).
if !strings.Contains(body, "5.5.5.5") {
t.Fatalf("expected 5.5.5.5 still listed, got:\n%s", body)
}
if fake.ips[0].State != "checking" {
t.Fatalf("expected state untouched (checking), got %s", fake.ips[0].State)
}
}
func TestIPCancel(t *testing.T) {
fake, caURL := newFakeControlAPI(t)
now := time.Now()
fake.ips = []ipQueueItem{{IPAddress: "7.7.7.7", State: "checking", UpdatedAt: now, CreatedAt: now}}
ts := newTestServer(t, caURL)
body := postForm(t, ts, "POST", "/ips/7.7.7.7/cancel", nil)
if !strings.Contains(body, "cancelled") {
t.Fatalf("expected cancelled badge after cancel, got:\n%s", body)
}
if fake.ips[0].OverallResult != "cancelled" {
t.Fatalf("expected fake control-api state cancelled, got %+v", fake.ips[0])
}
// Cancelling an already-terminal ip is a 409 from control-api, surfaced
// as a banner, not a crash.
body = postForm(t, ts, "POST", "/ips/7.7.7.7/cancel", nil)
if !strings.Contains(body, "error-banner") {
t.Fatalf("expected error banner for double-cancel, got:\n%s", body)
}
}
func TestValidatorsCRUD(t *testing.T) {
_, caURL := newFakeControlAPI(t)
ts := newTestServer(t, caURL)
body := postForm(t, ts, "POST", "/validators", map[string][]string{"validator_id": {"val-1"}, "os_port_id": {"port-1"}})
if !strings.Contains(body, "val-1") || !strings.Contains(body, "port-1") {
t.Fatalf("expected new validator in table, got:\n%s", body)
}
// Conflict: creating the same validator_id again.
body = postForm(t, ts, "POST", "/validators", map[string][]string{"validator_id": {"val-1"}, "os_port_id": {"port-x"}})
2026-08-23 21:15:31 +03:00
if !strings.Contains(body, "alert-warning") {
2026-08-23 20:39:22 +03:00
t.Fatalf("expected client error banner on duplicate create, got:\n%s", body)
}
body = postForm(t, ts, "PUT", "/validators/val-1", map[string][]string{"os_port_id": {"port-2"}})
if !strings.Contains(body, "port-2") {
t.Fatalf("expected updated port in table, got:\n%s", body)
}
body = postForm(t, ts, "DELETE", "/validators/val-1", nil)
if strings.Contains(body, "val-1") {
t.Fatalf("expected validator removed after delete, got:\n%s", body)
}
}
func TestSitesPutAndConflict(t *testing.T) {
_, caURL := newFakeControlAPI(t)
ts := newTestServer(t, caURL)
body := postForm(t, ts, "PUT", "/sites/1", map[string][]string{"site_id": {"site-1"}})
if !strings.Contains(body, "site-1") {
t.Fatalf("expected site-1 assigned to slot 1, got:\n%s", body)
}
// Slot page always renders exactly 3 rows, including empty ones — count
// the per-slot PUT forms rather than raw <tr> (the thead row has one too).
page := get(t, ts, "/sites")
if strings.Count(page, `hx-put="/sites/`) != 3 {
t.Fatalf("expected exactly 3 site slot rows, got:\n%s", page)
}
body = postForm(t, ts, "PUT", "/sites/2", map[string][]string{"site_id": {"site-1"}})
2026-08-23 21:15:31 +03:00
if !strings.Contains(body, "alert-warning") {
2026-08-23 20:39:22 +03:00
t.Fatalf("expected conflict banner assigning a taken site_id to another slot, got:\n%s", body)
}
}
func TestTargetsAndCheckTypesRoundTrip(t *testing.T) {
_, caURL := newFakeControlAPI(t)
ts := newTestServer(t, caURL)
body := postForm(t, ts, "POST", "/targets", map[string][]string{"name": {"web"}, "targets": {"https://a.test\nhttps://b.test"}})
if !strings.Contains(body, "web") {
t.Fatalf("expected new target group in table, got:\n%s", body)
}
body = postForm(t, ts, "POST", "/check-types", map[string][]string{"name": {"https"}, "enabled": {"1"}, "targets": {"web"}})
if !strings.Contains(body, "https") {
t.Fatalf("expected new check type in table, got:\n%s", body)
}
// Can't delete a target group still referenced by a check type.
body = postForm(t, ts, "DELETE", "/targets/web", nil)
2026-08-23 21:15:31 +03:00
if !strings.Contains(body, "alert-warning") {
2026-08-23 20:39:22 +03:00
t.Fatalf("expected conflict banner deleting in-use target group, got:\n%s", body)
}
body = postForm(t, ts, "DELETE", "/check-types/https", nil)
if strings.Contains(body, "<td>https</td>") {
t.Fatalf("expected check type removed, got:\n%s", body)
}
body = postForm(t, ts, "DELETE", "/targets/web", nil)
if strings.Contains(body, "<td>web</td>") {
t.Fatalf("expected target group removable once unreferenced, got:\n%s", body)
}
}
func TestControlAPIUnreachable(t *testing.T) {
// Point the dashboard at an address nothing listens on, rather than a
// closed httptest.Server, to get a deterministic connection-refused
// transport failure.
ts := newTestServer(t, "http://127.0.0.1:1")
body := get(t, ts, "/overview")
2026-08-23 21:15:31 +03:00
if !strings.Contains(body, "alert-danger") {
2026-08-23 20:39:22 +03:00
t.Fatalf("expected server/transport error banner when control-api is unreachable, got:\n%s", body)
}
}