dev-pod-api-build/internal/api/cluster_test.go
2026-04-16 04:16:36 +00:00

268 lines
8.1 KiB
Go

package api
import (
"context"
"errors"
"io"
"log/slog"
"net/http"
"testing"
"github.com/iliaivanov/spec-kit-remote/cmd/dev-pod-api/internal/model"
)
// mockClusterInfo implements ClusterInfoProvider for testing.
type mockClusterInfo struct {
status *model.ClusterStatus
stats []model.CacheStat
statusErr error
statsErr error
}
func (m *mockClusterInfo) GetClusterStatus(_ context.Context) (*model.ClusterStatus, error) {
if m.statusErr != nil {
return nil, m.statusErr
}
return m.status, nil
}
func (m *mockClusterInfo) GetCacheStats(_ context.Context) ([]model.CacheStat, error) {
if m.statsErr != nil {
return nil, m.statsErr
}
return m.stats, nil
}
func newClusterTestRouter(ci ClusterInfoProvider) (*mockKeyValidator, http.Handler) {
kv := newMockValidator()
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
srv := &Server{
Store: kv,
K8s: newMockPodManager(),
Cluster: ci,
Users: newMockUserStore(),
Usage: newMockUsageStore(),
Logger: logger,
GenerateKey: mockGenerateKey,
}
return kv, NewRouter(srv)
}
// --- GET /api/v1/cluster/status ---
func TestClusterStatus_AdminSuccess(t *testing.T) {
ci := &mockClusterInfo{
status: &model.ClusterStatus{
Nodes: []model.NodeStatus{
{
Name: "node-1",
Status: "Ready",
CPUCapacity: "8",
CPUAllocatable: "7800m",
MemCapacity: "32Gi",
MemAllocatable: "30Gi",
CPUUsage: "2500m",
MemUsage: "12Gi",
},
{
Name: "node-2",
Status: "Ready",
CPUCapacity: "8",
CPUAllocatable: "7800m",
MemCapacity: "32Gi",
MemAllocatable: "30Gi",
},
},
Total: model.ResourceSummary{
CPUCapacity: "16",
CPUAllocatable: "15600m",
MemCapacity: "64Gi",
MemAllocatable: "60Gi",
},
},
}
kv, router := newClusterTestRouter(ci)
admin := &model.User{ID: "admin-user", Quota: model.DefaultQuota()}
kv.addKey("dpk_admin", admin, model.RoleAdmin)
rr := doRequest(router, http.MethodGet, "/api/v1/cluster/status", "", "dpk_admin")
if rr.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
}
var status model.ClusterStatus
decodeJSON(t, rr, &status)
if len(status.Nodes) != 2 {
t.Fatalf("expected 2 nodes, got %d", len(status.Nodes))
}
if status.Nodes[0].Name != "node-1" {
t.Fatalf("expected node-1, got %s", status.Nodes[0].Name)
}
if status.Nodes[0].CPUUsage != "2500m" {
t.Fatalf("expected cpu usage 2500m, got %s", status.Nodes[0].CPUUsage)
}
if status.Total.CPUCapacity != "16" {
t.Fatalf("expected total CPU capacity 16, got %s", status.Total.CPUCapacity)
}
}
func TestClusterStatus_ForbiddenForUser(t *testing.T) {
ci := &mockClusterInfo{status: &model.ClusterStatus{}}
kv, router := newClusterTestRouter(ci)
user := &model.User{ID: "alice", Quota: model.DefaultQuota()}
kv.addKey("dpk_test", user, model.RoleUser)
rr := doRequest(router, http.MethodGet, "/api/v1/cluster/status", "", "dpk_test")
if rr.Code != http.StatusForbidden {
t.Fatalf("expected 403, got %d: %s", rr.Code, rr.Body.String())
}
}
func TestClusterStatus_NoAuth(t *testing.T) {
ci := &mockClusterInfo{status: &model.ClusterStatus{}}
_, router := newClusterTestRouter(ci)
rr := doRequest(router, http.MethodGet, "/api/v1/cluster/status", "", "")
if rr.Code != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", rr.Code)
}
}
func TestClusterStatus_Error(t *testing.T) {
ci := &mockClusterInfo{statusErr: errors.New("k8s unreachable")}
kv, router := newClusterTestRouter(ci)
admin := &model.User{ID: "admin-user", Quota: model.DefaultQuota()}
kv.addKey("dpk_admin", admin, model.RoleAdmin)
rr := doRequest(router, http.MethodGet, "/api/v1/cluster/status", "", "dpk_admin")
if rr.Code != http.StatusInternalServerError {
t.Fatalf("expected 500, got %d: %s", rr.Code, rr.Body.String())
}
}
func TestClusterStatus_NilCluster(t *testing.T) {
kv, router := newClusterTestRouter(nil)
admin := &model.User{ID: "admin-user", Quota: model.DefaultQuota()}
kv.addKey("dpk_admin", admin, model.RoleAdmin)
rr := doRequest(router, http.MethodGet, "/api/v1/cluster/status", "", "dpk_admin")
if rr.Code != http.StatusServiceUnavailable {
t.Fatalf("expected 503, got %d: %s", rr.Code, rr.Body.String())
}
}
// --- GET /api/v1/cache/stats ---
func TestCacheStats_AdminSuccess(t *testing.T) {
ci := &mockClusterInfo{
stats: []model.CacheStat{
{Name: "verdaccio", PVCName: "verdaccio-storage", Capacity: "10Gi", Status: "Bound"},
{Name: "athens", PVCName: "athens-storage", Capacity: "10Gi", Status: "Bound"},
{Name: "cargo-proxy", PVCName: "cargo-proxy-cache", Capacity: "10Gi", Status: "Bound"},
},
}
kv, router := newClusterTestRouter(ci)
admin := &model.User{ID: "admin-user", Quota: model.DefaultQuota()}
kv.addKey("dpk_admin", admin, model.RoleAdmin)
rr := doRequest(router, http.MethodGet, "/api/v1/cache/stats", "", "dpk_admin")
if rr.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
}
var stats []model.CacheStat
decodeJSON(t, rr, &stats)
if len(stats) != 3 {
t.Fatalf("expected 3 cache stats, got %d", len(stats))
}
names := map[string]bool{}
for _, s := range stats {
names[s.Name] = true
if s.Status != "Bound" {
t.Fatalf("expected Bound status for %s, got %s", s.Name, s.Status)
}
if s.Capacity != "10Gi" {
t.Fatalf("expected 10Gi capacity for %s, got %s", s.Name, s.Capacity)
}
}
for _, expected := range []string{"verdaccio", "athens", "cargo-proxy"} {
if !names[expected] {
t.Fatalf("missing cache stat for %s", expected)
}
}
}
func TestCacheStats_ForbiddenForUser(t *testing.T) {
ci := &mockClusterInfo{stats: []model.CacheStat{}}
kv, router := newClusterTestRouter(ci)
user := &model.User{ID: "alice", Quota: model.DefaultQuota()}
kv.addKey("dpk_test", user, model.RoleUser)
rr := doRequest(router, http.MethodGet, "/api/v1/cache/stats", "", "dpk_test")
if rr.Code != http.StatusForbidden {
t.Fatalf("expected 403, got %d: %s", rr.Code, rr.Body.String())
}
}
func TestCacheStats_NoAuth(t *testing.T) {
ci := &mockClusterInfo{stats: []model.CacheStat{}}
_, router := newClusterTestRouter(ci)
rr := doRequest(router, http.MethodGet, "/api/v1/cache/stats", "", "")
if rr.Code != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", rr.Code)
}
}
func TestCacheStats_Error(t *testing.T) {
ci := &mockClusterInfo{statsErr: errors.New("pvc query failed")}
kv, router := newClusterTestRouter(ci)
admin := &model.User{ID: "admin-user", Quota: model.DefaultQuota()}
kv.addKey("dpk_admin", admin, model.RoleAdmin)
rr := doRequest(router, http.MethodGet, "/api/v1/cache/stats", "", "dpk_admin")
if rr.Code != http.StatusInternalServerError {
t.Fatalf("expected 500, got %d: %s", rr.Code, rr.Body.String())
}
}
func TestCacheStats_NilCluster(t *testing.T) {
kv, router := newClusterTestRouter(nil)
admin := &model.User{ID: "admin-user", Quota: model.DefaultQuota()}
kv.addKey("dpk_admin", admin, model.RoleAdmin)
rr := doRequest(router, http.MethodGet, "/api/v1/cache/stats", "", "dpk_admin")
if rr.Code != http.StatusServiceUnavailable {
t.Fatalf("expected 503, got %d: %s", rr.Code, rr.Body.String())
}
}
func TestCacheStats_PartialNotFound(t *testing.T) {
ci := &mockClusterInfo{
stats: []model.CacheStat{
{Name: "verdaccio", PVCName: "verdaccio-storage", Capacity: "10Gi", Status: "Bound"},
{Name: "athens", PVCName: "athens-storage", Status: "NotFound"},
{Name: "cargo-proxy", PVCName: "cargo-proxy-cache", Capacity: "10Gi", Status: "Bound"},
},
}
kv, router := newClusterTestRouter(ci)
admin := &model.User{ID: "admin-user", Quota: model.DefaultQuota()}
kv.addKey("dpk_admin", admin, model.RoleAdmin)
rr := doRequest(router, http.MethodGet, "/api/v1/cache/stats", "", "dpk_admin")
if rr.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
}
var stats []model.CacheStat
decodeJSON(t, rr, &stats)
if len(stats) != 3 {
t.Fatalf("expected 3 entries, got %d", len(stats))
}
for _, s := range stats {
if s.Name == "athens" && s.Status != "NotFound" {
t.Fatalf("expected athens NotFound, got %s", s.Status)
}
}
}