build source
This commit is contained in:
commit
ee1fec43ed
4171 changed files with 1351288 additions and 0 deletions
225
internal/api/billing_test.go
Normal file
225
internal/api/billing_test.go
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/iliaivanov/spec-kit-remote/cmd/dev-pod-api/internal/model"
|
||||
)
|
||||
|
||||
// mockUsageStore implements UsageStore for testing.
|
||||
type mockUsageStore struct {
|
||||
mu sync.Mutex
|
||||
starts []usageEvent
|
||||
stops []usageEvent
|
||||
usageResult *model.UsageSummary
|
||||
dailyResult []model.DailyUsage
|
||||
}
|
||||
|
||||
type usageEvent struct {
|
||||
userID string
|
||||
podName string
|
||||
}
|
||||
|
||||
func newMockUsageStore() *mockUsageStore {
|
||||
return &mockUsageStore{
|
||||
usageResult: &model.UsageSummary{},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockUsageStore) RecordPodStart(_ context.Context, userID, podName string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.starts = append(m.starts, usageEvent{userID, podName})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockUsageStore) RecordPodStop(_ context.Context, userID, podName string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.stops = append(m.stops, usageEvent{userID, podName})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockUsageStore) RecordResourceSample(_ context.Context, _, _ string, _, _ float64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockUsageStore) RecordAIRequest(_ context.Context, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockUsageStore) GetUsage(_ context.Context, _ string, _ int, _ time.Month, _ int) (*model.UsageSummary, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.usageResult, nil
|
||||
}
|
||||
|
||||
func (m *mockUsageStore) GetDailyUsage(_ context.Context, _ string, _ int, _ time.Month) ([]model.DailyUsage, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.dailyResult, nil
|
||||
}
|
||||
|
||||
// --- GET /api/v1/users/{user}/usage ---
|
||||
|
||||
func TestGetUserUsage_Success(t *testing.T) {
|
||||
kv, _, us, router := newPodTestRouter()
|
||||
user := &model.User{ID: "alice", Quota: model.DefaultQuota()}
|
||||
kv.addKey("dpk_test", user, model.RoleUser)
|
||||
us.addUser(user)
|
||||
|
||||
rr := doRequest(router, http.MethodGet, "/api/v1/users/alice/usage", "", "dpk_test")
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
|
||||
var resp map[string]model.UsageSummary
|
||||
decodeJSON(t, rr, &resp)
|
||||
if _, ok := resp["current_month"]; !ok {
|
||||
t.Fatal("response should contain 'current_month' key")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserUsage_AdminCanViewOther(t *testing.T) {
|
||||
kv, _, us, router := newPodTestRouter()
|
||||
admin := &model.User{ID: "admin-user", Quota: model.DefaultQuota()}
|
||||
alice := &model.User{ID: "alice", Quota: model.DefaultQuota()}
|
||||
kv.addKey("dpk_admin", admin, model.RoleAdmin)
|
||||
us.addUser(admin)
|
||||
us.addUser(alice)
|
||||
|
||||
rr := doRequest(router, http.MethodGet, "/api/v1/users/alice/usage", "", "dpk_admin")
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserUsage_Forbidden(t *testing.T) {
|
||||
kv, _, _, router := newPodTestRouter()
|
||||
user := &model.User{ID: "alice", Quota: model.DefaultQuota()}
|
||||
kv.addKey("dpk_test", user, model.RoleUser)
|
||||
|
||||
rr := doRequest(router, http.MethodGet, "/api/v1/users/bob/usage", "", "dpk_test")
|
||||
if rr.Code != http.StatusForbidden {
|
||||
t.Fatalf("expected 403, got %d: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserUsage_NotFound(t *testing.T) {
|
||||
kv, _, _, router := newPodTestRouter()
|
||||
user := &model.User{ID: "alice", Quota: model.DefaultQuota()}
|
||||
kv.addKey("dpk_test", user, model.RoleUser)
|
||||
// Don't add alice to user store
|
||||
|
||||
rr := doRequest(router, http.MethodGet, "/api/v1/users/alice/usage", "", "dpk_test")
|
||||
if rr.Code != http.StatusNotFound {
|
||||
t.Fatalf("expected 404, got %d: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// --- GET /api/v1/billing/summary ---
|
||||
|
||||
func TestBillingSummary_AdminSuccess(t *testing.T) {
|
||||
kv, _, us, router := newPodTestRouter()
|
||||
admin := &model.User{ID: "admin-user", Quota: model.DefaultQuota()}
|
||||
alice := &model.User{ID: "alice", Quota: model.DefaultQuota()}
|
||||
kv.addKey("dpk_admin", admin, model.RoleAdmin)
|
||||
us.addUser(admin)
|
||||
us.addUser(alice)
|
||||
|
||||
rr := doRequest(router, http.MethodGet, "/api/v1/billing/summary", "", "dpk_admin")
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
|
||||
var summaries []model.UserUsageSummary
|
||||
decodeJSON(t, rr, &summaries)
|
||||
if len(summaries) != 2 {
|
||||
t.Fatalf("expected 2 user summaries, got %d", len(summaries))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBillingSummary_Forbidden(t *testing.T) {
|
||||
kv, _, _, router := newPodTestRouter()
|
||||
user := &model.User{ID: "alice", Quota: model.DefaultQuota()}
|
||||
kv.addKey("dpk_test", user, model.RoleUser)
|
||||
|
||||
rr := doRequest(router, http.MethodGet, "/api/v1/billing/summary", "", "dpk_test")
|
||||
if rr.Code != http.StatusForbidden {
|
||||
t.Fatalf("expected 403, got %d: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// --- GET /api/v1/billing/{user}/history ---
|
||||
|
||||
func TestBillingHistory_Success(t *testing.T) {
|
||||
kv, _, us, router := newPodTestRouter()
|
||||
user := &model.User{ID: "alice", Quota: model.DefaultQuota()}
|
||||
kv.addKey("dpk_test", user, model.RoleUser)
|
||||
us.addUser(user)
|
||||
|
||||
rr := doRequest(router, http.MethodGet, "/api/v1/billing/alice/history", "", "dpk_test")
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
|
||||
var daily []model.DailyUsage
|
||||
decodeJSON(t, rr, &daily)
|
||||
// Empty history returns empty array
|
||||
if daily == nil {
|
||||
t.Fatal("expected non-nil array")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBillingHistory_AdminCanView(t *testing.T) {
|
||||
kv, _, us, router := newPodTestRouter()
|
||||
admin := &model.User{ID: "admin-user", Quota: model.DefaultQuota()}
|
||||
alice := &model.User{ID: "alice", Quota: model.DefaultQuota()}
|
||||
kv.addKey("dpk_admin", admin, model.RoleAdmin)
|
||||
us.addUser(admin)
|
||||
us.addUser(alice)
|
||||
|
||||
rr := doRequest(router, http.MethodGet, "/api/v1/billing/alice/history", "", "dpk_admin")
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBillingHistory_Forbidden(t *testing.T) {
|
||||
kv, _, _, router := newPodTestRouter()
|
||||
user := &model.User{ID: "alice", Quota: model.DefaultQuota()}
|
||||
kv.addKey("dpk_test", user, model.RoleUser)
|
||||
|
||||
rr := doRequest(router, http.MethodGet, "/api/v1/billing/bob/history", "", "dpk_test")
|
||||
if rr.Code != http.StatusForbidden {
|
||||
t.Fatalf("expected 403, got %d: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// --- Auth integration ---
|
||||
|
||||
func TestBillingRoutes_NoAuth(t *testing.T) {
|
||||
_, _, _, router := newPodTestRouter()
|
||||
|
||||
routes := []struct {
|
||||
method string
|
||||
path string
|
||||
}{
|
||||
{http.MethodGet, "/api/v1/users/alice/usage"},
|
||||
{http.MethodGet, "/api/v1/billing/summary"},
|
||||
{http.MethodGet, "/api/v1/billing/alice/history"},
|
||||
}
|
||||
|
||||
for _, rt := range routes {
|
||||
t.Run(rt.method+" "+rt.path, func(t *testing.T) {
|
||||
rr := doRequest(router, rt.method, rt.path, "", "")
|
||||
if rr.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("expected 401, got %d", rr.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue