build source
This commit is contained in:
commit
ee1fec43ed
4171 changed files with 1351288 additions and 0 deletions
268
internal/api/cluster_test.go
Normal file
268
internal/api/cluster_test.go
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue