268 lines
8.1 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|