Files
DiunDashboard/pkg/diunwebhook/diunwebhook_test.go
Jean-Luc Makiola e35b4f882d
All checks were successful
CI / build-test (push) Successful in 1m42s
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
2026-03-23 22:05:09 +01:00

812 lines
25 KiB
Go

package diunwebhook_test
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
diun "awesomeProject/pkg/diunwebhook"
)
func TestUpdateEventAndGetUpdates(t *testing.T) {
srv, err := diun.NewTestServer()
if err != nil {
t.Fatalf("NewTestServer: %v", err)
}
event := diun.DiunEvent{
DiunVersion: "1.0",
Hostname: "host",
Status: "new",
Provider: "docker",
Image: "nginx:latest",
HubLink: "https://hub.docker.com/nginx",
MimeType: "application/json",
Digest: "sha256:abc",
Created: time.Now(),
Platform: "linux/amd64",
}
if err := srv.TestUpsertEvent(event); err != nil {
t.Fatalf("test setup: TestUpsertEvent failed: %v", err)
}
got, err := srv.TestGetUpdates()
if err != nil {
t.Fatalf("TestGetUpdates error: %v", err)
}
if len(got) != 1 {
t.Fatalf("expected 1 update, got %d", len(got))
}
if got["nginx:latest"].Event.DiunVersion != "1.0" {
t.Errorf("unexpected DiunVersion: %s", got["nginx:latest"].Event.DiunVersion)
}
}
func TestWebhookHandler(t *testing.T) {
srv, err := diun.NewTestServer()
if err != nil {
t.Fatalf("NewTestServer: %v", err)
}
event := diun.DiunEvent{
DiunVersion: "2.0",
Hostname: "host2",
Status: "updated",
Provider: "docker",
Image: "alpine:latest",
HubLink: "https://hub.docker.com/alpine",
MimeType: "application/json",
Digest: "sha256:def",
Created: time.Now(),
Platform: "linux/amd64",
}
body, _ := json.Marshal(event)
req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader(body))
rec := httptest.NewRecorder()
srv.WebhookHandler(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", rec.Code)
}
if len(srv.TestGetUpdatesMap()) != 1 {
t.Errorf("expected 1 update, got %d", len(srv.TestGetUpdatesMap()))
}
}
func TestWebhookHandler_Unauthorized(t *testing.T) {
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()
srv.WebhookHandler(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Errorf("expected 401, got %d", rec.Code)
}
}
func TestWebhookHandler_WrongToken(t *testing.T) {
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()
srv.WebhookHandler(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Errorf("expected 401, got %d", rec.Code)
}
}
func TestWebhookHandler_ValidToken(t *testing.T) {
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()
srv.WebhookHandler(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("expected 200, got %d", rec.Code)
}
}
func TestWebhookHandler_NoSecretConfigured(t *testing.T) {
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()
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()
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) {
srv, err := diun.NewTestServer()
if err != nil {
t.Fatalf("NewTestServer: %v", err)
}
event := diun.DiunEvent{Image: "busybox:latest"}
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()
srv.UpdatesHandler(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", rec.Code)
}
var got map[string]diun.UpdateEntry
if err := json.NewDecoder(rec.Body).Decode(&got); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if _, ok := got["busybox:latest"]; !ok {
t.Errorf("expected busybox:latest in updates")
}
}
// Helper for simulating a broken ResponseWriter
// Used in TestUpdatesHandler_EncodeError
type failWriter struct{ http.ResponseWriter }
func (f failWriter) Header() http.Header { return http.Header{} }
func (f failWriter) Write([]byte) (int, error) { return 0, errors.New("forced error") }
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()}
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()
srv.WebhookHandler(rec, req)
if rec.Code != http.StatusMethodNotAllowed {
t.Errorf("method %s: expected 405, got %d", method, rec.Code)
}
}
// POST should not return 405
event := diun.DiunEvent{Image: "nginx:latest"}
body, _ := json.Marshal(event)
req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader(body))
rec := httptest.NewRecorder()
srv.WebhookHandler(rec, req)
if rec.Code == http.StatusMethodNotAllowed {
t.Errorf("POST should not return 405")
}
}
func TestWebhookHandler_EmptyImage(t *testing.T) {
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()
srv.WebhookHandler(rec, req)
if rec.Code != http.StatusBadRequest {
t.Errorf("expected 400 for empty image, got %d", rec.Code)
}
if len(srv.TestGetUpdatesMap()) != 0 {
t.Errorf("expected map to stay empty, got %d entries", len(srv.TestGetUpdatesMap()))
}
}
func TestConcurrentUpdateEvent(t *testing.T) {
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 := 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(srv.TestGetUpdatesMap()); got != n {
t.Errorf("expected %d entries, got %d", n, got)
}
}
func TestMainHandlerIntegration(t *testing.T) {
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" {
srv.WebhookHandler(w, r)
} else if r.URL.Path == "/api/updates" {
srv.UpdatesHandler(w, r)
} else {
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
event := diun.DiunEvent{Image: "integration:latest"}
body, _ := json.Marshal(event)
resp, err := http.Post(ts.URL+"/webhook", "application/json", bytes.NewReader(body))
if err != nil {
t.Fatalf("webhook POST failed: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("webhook POST returned status: %d", resp.StatusCode)
}
if cerr := resp.Body.Close(); cerr != nil {
t.Errorf("failed to close response body: %v", cerr)
}
resp, err = http.Get(ts.URL + "/api/updates")
if err != nil {
t.Fatalf("GET /api/updates failed: %v", err)
}
defer func() {
if cerr := resp.Body.Close(); cerr != nil {
t.Errorf("failed to close response body: %v", cerr)
}
}()
var got map[string]diun.UpdateEntry
if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
t.Fatalf("decode failed: %v", err)
}
if _, ok := got["integration:latest"]; !ok {
t.Errorf("expected integration:latest in updates")
}
}
func TestDismissHandler_Success(t *testing.T) {
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()
srv.DismissHandler(rec, req)
if rec.Code != http.StatusNoContent {
t.Errorf("expected 204, got %d", rec.Code)
}
m := srv.TestGetUpdatesMap()
if len(m) != 1 {
t.Errorf("expected entry to remain after acknowledge, got %d entries", len(m))
}
if !m["nginx:latest"].Acknowledged {
t.Errorf("expected entry to be acknowledged")
}
}
func TestDismissHandler_NotFound(t *testing.T) {
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()
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()
srv.DismissHandler(rec, req)
if rec.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d", rec.Code)
}
}
func TestDismissHandler_SlashInImageName(t *testing.T) {
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()
srv.DismissHandler(rec, req)
if rec.Code != http.StatusNoContent {
t.Errorf("expected 204, got %d", rec.Code)
}
m := srv.TestGetUpdatesMap()
if len(m) != 1 {
t.Errorf("expected entry to remain after acknowledge, got %d entries", len(m))
}
if !m["ghcr.io/user/image:tag"].Acknowledged {
t.Errorf("expected entry to be acknowledged")
}
}
func TestDismissHandler_ReappearsAfterNewWebhook(t *testing.T) {
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()
srv.DismissHandler(rec, req)
if rec.Code != http.StatusNoContent {
t.Fatalf("expected 204 on acknowledge, got %d", rec.Code)
}
if !srv.TestGetUpdatesMap()["nginx:latest"].Acknowledged {
t.Errorf("expected entry to be acknowledged after PATCH")
}
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))
}
if m["nginx:latest"].Event.Status != "update" {
t.Errorf("unexpected status: %s", m["nginx:latest"].Event.Status)
}
if m["nginx:latest"].Acknowledged {
t.Errorf("expected acknowledged to be reset after new webhook")
}
}
// --- Tag handler tests ---
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()
srv.TagsHandler(rec, req)
return rec.Code, rec.Body.Len()
}
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()
srv.TagsHandler(rec, req)
if rec.Code != http.StatusCreated {
t.Fatalf("expected 201 creating tag %q, got %d", name, rec.Code)
}
var tag diun.Tag
json.NewDecoder(rec.Body).Decode(&tag)
return tag.ID
}
func TestCreateTagHandler_Success(t *testing.T) {
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()
srv.TagsHandler(rec, req)
if rec.Code != http.StatusCreated {
t.Fatalf("expected 201, got %d", rec.Code)
}
var tag diun.Tag
if err := json.NewDecoder(rec.Body).Decode(&tag); err != nil {
t.Fatalf("decode failed: %v", err)
}
if tag.Name != "nextcloud" {
t.Errorf("expected name 'nextcloud', got %q", tag.Name)
}
if tag.ID == 0 {
t.Errorf("expected non-zero ID")
}
}
func TestCreateTagHandler_DuplicateName(t *testing.T) {
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) {
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()
srv.TagsHandler(rec, req)
if rec.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d", rec.Code)
}
}
func TestGetTagsHandler_Empty(t *testing.T) {
srv, err := diun.NewTestServer()
if err != nil {
t.Fatalf("NewTestServer: %v", err)
}
req := httptest.NewRequest(http.MethodGet, "/api/tags", nil)
rec := httptest.NewRecorder()
srv.TagsHandler(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", rec.Code)
}
var tags []diun.Tag
json.NewDecoder(rec.Body).Decode(&tags)
if len(tags) != 0 {
t.Errorf("expected empty list, got %d tags", len(tags))
}
}
func TestGetTagsHandler_WithTags(t *testing.T) {
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()
srv.TagsHandler(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", rec.Code)
}
var tags []diun.Tag
json.NewDecoder(rec.Body).Decode(&tags)
if len(tags) != 2 {
t.Errorf("expected 2 tags, got %d", len(tags))
}
}
func TestDeleteTagHandler_Success(t *testing.T) {
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()
srv.TagByIDHandler(rec, req)
if rec.Code != http.StatusNoContent {
t.Errorf("expected 204, got %d", rec.Code)
}
}
func TestDeleteTagHandler_NotFound(t *testing.T) {
srv, err := diun.NewTestServer()
if err != nil {
t.Fatalf("NewTestServer: %v", err)
}
req := httptest.NewRequest(http.MethodDelete, "/api/tags/9999", nil)
rec := httptest.NewRecorder()
srv.TagByIDHandler(rec, req)
if rec.Code != http.StatusNotFound {
t.Errorf("expected 404, got %d", rec.Code)
}
}
func TestDeleteTagHandler_CascadesAssignment(t *testing.T) {
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()
srv.TagAssignmentHandler(rec, req)
if rec.Code != http.StatusNoContent {
t.Fatalf("expected 204 on assign, got %d", rec.Code)
}
// Delete the tag
req = httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/api/tags/%d", id), nil)
rec = httptest.NewRecorder()
srv.TagByIDHandler(rec, req)
if rec.Code != http.StatusNoContent {
t.Fatalf("expected 204 on delete, got %d", rec.Code)
}
// Confirm assignment cascaded
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) {
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()
srv.TagAssignmentHandler(rec, req)
if rec.Code != http.StatusNoContent {
t.Errorf("expected 204, got %d", rec.Code)
}
}
func TestTagAssignmentHandler_Reassign(t *testing.T) {
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()
srv.TagAssignmentHandler(rec, req)
if rec.Code != http.StatusNoContent {
t.Fatalf("expected 204, got %d", rec.Code)
}
}
assign(id1)
assign(id2)
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) {
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()
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()
srv.TagAssignmentHandler(rec, req)
if rec.Code != http.StatusNoContent {
t.Errorf("expected 204, got %d", rec.Code)
}
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) {
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()
srv.TagAssignmentHandler(rec, req)
if rec.Code != http.StatusNoContent {
t.Fatalf("expected 204, got %d", rec.Code)
}
m := srv.TestGetUpdatesMap()
entry, ok := m["postgres:latest"]
if !ok {
t.Fatal("expected postgres:latest in updates")
}
if entry.Tag == nil {
t.Fatal("expected tag to be set")
}
if entry.Tag.Name != "databases" {
t.Errorf("expected tag name 'databases', got %q", entry.Tag.Name)
}
if entry.Tag.ID != id {
t.Errorf("expected tag id %d, got %d", id, entry.Tag.ID)
}
}
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.
prefix := []byte(`{"image":"`)
padding := bytes.Repeat([]byte("x"), 1<<20+1)
oversized := append(prefix, padding...)
req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader(oversized))
rec := httptest.NewRecorder()
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()
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()
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) {
srv, err := diun.NewTestServer()
if err != nil {
t.Fatalf("NewTestServer: %v", err)
}
// Insert image
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, 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()
srv.TagAssignmentHandler(rec, req)
if rec.Code != http.StatusNoContent {
t.Fatalf("tag assignment failed: got %d", rec.Code)
}
// Dismiss (acknowledge) the image — second event must reset this
req = httptest.NewRequest(http.MethodPatch, "/api/updates/nginx:latest", nil)
rec = httptest.NewRecorder()
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 := 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 := 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 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)
}
// Acknowledged state must be reset by the new event
if entry.Acknowledged {
t.Error("acknowledged state must be reset by new event")
}
// Status must reflect the new event
if entry.Event.Status != "update" {
t.Errorf("expected status 'update', got %q", entry.Event.Status)
}
}