Refactor project structure: enhance tests, improve server shutdown, expand CI checks, and update UI for better event presentation.
This commit is contained in:
@@ -14,6 +14,18 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
- name: Check formatting
|
||||
run: |
|
||||
unformatted=$(gofmt -l .)
|
||||
if [ -n "$unformatted" ]; then
|
||||
echo "Unformatted files:"
|
||||
echo "$unformatted"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Run go vet
|
||||
run: go vet ./...
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: |
|
||||
go test -v -coverprofile=coverage.out -coverpkg=./... ./...
|
||||
|
||||
@@ -1,17 +1,54 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
diun "awesomeProject/pkg/diunwebhook"
|
||||
)
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/webhook", diun.WebhookHandler)
|
||||
http.HandleFunc("/api/updates", diun.UpdatesHandler)
|
||||
http.Handle("/", http.FileServer(http.Dir("./static")))
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "8080"
|
||||
}
|
||||
|
||||
log.Println("Listening on :8080")
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/webhook", diun.WebhookHandler)
|
||||
mux.HandleFunc("/api/updates", diun.UpdatesHandler)
|
||||
mux.Handle("/", http.FileServer(http.Dir("./static")))
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: ":" + port,
|
||||
Handler: mux,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
log.Printf("Listening on :%s", port)
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("ListenAndServe: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
<-stop
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
log.Printf("Shutdown error: %v", err)
|
||||
} else {
|
||||
log.Println("Server stopped cleanly")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -104,6 +106,58 @@ func TestUpdatesHandler_EncodeError(t *testing.T) {
|
||||
// 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
|
||||
12
pkg/diunwebhook/export_test.go
Normal file
12
pkg/diunwebhook/export_test.go
Normal 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)
|
||||
}
|
||||
@@ -15,9 +15,11 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Image</th>
|
||||
<th>Tag</th>
|
||||
<th>Status</th>
|
||||
<th>Last Seen</th>
|
||||
<th>Hostname</th>
|
||||
<th>Provider</th>
|
||||
<th>Digest</th>
|
||||
<th>Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
@@ -28,16 +30,24 @@
|
||||
const res = await fetch('/api/updates');
|
||||
const data = await res.json();
|
||||
const tbody = document.querySelector('tbody');
|
||||
tbody.innerHTML = '';
|
||||
tbody.textContent = '';
|
||||
|
||||
Object.values(data).forEach(update => {
|
||||
const row = `<tr>
|
||||
<td>${update.image}</td>
|
||||
<td>${update.tag}</td>
|
||||
<td>${update.status}</td>
|
||||
<td>${update.time}</td>
|
||||
</tr>`;
|
||||
tbody.innerHTML += row;
|
||||
const row = document.createElement('tr');
|
||||
const fields = [
|
||||
update.image,
|
||||
update.status,
|
||||
update.hostname,
|
||||
update.provider,
|
||||
update.digest,
|
||||
update.created ? new Date(update.created).toLocaleString() : '',
|
||||
];
|
||||
fields.forEach(value => {
|
||||
const td = document.createElement('td');
|
||||
td.textContent = value || '';
|
||||
row.appendChild(td);
|
||||
});
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,4 +55,4 @@
|
||||
load();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user