Refactor project structure: enhance tests, improve server shutdown, expand CI checks, and update UI for better event presentation.
Some checks failed
CI / build-test (push) Failing after 4s
CI / docker (push) Has been skipped

This commit is contained in:
2026-02-23 21:12:39 +01:00
parent d38a0e7044
commit e4f32132e3
7 changed files with 153 additions and 28 deletions

View File

@@ -32,17 +32,6 @@ var (
updates = make(map[string]DiunEvent)
)
// Exported for test package
func GetUpdatesMap() map[string]DiunEvent {
return updates
}
func UpdatesReset() {
mu.Lock()
defer mu.Unlock()
updates = make(map[string]DiunEvent)
}
func UpdateEvent(event DiunEvent) {
mu.Lock()
defer mu.Unlock()
@@ -60,10 +49,21 @@ func GetUpdates() map[string]DiunEvent {
}
func WebhookHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var event DiunEvent
if err := json.NewDecoder(r.Body).Decode(&event); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
log.Printf("WebhookHandler: failed to decode request: %v", err)
http.Error(w, "bad request", http.StatusBadRequest)
return
}
if event.Image == "" {
http.Error(w, "bad request: image field is required", http.StatusBadRequest)
return
}

View File

@@ -0,0 +1,206 @@
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) {
diun.UpdatesReset() // helper to reset global state
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",
}
diun.UpdateEvent(event)
got := diun.GetUpdates()
if len(got) != 1 {
t.Fatalf("expected 1 update, got %d", len(got))
}
if got["nginx:latest"].DiunVersion != "1.0" {
t.Errorf("unexpected DiunVersion: %s", got["nginx:latest"].DiunVersion)
}
}
func TestWebhookHandler(t *testing.T) {
diun.UpdatesReset() // reset global state
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()
diun.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()))
}
}
func TestWebhookHandler_BadRequest(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader([]byte("not-json")))
rec := httptest.NewRecorder()
diun.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() // reset global state
event := diun.DiunEvent{Image: "busybox:latest"}
diun.UpdateEvent(event)
req := httptest.NewRequest(http.MethodGet, "/api/updates", nil)
rec := httptest.NewRecorder()
diun.UpdatesHandler(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", rec.Code)
}
var got map[string]diun.DiunEvent
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) {
rec := failWriter{httptest.NewRecorder()}
diun.UpdatesHandler(rec, httptest.NewRequest(http.MethodGet, "/api/updates", nil))
// No panic = pass
}
func TestWebhookHandler_MethodNotAllowed(t *testing.T) {
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)
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()
diun.WebhookHandler(rec, req)
if rec.Code == http.StatusMethodNotAllowed {
t.Errorf("POST should not return 405")
}
}
func TestWebhookHandler_EmptyImage(t *testing.T) {
diun.UpdatesReset()
body, _ := json.Marshal(diun.DiunEvent{Image: ""})
req := httptest.NewRequest(http.MethodPost, "/webhook", bytes.NewReader(body))
rec := httptest.NewRecorder()
diun.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()))
}
}
func TestConcurrentUpdateEvent(t *testing.T) {
diun.UpdatesReset()
const n = 100
var wg sync.WaitGroup
wg.Add(n)
for i := range n {
go func(i int) {
defer wg.Done()
diun.UpdateEvent(diun.DiunEvent{Image: fmt.Sprintf("image:%d", i)})
}(i)
}
wg.Wait()
if got := len(diun.GetUpdatesMap()); got != n {
t.Errorf("expected %d entries, got %d", n, got)
}
}
func TestMainHandlerIntegration(t *testing.T) {
diun.UpdatesReset() // reset global state
// Start test server
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/webhook" {
diun.WebhookHandler(w, r)
} else if r.URL.Path == "/api/updates" {
diun.UpdatesHandler(w, r)
} else {
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
// Post event
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)
}
// Get updates
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.DiunEvent
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")
}
}

View File

@@ -0,0 +1,12 @@
package diunwebhook
func GetUpdatesMap() map[string]DiunEvent {
mu.Lock()
defer mu.Unlock()
return updates
}
func UpdatesReset() {
mu.Lock()
defer mu.Unlock()
updates = make(map[string]DiunEvent)
}