From e35b4f882ddaea4830d2524646f6d8413f54ce0e Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 23 Mar 2026 22:05:09 +0100 Subject: [PATCH] test(02-02): rewrite all tests to use per-test in-memory databases via NewTestServer - Remove TestMain (no longer needed; each test is isolated) - Replace all diun.UpdatesReset() with diun.NewTestServer() per test - Replace all diun.SetWebhookSecret/ResetWebhookSecret with NewTestServerWithSecret - Replace all diun.WebhookHandler etc with srv.WebhookHandler (method calls) - Replace diun.UpdateEvent with srv.TestUpsertEvent - Replace diun.GetUpdatesMap with srv.TestGetUpdatesMap - Update helper functions postTag/postTagAndGetID to accept *diun.Server parameter - Change t.Fatalf to t.Errorf inside goroutine in TestConcurrentUpdateEvent - Add error check on second TestUpsertEvent in TestDismissHandler_ReappearsAfterNewWebhook - All 32 tests pass with zero failures --- pkg/diunwebhook/diunwebhook_test.go | 370 ++++++++++++++++++---------- 1 file changed, 239 insertions(+), 131 deletions(-) diff --git a/pkg/diunwebhook/diunwebhook_test.go b/pkg/diunwebhook/diunwebhook_test.go index ca2dc4b..4b9538e 100644 --- a/pkg/diunwebhook/diunwebhook_test.go +++ b/pkg/diunwebhook/diunwebhook_test.go @@ -7,7 +7,6 @@ import ( "fmt" "net/http" "net/http/httptest" - "os" "sync" "testing" "time" @@ -15,13 +14,11 @@ import ( diun "awesomeProject/pkg/diunwebhook" ) -func TestMain(m *testing.M) { - diun.UpdatesReset() - os.Exit(m.Run()) -} - func TestUpdateEventAndGetUpdates(t *testing.T) { - diun.UpdatesReset() + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } event := diun.DiunEvent{ DiunVersion: "1.0", Hostname: "host", @@ -34,12 +31,12 @@ func TestUpdateEventAndGetUpdates(t *testing.T) { Created: time.Now(), Platform: "linux/amd64", } - if err := diun.UpdateEvent(event); err != nil { - t.Fatalf("test setup: UpdateEvent failed: %v", err) + if err := srv.TestUpsertEvent(event); err != nil { + t.Fatalf("test setup: TestUpsertEvent failed: %v", err) } - got, err := diun.GetUpdates() + got, err := srv.TestGetUpdates() if err != nil { - t.Fatalf("GetUpdates error: %v", err) + t.Fatalf("TestGetUpdates error: %v", err) } if len(got) != 1 { t.Fatalf("expected 1 update, got %d", len(got)) @@ -50,7 +47,10 @@ func TestUpdateEventAndGetUpdates(t *testing.T) { } func TestWebhookHandler(t *testing.T) { - diun.UpdatesReset() + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } event := diun.DiunEvent{ DiunVersion: "2.0", Hostname: "host2", @@ -66,94 +66,106 @@ func TestWebhookHandler(t *testing.T) { body, _ := json.Marshal(event) req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader(body)) rec := httptest.NewRecorder() - diun.WebhookHandler(rec, req) + srv.WebhookHandler(rec, req) if rec.Code != http.StatusOK { t.Errorf("expected status 200, got %d", rec.Code) } - if len(diun.GetUpdatesMap()) != 1 { - t.Errorf("expected 1 update, got %d", len(diun.GetUpdatesMap())) + if len(srv.TestGetUpdatesMap()) != 1 { + t.Errorf("expected 1 update, got %d", len(srv.TestGetUpdatesMap())) } } func TestWebhookHandler_Unauthorized(t *testing.T) { - diun.UpdatesReset() - diun.SetWebhookSecret("my-secret") - defer diun.ResetWebhookSecret() + srv, err := diun.NewTestServerWithSecret("my-secret") + if err != nil { + t.Fatalf("NewTestServerWithSecret: %v", err) + } event := diun.DiunEvent{Image: "nginx:latest"} body, _ := json.Marshal(event) req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader(body)) rec := httptest.NewRecorder() - diun.WebhookHandler(rec, req) + srv.WebhookHandler(rec, req) if rec.Code != http.StatusUnauthorized { t.Errorf("expected 401, got %d", rec.Code) } } func TestWebhookHandler_WrongToken(t *testing.T) { - diun.UpdatesReset() - diun.SetWebhookSecret("my-secret") - defer diun.ResetWebhookSecret() + srv, err := diun.NewTestServerWithSecret("my-secret") + if err != nil { + t.Fatalf("NewTestServerWithSecret: %v", err) + } event := diun.DiunEvent{Image: "nginx:latest"} body, _ := json.Marshal(event) req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader(body)) req.Header.Set("Authorization", "wrong-token") rec := httptest.NewRecorder() - diun.WebhookHandler(rec, req) + srv.WebhookHandler(rec, req) if rec.Code != http.StatusUnauthorized { t.Errorf("expected 401, got %d", rec.Code) } } func TestWebhookHandler_ValidToken(t *testing.T) { - diun.UpdatesReset() - diun.SetWebhookSecret("my-secret") - defer diun.ResetWebhookSecret() + srv, err := diun.NewTestServerWithSecret("my-secret") + if err != nil { + t.Fatalf("NewTestServerWithSecret: %v", err) + } event := diun.DiunEvent{Image: "nginx:latest"} body, _ := json.Marshal(event) req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader(body)) req.Header.Set("Authorization", "my-secret") rec := httptest.NewRecorder() - diun.WebhookHandler(rec, req) + srv.WebhookHandler(rec, req) if rec.Code != http.StatusOK { t.Errorf("expected 200, got %d", rec.Code) } } func TestWebhookHandler_NoSecretConfigured(t *testing.T) { - diun.UpdatesReset() - diun.ResetWebhookSecret() + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } event := diun.DiunEvent{Image: "nginx:latest"} body, _ := json.Marshal(event) req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader(body)) rec := httptest.NewRecorder() - diun.WebhookHandler(rec, req) + srv.WebhookHandler(rec, req) if rec.Code != http.StatusOK { t.Errorf("expected 200 (no secret configured), got %d", rec.Code) } } func TestWebhookHandler_BadRequest(t *testing.T) { + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader([]byte("not-json"))) rec := httptest.NewRecorder() - diun.WebhookHandler(rec, req) + srv.WebhookHandler(rec, req) if rec.Code != http.StatusBadRequest { t.Errorf("expected 400 for bad JSON, got %d", rec.Code) } } func TestUpdatesHandler(t *testing.T) { - diun.UpdatesReset() + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } event := diun.DiunEvent{Image: "busybox:latest"} - if err := diun.UpdateEvent(event); err != nil { - t.Fatalf("test setup: UpdateEvent failed: %v", err) + if err := srv.TestUpsertEvent(event); err != nil { + t.Fatalf("test setup: TestUpsertEvent failed: %v", err) } req := httptest.NewRequest(http.MethodGet, "/api/updates", nil) rec := httptest.NewRecorder() - diun.UpdatesHandler(rec, req) + srv.UpdatesHandler(rec, req) if rec.Code != http.StatusOK { t.Errorf("expected status 200, got %d", rec.Code) } @@ -175,17 +187,25 @@ func (f failWriter) Write([]byte) (int, error) { return 0, errors.New("forced er func (f failWriter) WriteHeader(_ int) {} func TestUpdatesHandler_EncodeError(t *testing.T) { + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } rec := failWriter{httptest.NewRecorder()} - diun.UpdatesHandler(rec, httptest.NewRequest(http.MethodGet, "/api/updates", nil)) + srv.UpdatesHandler(rec, httptest.NewRequest(http.MethodGet, "/api/updates", nil)) // No panic = pass } func TestWebhookHandler_MethodNotAllowed(t *testing.T) { + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } methods := []string{http.MethodGet, http.MethodPut, http.MethodDelete} for _, method := range methods { req := httptest.NewRequest(method, "/webhook", nil) rec := httptest.NewRecorder() - diun.WebhookHandler(rec, req) + srv.WebhookHandler(rec, req) if rec.Code != http.StatusMethodNotAllowed { t.Errorf("method %s: expected 405, got %d", method, rec.Code) } @@ -195,52 +215,61 @@ func TestWebhookHandler_MethodNotAllowed(t *testing.T) { body, _ := json.Marshal(event) req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader(body)) rec := httptest.NewRecorder() - diun.WebhookHandler(rec, req) + srv.WebhookHandler(rec, req) if rec.Code == http.StatusMethodNotAllowed { t.Errorf("POST should not return 405") } } func TestWebhookHandler_EmptyImage(t *testing.T) { - diun.UpdatesReset() + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } body, _ := json.Marshal(diun.DiunEvent{Image: ""}) req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader(body)) rec := httptest.NewRecorder() - diun.WebhookHandler(rec, req) + srv.WebhookHandler(rec, req) if rec.Code != http.StatusBadRequest { t.Errorf("expected 400 for empty image, got %d", rec.Code) } - if len(diun.GetUpdatesMap()) != 0 { - t.Errorf("expected map to stay empty, got %d entries", len(diun.GetUpdatesMap())) + if len(srv.TestGetUpdatesMap()) != 0 { + t.Errorf("expected map to stay empty, got %d entries", len(srv.TestGetUpdatesMap())) } } func TestConcurrentUpdateEvent(t *testing.T) { - diun.UpdatesReset() + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } const n = 100 var wg sync.WaitGroup wg.Add(n) for i := range n { go func(i int) { defer wg.Done() - if err := diun.UpdateEvent(diun.DiunEvent{Image: fmt.Sprintf("image:%d", i)}); err != nil { - t.Fatalf("test setup: UpdateEvent[%d] failed: %v", i, err) + if err := srv.TestUpsertEvent(diun.DiunEvent{Image: fmt.Sprintf("image:%d", i)}); err != nil { + t.Errorf("test setup: TestUpsertEvent[%d] failed: %v", i, err) } }(i) } wg.Wait() - if got := len(diun.GetUpdatesMap()); got != n { + if got := len(srv.TestGetUpdatesMap()); got != n { t.Errorf("expected %d entries, got %d", n, got) } } func TestMainHandlerIntegration(t *testing.T) { - diun.UpdatesReset() + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/webhook" { - diun.WebhookHandler(w, r) + srv.WebhookHandler(w, r) } else if r.URL.Path == "/api/updates" { - diun.UpdatesHandler(w, r) + srv.UpdatesHandler(w, r) } else { w.WriteHeader(http.StatusNotFound) } @@ -279,18 +308,21 @@ func TestMainHandlerIntegration(t *testing.T) { } func TestDismissHandler_Success(t *testing.T) { - diun.UpdatesReset() - if err := diun.UpdateEvent(diun.DiunEvent{Image: "nginx:latest"}); err != nil { - t.Fatalf("test setup: UpdateEvent failed: %v", err) + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } + if err := srv.TestUpsertEvent(diun.DiunEvent{Image: "nginx:latest"}); err != nil { + t.Fatalf("test setup: TestUpsertEvent failed: %v", err) } req := httptest.NewRequest(http.MethodPatch, "/api/updates/nginx:latest", nil) rec := httptest.NewRecorder() - diun.DismissHandler(rec, req) + srv.DismissHandler(rec, req) if rec.Code != http.StatusNoContent { t.Errorf("expected 204, got %d", rec.Code) } - m := diun.GetUpdatesMap() + m := srv.TestGetUpdatesMap() if len(m) != 1 { t.Errorf("expected entry to remain after acknowledge, got %d entries", len(m)) } @@ -300,38 +332,48 @@ func TestDismissHandler_Success(t *testing.T) { } func TestDismissHandler_NotFound(t *testing.T) { - diun.UpdatesReset() + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } req := httptest.NewRequest(http.MethodPatch, "/api/updates/does-not-exist:latest", nil) rec := httptest.NewRecorder() - diun.DismissHandler(rec, req) + srv.DismissHandler(rec, req) if rec.Code != http.StatusNotFound { t.Errorf("expected 404, got %d", rec.Code) } } func TestDismissHandler_EmptyImage(t *testing.T) { + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } req := httptest.NewRequest(http.MethodPatch, "/api/updates/", nil) rec := httptest.NewRecorder() - diun.DismissHandler(rec, req) + srv.DismissHandler(rec, req) if rec.Code != http.StatusBadRequest { t.Errorf("expected 400, got %d", rec.Code) } } func TestDismissHandler_SlashInImageName(t *testing.T) { - diun.UpdatesReset() - if err := diun.UpdateEvent(diun.DiunEvent{Image: "ghcr.io/user/image:tag"}); err != nil { - t.Fatalf("test setup: UpdateEvent failed: %v", err) + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } + if err := srv.TestUpsertEvent(diun.DiunEvent{Image: "ghcr.io/user/image:tag"}); err != nil { + t.Fatalf("test setup: TestUpsertEvent failed: %v", err) } req := httptest.NewRequest(http.MethodPatch, "/api/updates/ghcr.io/user/image:tag", nil) rec := httptest.NewRecorder() - diun.DismissHandler(rec, req) + srv.DismissHandler(rec, req) if rec.Code != http.StatusNoContent { t.Errorf("expected 204, got %d", rec.Code) } - m := diun.GetUpdatesMap() + m := srv.TestGetUpdatesMap() if len(m) != 1 { t.Errorf("expected entry to remain after acknowledge, got %d entries", len(m)) } @@ -341,23 +383,28 @@ func TestDismissHandler_SlashInImageName(t *testing.T) { } func TestDismissHandler_ReappearsAfterNewWebhook(t *testing.T) { - diun.UpdatesReset() - if err := diun.UpdateEvent(diun.DiunEvent{Image: "nginx:latest"}); err != nil { - t.Fatalf("test setup: UpdateEvent failed: %v", err) + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } + if err := srv.TestUpsertEvent(diun.DiunEvent{Image: "nginx:latest"}); err != nil { + t.Fatalf("test setup: TestUpsertEvent failed: %v", err) } req := httptest.NewRequest(http.MethodPatch, "/api/updates/nginx:latest", nil) rec := httptest.NewRecorder() - diun.DismissHandler(rec, req) + srv.DismissHandler(rec, req) if rec.Code != http.StatusNoContent { t.Fatalf("expected 204 on acknowledge, got %d", rec.Code) } - if !diun.GetUpdatesMap()["nginx:latest"].Acknowledged { + if !srv.TestGetUpdatesMap()["nginx:latest"].Acknowledged { t.Errorf("expected entry to be acknowledged after PATCH") } - diun.UpdateEvent(diun.DiunEvent{Image: "nginx:latest", Status: "update"}) - m := diun.GetUpdatesMap() + if err := srv.TestUpsertEvent(diun.DiunEvent{Image: "nginx:latest", Status: "update"}); err != nil { + t.Fatalf("second TestUpsertEvent failed: %v", err) + } + m := srv.TestGetUpdatesMap() if len(m) != 1 { t.Errorf("expected entry to remain, got %d entries", len(m)) } @@ -371,21 +418,21 @@ func TestDismissHandler_ReappearsAfterNewWebhook(t *testing.T) { // --- Tag handler tests --- -func postTag(t *testing.T, name string) (int, int) { +func postTag(t *testing.T, srv *diun.Server, name string) (int, int) { t.Helper() body, _ := json.Marshal(map[string]string{"name": name}) req := httptest.NewRequest(http.MethodPost, "/api/tags", bytes.NewReader(body)) rec := httptest.NewRecorder() - diun.TagsHandler(rec, req) + srv.TagsHandler(rec, req) return rec.Code, rec.Body.Len() } -func postTagAndGetID(t *testing.T, name string) int { +func postTagAndGetID(t *testing.T, srv *diun.Server, name string) int { t.Helper() body, _ := json.Marshal(map[string]string{"name": name}) req := httptest.NewRequest(http.MethodPost, "/api/tags", bytes.NewReader(body)) rec := httptest.NewRecorder() - diun.TagsHandler(rec, req) + srv.TagsHandler(rec, req) if rec.Code != http.StatusCreated { t.Fatalf("expected 201 creating tag %q, got %d", name, rec.Code) } @@ -395,11 +442,14 @@ func postTagAndGetID(t *testing.T, name string) int { } func TestCreateTagHandler_Success(t *testing.T) { - diun.UpdatesReset() + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } body, _ := json.Marshal(map[string]string{"name": "nextcloud"}) req := httptest.NewRequest(http.MethodPost, "/api/tags", bytes.NewReader(body)) rec := httptest.NewRecorder() - diun.TagsHandler(rec, req) + srv.TagsHandler(rec, req) if rec.Code != http.StatusCreated { t.Fatalf("expected 201, got %d", rec.Code) } @@ -416,30 +466,39 @@ func TestCreateTagHandler_Success(t *testing.T) { } func TestCreateTagHandler_DuplicateName(t *testing.T) { - diun.UpdatesReset() - postTag(t, "monitoring") - code, _ := postTag(t, "monitoring") + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } + postTag(t, srv, "monitoring") + code, _ := postTag(t, srv, "monitoring") if code != http.StatusConflict { t.Errorf("expected 409, got %d", code) } } func TestCreateTagHandler_EmptyName(t *testing.T) { - diun.UpdatesReset() + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } body, _ := json.Marshal(map[string]string{"name": ""}) req := httptest.NewRequest(http.MethodPost, "/api/tags", bytes.NewReader(body)) rec := httptest.NewRecorder() - diun.TagsHandler(rec, req) + srv.TagsHandler(rec, req) if rec.Code != http.StatusBadRequest { t.Errorf("expected 400, got %d", rec.Code) } } func TestGetTagsHandler_Empty(t *testing.T) { - diun.UpdatesReset() + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } req := httptest.NewRequest(http.MethodGet, "/api/tags", nil) rec := httptest.NewRecorder() - diun.TagsHandler(rec, req) + srv.TagsHandler(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d", rec.Code) } @@ -451,12 +510,15 @@ func TestGetTagsHandler_Empty(t *testing.T) { } func TestGetTagsHandler_WithTags(t *testing.T) { - diun.UpdatesReset() - postTag(t, "alpha") - postTag(t, "beta") + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } + postTag(t, srv, "alpha") + postTag(t, srv, "beta") req := httptest.NewRequest(http.MethodGet, "/api/tags", nil) rec := httptest.NewRecorder() - diun.TagsHandler(rec, req) + srv.TagsHandler(rec, req) if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d", rec.Code) } @@ -468,36 +530,47 @@ func TestGetTagsHandler_WithTags(t *testing.T) { } func TestDeleteTagHandler_Success(t *testing.T) { - diun.UpdatesReset() - id := postTagAndGetID(t, "to-delete") + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } + id := postTagAndGetID(t, srv, "to-delete") req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/api/tags/%d", id), nil) rec := httptest.NewRecorder() - diun.TagByIDHandler(rec, req) + srv.TagByIDHandler(rec, req) if rec.Code != http.StatusNoContent { t.Errorf("expected 204, got %d", rec.Code) } } func TestDeleteTagHandler_NotFound(t *testing.T) { - diun.UpdatesReset() + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } req := httptest.NewRequest(http.MethodDelete, "/api/tags/9999", nil) rec := httptest.NewRecorder() - diun.TagByIDHandler(rec, req) + srv.TagByIDHandler(rec, req) if rec.Code != http.StatusNotFound { t.Errorf("expected 404, got %d", rec.Code) } } func TestDeleteTagHandler_CascadesAssignment(t *testing.T) { - diun.UpdatesReset() - diun.UpdateEvent(diun.DiunEvent{Image: "nginx:latest"}) - id := postTagAndGetID(t, "cascade-test") + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } + if err := srv.TestUpsertEvent(diun.DiunEvent{Image: "nginx:latest"}); err != nil { + t.Fatalf("test setup: TestUpsertEvent failed: %v", err) + } + id := postTagAndGetID(t, srv, "cascade-test") // Assign the tag body, _ := json.Marshal(map[string]interface{}{"image": "nginx:latest", "tag_id": id}) req := httptest.NewRequest(http.MethodPut, "/api/tag-assignments", bytes.NewReader(body)) rec := httptest.NewRecorder() - diun.TagAssignmentHandler(rec, req) + srv.TagAssignmentHandler(rec, req) if rec.Code != http.StatusNoContent { t.Fatalf("expected 204 on assign, got %d", rec.Code) } @@ -505,43 +578,53 @@ func TestDeleteTagHandler_CascadesAssignment(t *testing.T) { // Delete the tag req = httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/api/tags/%d", id), nil) rec = httptest.NewRecorder() - diun.TagByIDHandler(rec, req) + srv.TagByIDHandler(rec, req) if rec.Code != http.StatusNoContent { t.Fatalf("expected 204 on delete, got %d", rec.Code) } // Confirm assignment cascaded - m := diun.GetUpdatesMap() + m := srv.TestGetUpdatesMap() if m["nginx:latest"].Tag != nil { t.Errorf("expected tag to be nil after cascade delete, got %+v", m["nginx:latest"].Tag) } } func TestTagAssignmentHandler_Assign(t *testing.T) { - diun.UpdatesReset() - diun.UpdateEvent(diun.DiunEvent{Image: "alpine:latest"}) - id := postTagAndGetID(t, "assign-test") + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } + if err := srv.TestUpsertEvent(diun.DiunEvent{Image: "alpine:latest"}); err != nil { + t.Fatalf("test setup: TestUpsertEvent failed: %v", err) + } + id := postTagAndGetID(t, srv, "assign-test") body, _ := json.Marshal(map[string]interface{}{"image": "alpine:latest", "tag_id": id}) req := httptest.NewRequest(http.MethodPut, "/api/tag-assignments", bytes.NewReader(body)) rec := httptest.NewRecorder() - diun.TagAssignmentHandler(rec, req) + srv.TagAssignmentHandler(rec, req) if rec.Code != http.StatusNoContent { t.Errorf("expected 204, got %d", rec.Code) } } func TestTagAssignmentHandler_Reassign(t *testing.T) { - diun.UpdatesReset() - diun.UpdateEvent(diun.DiunEvent{Image: "redis:latest"}) - id1 := postTagAndGetID(t, "group-a") - id2 := postTagAndGetID(t, "group-b") + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } + if err := srv.TestUpsertEvent(diun.DiunEvent{Image: "redis:latest"}); err != nil { + t.Fatalf("test setup: TestUpsertEvent failed: %v", err) + } + id1 := postTagAndGetID(t, srv, "group-a") + id2 := postTagAndGetID(t, srv, "group-b") assign := func(tagID int) { body, _ := json.Marshal(map[string]interface{}{"image": "redis:latest", "tag_id": tagID}) req := httptest.NewRequest(http.MethodPut, "/api/tag-assignments", bytes.NewReader(body)) rec := httptest.NewRecorder() - diun.TagAssignmentHandler(rec, req) + srv.TagAssignmentHandler(rec, req) if rec.Code != http.StatusNoContent { t.Fatalf("expected 204, got %d", rec.Code) } @@ -550,51 +633,61 @@ func TestTagAssignmentHandler_Reassign(t *testing.T) { assign(id1) assign(id2) - m := diun.GetUpdatesMap() + m := srv.TestGetUpdatesMap() if m["redis:latest"].Tag == nil || m["redis:latest"].Tag.ID != id2 { t.Errorf("expected tag id %d after reassign, got %+v", id2, m["redis:latest"].Tag) } } func TestTagAssignmentHandler_Unassign(t *testing.T) { - diun.UpdatesReset() - diun.UpdateEvent(diun.DiunEvent{Image: "busybox:latest"}) - id := postTagAndGetID(t, "unassign-test") + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } + if err := srv.TestUpsertEvent(diun.DiunEvent{Image: "busybox:latest"}); err != nil { + t.Fatalf("test setup: TestUpsertEvent failed: %v", err) + } + id := postTagAndGetID(t, srv, "unassign-test") body, _ := json.Marshal(map[string]interface{}{"image": "busybox:latest", "tag_id": id}) req := httptest.NewRequest(http.MethodPut, "/api/tag-assignments", bytes.NewReader(body)) rec := httptest.NewRecorder() - diun.TagAssignmentHandler(rec, req) + srv.TagAssignmentHandler(rec, req) // Now unassign body, _ = json.Marshal(map[string]string{"image": "busybox:latest"}) req = httptest.NewRequest(http.MethodDelete, "/api/tag-assignments", bytes.NewReader(body)) rec = httptest.NewRecorder() - diun.TagAssignmentHandler(rec, req) + srv.TagAssignmentHandler(rec, req) if rec.Code != http.StatusNoContent { t.Errorf("expected 204, got %d", rec.Code) } - m := diun.GetUpdatesMap() + m := srv.TestGetUpdatesMap() if m["busybox:latest"].Tag != nil { t.Errorf("expected tag nil after unassign, got %+v", m["busybox:latest"].Tag) } } func TestGetUpdates_IncludesTag(t *testing.T) { - diun.UpdatesReset() - diun.UpdateEvent(diun.DiunEvent{Image: "postgres:latest"}) - id := postTagAndGetID(t, "databases") + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } + if err := srv.TestUpsertEvent(diun.DiunEvent{Image: "postgres:latest"}); err != nil { + t.Fatalf("test setup: TestUpsertEvent failed: %v", err) + } + id := postTagAndGetID(t, srv, "databases") body, _ := json.Marshal(map[string]interface{}{"image": "postgres:latest", "tag_id": id}) req := httptest.NewRequest(http.MethodPut, "/api/tag-assignments", bytes.NewReader(body)) rec := httptest.NewRecorder() - diun.TagAssignmentHandler(rec, req) + srv.TagAssignmentHandler(rec, req) if rec.Code != http.StatusNoContent { t.Fatalf("expected 204, got %d", rec.Code) } - m := diun.GetUpdatesMap() + m := srv.TestGetUpdatesMap() entry, ok := m["postgres:latest"] if !ok { t.Fatal("expected postgres:latest in updates") @@ -611,6 +704,10 @@ func TestGetUpdates_IncludesTag(t *testing.T) { } func TestWebhookHandler_OversizedBody(t *testing.T) { + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } // Generate a body that exceeds 1 MB (maxBodyBytes = 1<<20 = 1,048,576 bytes). // Use a valid JSON prefix so the decoder reads past the limit before failing, // ensuring MaxBytesReader triggers a 413 rather than a JSON parse 400. @@ -619,50 +716,61 @@ func TestWebhookHandler_OversizedBody(t *testing.T) { oversized := append(prefix, padding...) req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader(oversized)) rec := httptest.NewRecorder() - diun.WebhookHandler(rec, req) + srv.WebhookHandler(rec, req) if rec.Code != http.StatusRequestEntityTooLarge { t.Errorf("expected 413 for oversized body, got %d", rec.Code) } } func TestTagsHandler_OversizedBody(t *testing.T) { + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } prefix := []byte(`{"name":"`) padding := bytes.Repeat([]byte("x"), 1<<20+1) oversized := append(prefix, padding...) req := httptest.NewRequest(http.MethodPost, "/api/tags", bytes.NewReader(oversized)) rec := httptest.NewRecorder() - diun.TagsHandler(rec, req) + srv.TagsHandler(rec, req) if rec.Code != http.StatusRequestEntityTooLarge { t.Errorf("expected 413 for oversized body, got %d", rec.Code) } } func TestTagAssignmentHandler_OversizedBody(t *testing.T) { + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } prefix := []byte(`{"image":"`) padding := bytes.Repeat([]byte("x"), 1<<20+1) oversized := append(prefix, padding...) req := httptest.NewRequest(http.MethodPut, "/api/tag-assignments", bytes.NewReader(oversized)) rec := httptest.NewRecorder() - diun.TagAssignmentHandler(rec, req) + srv.TagAssignmentHandler(rec, req) if rec.Code != http.StatusRequestEntityTooLarge { t.Errorf("expected 413 for oversized body, got %d", rec.Code) } } func TestUpdateEvent_PreservesTagOnUpsert(t *testing.T) { - diun.UpdatesReset() + srv, err := diun.NewTestServer() + if err != nil { + t.Fatalf("NewTestServer: %v", err) + } // Insert image - if err := diun.UpdateEvent(diun.DiunEvent{Image: "nginx:latest", Status: "new"}); err != nil { - t.Fatalf("first UpdateEvent failed: %v", err) + if err := srv.TestUpsertEvent(diun.DiunEvent{Image: "nginx:latest", Status: "new"}); err != nil { + t.Fatalf("first TestUpsertEvent failed: %v", err) } // Assign tag - tagID := postTagAndGetID(t, "webservers") + tagID := postTagAndGetID(t, srv, "webservers") body, _ := json.Marshal(map[string]interface{}{"image": "nginx:latest", "tag_id": tagID}) req := httptest.NewRequest(http.MethodPut, "/api/tag-assignments", bytes.NewReader(body)) rec := httptest.NewRecorder() - diun.TagAssignmentHandler(rec, req) + srv.TagAssignmentHandler(rec, req) if rec.Code != http.StatusNoContent { t.Fatalf("tag assignment failed: got %d", rec.Code) } @@ -670,24 +778,24 @@ func TestUpdateEvent_PreservesTagOnUpsert(t *testing.T) { // Dismiss (acknowledge) the image — second event must reset this req = httptest.NewRequest(http.MethodPatch, "/api/updates/nginx:latest", nil) rec = httptest.NewRecorder() - diun.DismissHandler(rec, req) + srv.DismissHandler(rec, req) if rec.Code != http.StatusNoContent { t.Fatalf("dismiss failed: got %d", rec.Code) } // Receive a second event for the same image - if err := diun.UpdateEvent(diun.DiunEvent{Image: "nginx:latest", Status: "update"}); err != nil { - t.Fatalf("second UpdateEvent failed: %v", err) + if err := srv.TestUpsertEvent(diun.DiunEvent{Image: "nginx:latest", Status: "update"}); err != nil { + t.Fatalf("second TestUpsertEvent failed: %v", err) } // Tag must survive the second event - m := diun.GetUpdatesMap() + m := srv.TestGetUpdatesMap() entry, ok := m["nginx:latest"] if !ok { t.Fatal("nginx:latest missing from updates after second event") } if entry.Tag == nil { - t.Error("tag was lost after second UpdateEvent — UPSERT bug not fixed") + t.Error("tag was lost after second TestUpsertEvent — UPSERT bug not fixed") } if entry.Tag != nil && entry.Tag.ID != tagID { t.Errorf("tag ID changed: expected %d, got %d", tagID, entry.Tag.ID)