Add Certificates page and Traefik API frontend client
This commit is contained in:
parent
9e4c612dcb
commit
7fc90524b5
40 changed files with 6585 additions and 359 deletions
434
backend/internal/api/handlers/traefik_api_test.go
Normal file
434
backend/internal/api/handlers/traefik_api_test.go
Normal file
|
|
@ -0,0 +1,434 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/traefik/traefik-gui/backend/internal/api/middleware"
|
||||
"github.com/traefik/traefik-gui/backend/internal/database/repositories"
|
||||
"github.com/traefik/traefik-gui/backend/internal/traefik"
|
||||
)
|
||||
|
||||
// mockAPI implements traefik.TraefikAPI for handler tests
|
||||
type mockAPI struct {
|
||||
healthz func() (*traefik.HealthResponse, error)
|
||||
overview func() (*traefik.Overview, error)
|
||||
entrypoints func() ([]traefik.Entrypoint, error)
|
||||
routers func() ([]traefik.Router, error)
|
||||
services func() ([]traefik.Service, error)
|
||||
middlewares func() ([]traefik.Middleware, error)
|
||||
providers func() ([]traefik.Provider, error)
|
||||
certificates func() ([]traefik.Certificate, error)
|
||||
calls map[string]*int32
|
||||
}
|
||||
|
||||
func newMockAPI() *mockAPI {
|
||||
return &mockAPI{
|
||||
calls: make(map[string]*int32),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockAPI) inc(key string) {
|
||||
if _, ok := m.calls[key]; !ok {
|
||||
var v int32
|
||||
m.calls[key] = &v
|
||||
}
|
||||
atomic.AddInt32(m.calls[key], 1)
|
||||
}
|
||||
func (m *mockAPI) count(key string) int {
|
||||
if v, ok := m.calls[key]; ok {
|
||||
return int(atomic.LoadInt32(v))
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *mockAPI) GetHealthz(ctx context.Context) (*traefik.HealthResponse, error) {
|
||||
m.inc("healthz")
|
||||
if m.healthz != nil {
|
||||
return m.healthz()
|
||||
}
|
||||
return &traefik.HealthResponse{Status: "OK"}, nil
|
||||
}
|
||||
func (m *mockAPI) GetOverview(ctx context.Context) (*traefik.Overview, error) {
|
||||
m.inc("overview")
|
||||
if m.overview != nil {
|
||||
return m.overview()
|
||||
}
|
||||
return &traefik.Overview{TotalRouters: 1, TraefikVersion: "3.7.0"}, nil
|
||||
}
|
||||
func (m *mockAPI) GetEntrypoints(ctx context.Context) ([]traefik.Entrypoint, error) {
|
||||
m.inc("entrypoints")
|
||||
if m.entrypoints != nil {
|
||||
return m.entrypoints()
|
||||
}
|
||||
return []traefik.Entrypoint{{Name: "web", Address: ":80"}}, nil
|
||||
}
|
||||
func (m *mockAPI) GetRouters(ctx context.Context) ([]traefik.Router, error) {
|
||||
m.inc("routers")
|
||||
if m.routers != nil {
|
||||
return m.routers()
|
||||
}
|
||||
return []traefik.Router{{Name: "r1@docker", Provider: "docker"}}, nil
|
||||
}
|
||||
func (m *mockAPI) GetServices(ctx context.Context) ([]traefik.Service, error) {
|
||||
m.inc("services")
|
||||
if m.services != nil {
|
||||
return m.services()
|
||||
}
|
||||
return []traefik.Service{{Name: "s1@docker", Provider: "docker"}}, nil
|
||||
}
|
||||
func (m *mockAPI) GetMiddlewares(ctx context.Context) ([]traefik.Middleware, error) {
|
||||
m.inc("middlewares")
|
||||
if m.middlewares != nil {
|
||||
return m.middlewares()
|
||||
}
|
||||
return []traefik.Middleware{{Name: "m1@docker", Provider: "docker"}}, nil
|
||||
}
|
||||
func (m *mockAPI) GetProviders(ctx context.Context) ([]traefik.Provider, error) {
|
||||
m.inc("providers")
|
||||
if m.providers != nil {
|
||||
return m.providers()
|
||||
}
|
||||
return []traefik.Provider{{Name: "docker"}}, nil
|
||||
}
|
||||
func (m *mockAPI) GetCertificates(ctx context.Context) ([]traefik.Certificate, error) {
|
||||
m.inc("certificates")
|
||||
if m.certificates != nil {
|
||||
return m.certificates()
|
||||
}
|
||||
now := time.Now()
|
||||
return []traefik.Certificate{
|
||||
{Store: "default", Names: []string{"a.com"}, NotAfter: now.Add(48 * time.Hour), Issuer: "CA", SANs: []string{"a.com"}},
|
||||
{Store: "default", Names: []string{"b.com"}, NotAfter: now.Add(24 * time.Hour), Issuer: "CA", SANs: []string{"b.com"}},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// helper to setup router with auth and return admin cookie
|
||||
func setupTraefikHandlerTest(t *testing.T, api traefik.TraefikAPI) (*gin.Engine, *TraefikAPIHandler, *http.Cookie) {
|
||||
t.Helper()
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupTestDB(t)
|
||||
// Keep db open for duration of test; t.Cleanup will close when test ends? setupTestDB doesn't close, we need to defer close
|
||||
t.Cleanup(func() { db.Close() })
|
||||
admin := createUser(t, db, "admin-id", "admin", "admin", "adminpass12345")
|
||||
sess := createSession(t, db, admin.ID)
|
||||
// also create viewer for role tests
|
||||
viewer := createUser(t, db, "viewer-id", "viewer", "viewer", "viewerpass12345")
|
||||
_ = createSession(t, db, viewer.ID)
|
||||
|
||||
userRepo := repositories.NewUserRepository(db)
|
||||
sessionRepo := repositories.NewSessionRepository(db)
|
||||
authMw := middleware.NewAuthMiddleware(sessionRepo, userRepo)
|
||||
handler := NewTraefikAPIHandler(api)
|
||||
|
||||
r := gin.New()
|
||||
r.Use(middleware.CORSMiddleware("http://localhost:5173"))
|
||||
g := r.Group("/api/traefik")
|
||||
g.Use(authMw.RequireAuth())
|
||||
{
|
||||
g.GET("/health", handler.Health)
|
||||
g.GET("/overview", handler.Overview)
|
||||
g.GET("/entrypoints", handler.Entrypoints)
|
||||
g.GET("/routers", handler.Routers)
|
||||
g.GET("/services", handler.Services)
|
||||
g.GET("/middlewares", handler.Middlewares)
|
||||
g.GET("/providers", handler.Providers)
|
||||
g.GET("/certificates", handler.Certificates)
|
||||
}
|
||||
cookie := &http.Cookie{Name: "traefik_gui_session", Value: sess.ID, Path: "/"}
|
||||
return r, handler, cookie
|
||||
}
|
||||
|
||||
func doTraefikRequest(r *gin.Engine, method, path string, cookie *http.Cookie) *httptest.ResponseRecorder {
|
||||
req := httptest.NewRequest(method, path, nil)
|
||||
req.Header.Set("Origin", "http://localhost:5173")
|
||||
if cookie != nil {
|
||||
req.AddCookie(cookie)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
return w
|
||||
}
|
||||
|
||||
func TestTraefikHandler_Health_200(t *testing.T) {
|
||||
api := newMockAPI()
|
||||
r, _, cookie := setupTraefikHandlerTest(t, api)
|
||||
w := doTraefikRequest(r, "GET", "/api/traefik/health", cookie)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d body %s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp map[string]interface{}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if resp["healthy"] != true {
|
||||
t.Fatalf("expected healthy true, got %v", resp["healthy"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraefikHandler_Overview_200(t *testing.T) {
|
||||
api := newMockAPI()
|
||||
api.overview = func() (*traefik.Overview, error) {
|
||||
return &traefik.Overview{TotalRouters: 5, TotalServices: 3, TotalMiddlewares: 2, TraefikVersion: "3.7.0", TraefikCodename: "lascaux", Providers: []string{"docker"}}, nil
|
||||
}
|
||||
r, _, cookie := setupTraefikHandlerTest(t, api)
|
||||
w := doTraefikRequest(r, "GET", "/api/traefik/overview", cookie)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
var out traefik.Overview
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &out); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if out.TotalRouters != 5 || out.TraefikVersion != "3.7.0" {
|
||||
t.Fatalf("unexpected overview: %#v", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraefikHandler_EachRoute_200(t *testing.T) {
|
||||
api := newMockAPI()
|
||||
r, _, cookie := setupTraefikHandlerTest(t, api)
|
||||
paths := []string{"/api/traefik/entrypoints", "/api/traefik/routers", "/api/traefik/services", "/api/traefik/middlewares", "/api/traefik/providers", "/api/traefik/certificates"}
|
||||
for _, p := range paths {
|
||||
w := doTraefikRequest(r, "GET", p, cookie)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("path %s expected 200, got %d %s", p, w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraefikHandler_Upstream404(t *testing.T) {
|
||||
api := newMockAPI()
|
||||
api.routers = func() ([]traefik.Router, error) {
|
||||
return nil, &traefik.APIError{StatusCode: 404, Message: "not found", URL: "http://traefik/api/routers"}
|
||||
}
|
||||
r, _, cookie := setupTraefikHandlerTest(t, api)
|
||||
w := doTraefikRequest(r, "GET", "/api/traefik/routers", cookie)
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Fatalf("expected 404, got %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraefikHandler_Upstream500(t *testing.T) {
|
||||
api := newMockAPI()
|
||||
api.services = func() ([]traefik.Service, error) {
|
||||
return nil, &traefik.APIError{StatusCode: 500, Message: "internal", URL: "http://traefik/api/services"}
|
||||
}
|
||||
r, _, cookie := setupTraefikHandlerTest(t, api)
|
||||
w := doTraefikRequest(r, "GET", "/api/traefik/services", cookie)
|
||||
if w.Code != 500 {
|
||||
t.Fatalf("expected 500, got %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraefikHandler_UpstreamUnauthorizedMapsTo502(t *testing.T) {
|
||||
api := newMockAPI()
|
||||
api.middlewares = func() ([]traefik.Middleware, error) {
|
||||
return nil, &traefik.APIError{StatusCode: 401, Message: "unauthorized", URL: "http://traefik/api/middlewares"}
|
||||
}
|
||||
r, _, cookie := setupTraefikHandlerTest(t, api)
|
||||
w := doTraefikRequest(r, "GET", "/api/traefik/middlewares", cookie)
|
||||
if w.Code != http.StatusBadGateway {
|
||||
t.Fatalf("expected 502, got %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
if !contains(w.Body.String(), "private entrypoint") {
|
||||
t.Fatalf("expected private entrypoint message, got %s", w.Body.String())
|
||||
}
|
||||
// also test 403
|
||||
api.middlewares = func() ([]traefik.Middleware, error) {
|
||||
return nil, &traefik.APIError{StatusCode: 403, Message: "forbidden", URL: "http://traefik/api/middlewares"}
|
||||
}
|
||||
// need new handler to clear cache (previous 502 cached? Our cache caches only success, not errors, so fine)
|
||||
w = doTraefikRequest(r, "GET", "/api/traefik/middlewares", cookie)
|
||||
if w.Code != http.StatusBadGateway {
|
||||
t.Fatalf("403 should also map to 502, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraefikHandler_EachRoute_Upstream404_500_Unauthorized(t *testing.T) {
|
||||
// For each of the 8 routes, test 404, 500, 401->502
|
||||
routes := []struct {
|
||||
path string
|
||||
setup404 func(*mockAPI)
|
||||
setup500 func(*mockAPI)
|
||||
setup401 func(*mockAPI)
|
||||
}{
|
||||
{"/api/traefik/health", func(m *mockAPI) { m.healthz = func() (*traefik.HealthResponse, error) { return nil, &traefik.APIError{StatusCode: 404, Message: "not found", URL: ""} } }, func(m *mockAPI) { m.healthz = func() (*traefik.HealthResponse, error) { return nil, &traefik.APIError{StatusCode: 500, Message: "err", URL: ""} } }, func(m *mockAPI) { m.healthz = func() (*traefik.HealthResponse, error) { return nil, &traefik.APIError{StatusCode: 401, Message: "unauth", URL: ""} } }},
|
||||
{"/api/traefik/overview", func(m *mockAPI) { m.overview = func() (*traefik.Overview, error) { return nil, &traefik.APIError{StatusCode: 404, Message: "not found", URL: ""} } }, func(m *mockAPI) { m.overview = func() (*traefik.Overview, error) { return nil, &traefik.APIError{StatusCode: 500, Message: "err", URL: ""} } }, func(m *mockAPI) { m.overview = func() (*traefik.Overview, error) { return nil, &traefik.APIError{StatusCode: 401, Message: "unauth", URL: ""} } }},
|
||||
{"/api/traefik/entrypoints", func(m *mockAPI) { m.entrypoints = func() ([]traefik.Entrypoint, error) { return nil, &traefik.APIError{StatusCode: 404, Message: "not found", URL: ""} } }, func(m *mockAPI) { m.entrypoints = func() ([]traefik.Entrypoint, error) { return nil, &traefik.APIError{StatusCode: 500, Message: "err", URL: ""} } }, func(m *mockAPI) { m.entrypoints = func() ([]traefik.Entrypoint, error) { return nil, &traefik.APIError{StatusCode: 401, Message: "unauth", URL: ""} } }},
|
||||
{"/api/traefik/routers", func(m *mockAPI) { m.routers = func() ([]traefik.Router, error) { return nil, &traefik.APIError{StatusCode: 404, Message: "not found", URL: ""} } }, func(m *mockAPI) { m.routers = func() ([]traefik.Router, error) { return nil, &traefik.APIError{StatusCode: 500, Message: "err", URL: ""} } }, func(m *mockAPI) { m.routers = func() ([]traefik.Router, error) { return nil, &traefik.APIError{StatusCode: 401, Message: "unauth", URL: ""} } }},
|
||||
{"/api/traefik/services", func(m *mockAPI) { m.services = func() ([]traefik.Service, error) { return nil, &traefik.APIError{StatusCode: 404, Message: "not found", URL: ""} } }, func(m *mockAPI) { m.services = func() ([]traefik.Service, error) { return nil, &traefik.APIError{StatusCode: 500, Message: "err", URL: ""} } }, func(m *mockAPI) { m.services = func() ([]traefik.Service, error) { return nil, &traefik.APIError{StatusCode: 401, Message: "unauth", URL: ""} } }},
|
||||
{"/api/traefik/middlewares", func(m *mockAPI) { m.middlewares = func() ([]traefik.Middleware, error) { return nil, &traefik.APIError{StatusCode: 404, Message: "not found", URL: ""} } }, func(m *mockAPI) { m.middlewares = func() ([]traefik.Middleware, error) { return nil, &traefik.APIError{StatusCode: 500, Message: "err", URL: ""} } }, func(m *mockAPI) { m.middlewares = func() ([]traefik.Middleware, error) { return nil, &traefik.APIError{StatusCode: 401, Message: "unauth", URL: ""} } }},
|
||||
{"/api/traefik/providers", func(m *mockAPI) { m.providers = func() ([]traefik.Provider, error) { return nil, &traefik.APIError{StatusCode: 404, Message: "not found", URL: ""} } }, func(m *mockAPI) { m.providers = func() ([]traefik.Provider, error) { return nil, &traefik.APIError{StatusCode: 500, Message: "err", URL: ""} } }, func(m *mockAPI) { m.providers = func() ([]traefik.Provider, error) { return nil, &traefik.APIError{StatusCode: 401, Message: "unauth", URL: ""} } }},
|
||||
{"/api/traefik/certificates", func(m *mockAPI) { m.certificates = func() ([]traefik.Certificate, error) { return nil, &traefik.APIError{StatusCode: 404, Message: "not found", URL: ""} } }, func(m *mockAPI) { m.certificates = func() ([]traefik.Certificate, error) { return nil, &traefik.APIError{StatusCode: 500, Message: "err", URL: ""} } }, func(m *mockAPI) { m.certificates = func() ([]traefik.Certificate, error) { return nil, &traefik.APIError{StatusCode: 401, Message: "unauth", URL: ""} } }},
|
||||
}
|
||||
for _, tc := range routes {
|
||||
// 404
|
||||
api := newMockAPI()
|
||||
tc.setup404(api)
|
||||
r, _, cookie := setupTraefikHandlerTest(t, api)
|
||||
w := doTraefikRequest(r, "GET", tc.path, cookie)
|
||||
if w.Code != 404 {
|
||||
t.Fatalf("route %s 404 expected 404, got %d %s", tc.path, w.Code, w.Body.String())
|
||||
}
|
||||
// 500
|
||||
api = newMockAPI()
|
||||
tc.setup500(api)
|
||||
r, _, cookie = setupTraefikHandlerTest(t, api)
|
||||
w = doTraefikRequest(r, "GET", tc.path, cookie)
|
||||
if w.Code != 500 {
|
||||
t.Fatalf("route %s 500 expected 500, got %d %s", tc.path, w.Code, w.Body.String())
|
||||
}
|
||||
// 401 -> 502
|
||||
api = newMockAPI()
|
||||
tc.setup401(api)
|
||||
r, _, cookie = setupTraefikHandlerTest(t, api)
|
||||
w = doTraefikRequest(r, "GET", tc.path, cookie)
|
||||
if w.Code != 502 {
|
||||
t.Fatalf("route %s 401 expected 502, got %d %s", tc.path, w.Code, w.Body.String())
|
||||
}
|
||||
// unauthorized caller (no cookie) -> 401
|
||||
r, _, _ = setupTraefikHandlerTest(t, newMockAPI())
|
||||
w = doTraefikRequest(r, "GET", tc.path, nil)
|
||||
if w.Code != 401 {
|
||||
t.Fatalf("route %s unauth expected 401, got %d %s", tc.path, w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraefikHandler_UnauthorizedCaller(t *testing.T) {
|
||||
api := newMockAPI()
|
||||
r, _, _ := setupTraefikHandlerTest(t, api)
|
||||
// No cookie
|
||||
w := doTraefikRequest(r, "GET", "/api/traefik/overview", nil)
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("expected 401 for unauthenticated, got %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
// Also test viewer can access (viewer+)
|
||||
// setupTraefikHandlerTest already creates viewer, but we need cookie for viewer
|
||||
// Create a new test with viewer cookie
|
||||
// For simplicity, use admin cookie already tested; viewer should also succeed
|
||||
// We already tested admin 200, now test viewer
|
||||
// Need to get viewer cookie: we can create a new router with viewer session
|
||||
// Instead, just verify that viewer+ role is allowed by checking that viewer cookie succeeds
|
||||
// We'll create a viewer-specific test
|
||||
}
|
||||
|
||||
func TestTraefikHandler_ViewerCanRead(t *testing.T) {
|
||||
api := newMockAPI()
|
||||
// Need viewer cookie - create via helper that returns viewer cookie
|
||||
gin.SetMode(gin.TestMode)
|
||||
db := setupTestDB(t)
|
||||
t.Cleanup(func() { db.Close() })
|
||||
viewer := createUser(t, db, "viewer2", "viewer2", "viewer", "viewerpass12345")
|
||||
vsess := createSession(t, db, viewer.ID)
|
||||
userRepo := repositories.NewUserRepository(db)
|
||||
sessionRepo := repositories.NewSessionRepository(db)
|
||||
authMw := middleware.NewAuthMiddleware(sessionRepo, userRepo)
|
||||
handler := NewTraefikAPIHandler(api)
|
||||
r := gin.New()
|
||||
g := r.Group("/api/traefik")
|
||||
g.Use(authMw.RequireAuth())
|
||||
g.GET("/overview", handler.Overview)
|
||||
cookie := &http.Cookie{Name: "traefik_gui_session", Value: vsess.ID, Path: "/"}
|
||||
w := doTraefikRequest(r, "GET", "/api/traefik/overview", cookie)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("viewer should be allowed, got %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraefikHandler_Cache(t *testing.T) {
|
||||
api := newMockAPI()
|
||||
var count int32
|
||||
api.overview = func() (*traefik.Overview, error) {
|
||||
atomic.AddInt32(&count, 1)
|
||||
return &traefik.Overview{TotalRouters: 1}, nil
|
||||
}
|
||||
r, handler, cookie := setupTraefikHandlerTest(t, api)
|
||||
// Reduce TTL for test
|
||||
handler.ttl = 15 * time.Second
|
||||
w1 := doTraefikRequest(r, "GET", "/api/traefik/overview", cookie)
|
||||
if w1.Code != 200 {
|
||||
t.Fatalf("first 200, got %d", w1.Code)
|
||||
}
|
||||
if count != 1 {
|
||||
t.Fatalf("expected 1 upstream call, got %d", count)
|
||||
}
|
||||
w2 := doTraefikRequest(r, "GET", "/api/traefik/overview", cookie)
|
||||
if w2.Code != 200 {
|
||||
t.Fatalf("second 200, got %d", w2.Code)
|
||||
}
|
||||
if count != 1 {
|
||||
t.Fatalf("second call should be cached, count %d", count)
|
||||
}
|
||||
// refresh bypass
|
||||
w3 := doTraefikRequest(r, "GET", "/api/traefik/overview?refresh=1", cookie)
|
||||
if w3.Code != 200 {
|
||||
t.Fatalf("refresh 200, got %d", w3.Code)
|
||||
}
|
||||
if count != 2 {
|
||||
t.Fatalf("refresh should bypass cache, count %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTraefikHandler_CertificatesSortedAndExpiry(t *testing.T) {
|
||||
now := time.Now()
|
||||
api := newMockAPI()
|
||||
api.certificates = func() ([]traefik.Certificate, error) {
|
||||
return []traefik.Certificate{
|
||||
{Names: []string{"a.com"}, NotAfter: now.Add(48 * time.Hour), Issuer: "CA", SANs: []string{"a.com"}},
|
||||
{Names: []string{"b.com"}, NotAfter: now.Add(24 * time.Hour), Issuer: "CA", SANs: []string{"b.com"}},
|
||||
{Names: []string{"expired.com"}, NotAfter: now.Add(-24 * time.Hour), Issuer: "CA", SANs: []string{"expired.com"}},
|
||||
}, nil
|
||||
}
|
||||
r, _, cookie := setupTraefikHandlerTest(t, api)
|
||||
w := doTraefikRequest(r, "GET", "/api/traefik/certificates", cookie)
|
||||
if w.Code != 200 {
|
||||
t.Fatalf("expected 200, got %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
var out []struct {
|
||||
traefik.Certificate
|
||||
DaysUntilExpiry float64 `json:"days_until_expiry"`
|
||||
Expired bool `json:"expired"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &out); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if len(out) != 3 {
|
||||
t.Fatalf("expected 3, got %d", len(out))
|
||||
}
|
||||
// Sorted ascending by notAfter: expired.com first, then b.com (24h), then a.com (48h)
|
||||
if out[0].Names[0] != "expired.com" || out[1].Names[0] != "b.com" || out[2].Names[0] != "a.com" {
|
||||
t.Fatalf("not sorted ascending: %#v", out)
|
||||
}
|
||||
// Check days_until_expiry computed as float64 and expired flag
|
||||
if out[2].DaysUntilExpiry < 1.5 || out[2].DaysUntilExpiry > 2.5 {
|
||||
t.Fatalf("days_until_expiry for 48h should be ~2.0, got %f", out[2].DaysUntilExpiry)
|
||||
}
|
||||
if out[1].DaysUntilExpiry < 0.5 || out[1].DaysUntilExpiry > 1.5 {
|
||||
t.Fatalf("days_until_expiry for 24h should be ~1.0, got %f", out[1].DaysUntilExpiry)
|
||||
}
|
||||
if !out[0].Expired || out[1].Expired || out[2].Expired {
|
||||
t.Fatalf("expired flags wrong: got %v, %v, %v", out[0].Expired, out[1].Expired, out[2].Expired)
|
||||
}
|
||||
if out[0].DaysUntilExpiry >= 0 {
|
||||
t.Fatalf("expired should be negative, got %f", out[0].DaysUntilExpiry)
|
||||
}
|
||||
}
|
||||
|
||||
func contains(s, sub string) bool {
|
||||
return len(s) >= len(sub) && (func() bool {
|
||||
for i := 0; i <= len(s)-len(sub); i++ {
|
||||
if s[i:i+len(sub)] == sub {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue