Scaffolding baseline

This commit is contained in:
2026-07-16 20:47:33 +02:00
parent e9cbc56903
commit 38b4e8fd43
32 changed files with 3114 additions and 1 deletions
+46
View File
@@ -0,0 +1,46 @@
package main
import (
"context"
"errors"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"codeberg.org/SquidSE/ClusterCanvas/service/internal/api"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
server := &http.Server{
Addr: ":" + port,
Handler: api.NewRouter(),
ReadHeaderTimeout: 5 * time.Second,
}
go func() {
log.Printf("clustercanvas service listening on %s", server.Addr)
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("listen: %v", err)
}
}()
stopSignals := make(chan os.Signal, 1)
signal.Notify(stopSignals, syscall.SIGINT, syscall.SIGTERM)
<-stopSignals
shutdownContext, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(shutdownContext); err != nil {
log.Fatalf("shutdown: %v", err)
}
log.Print("server stopped")
}
+3
View File
@@ -0,0 +1,3 @@
module codeberg.org/SquidSE/ClusterCanvas/service
go 1.22.12
+34
View File
@@ -0,0 +1,34 @@
package api
import (
"encoding/json"
"net/http"
)
const Version = "0.1.0"
type healthResponse struct {
Status string `json:"status"`
}
type statusResponse struct {
Service string `json:"service"`
Version string `json:"version"`
}
func writeJSON(writer http.ResponseWriter, statusCode int, payload any) {
writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(statusCode)
_ = json.NewEncoder(writer).Encode(payload)
}
func HealthHandler(writer http.ResponseWriter, request *http.Request) {
writeJSON(writer, http.StatusOK, healthResponse{Status: "ok"})
}
func StatusHandler(writer http.ResponseWriter, request *http.Request) {
writeJSON(writer, http.StatusOK, statusResponse{
Service: "clustercanvas",
Version: Version,
})
}
+72
View File
@@ -0,0 +1,72 @@
package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestHealthHandler(t *testing.T) {
t.Parallel()
request := httptest.NewRequest(http.MethodGet, "/health", nil)
recorder := httptest.NewRecorder()
NewRouter().ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d", http.StatusOK, recorder.Code)
}
var payload healthResponse
if err := json.NewDecoder(recorder.Body).Decode(&payload); err != nil {
t.Fatalf("decode response: %v", err)
}
if payload.Status != "ok" {
t.Fatalf("expected status %q, got %q", "ok", payload.Status)
}
if contentType := recorder.Header().Get("Content-Type"); contentType != "application/json" {
t.Fatalf("expected Content-Type application/json, got %q", contentType)
}
}
func TestStatusHandler(t *testing.T) {
t.Parallel()
request := httptest.NewRequest(http.MethodGet, "/api/v1/status", nil)
recorder := httptest.NewRecorder()
NewRouter().ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("expected status %d, got %d", http.StatusOK, recorder.Code)
}
var payload statusResponse
if err := json.NewDecoder(recorder.Body).Decode(&payload); err != nil {
t.Fatalf("decode response: %v", err)
}
if payload.Service != "clustercanvas" {
t.Fatalf("expected service %q, got %q", "clustercanvas", payload.Service)
}
if payload.Version != Version {
t.Fatalf("expected version %q, got %q", Version, payload.Version)
}
}
func TestCORSPreflight(t *testing.T) {
t.Parallel()
request := httptest.NewRequest(http.MethodOptions, "/health", nil)
recorder := httptest.NewRecorder()
NewRouter().ServeHTTP(recorder, request)
if recorder.Code != http.StatusNoContent {
t.Fatalf("expected status %d, got %d", http.StatusNoContent, recorder.Code)
}
if origin := recorder.Header().Get("Access-Control-Allow-Origin"); origin != defaultAllowedOrigin {
t.Fatalf("expected Allow-Origin %q, got %q", defaultAllowedOrigin, origin)
}
}
+29
View File
@@ -0,0 +1,29 @@
package api
import (
"net/http"
)
const defaultAllowedOrigin = "http://localhost:5173"
func withCORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Access-Control-Allow-Origin", defaultAllowedOrigin)
writer.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
writer.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if request.Method == http.MethodOptions {
writer.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(writer, request)
})
}
func NewRouter() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /health", HealthHandler)
mux.HandleFunc("GET /api/v1/status", StatusHandler)
return withCORS(mux)
}