Files

132 lines
6.6 KiB
Go
Raw Permalink Normal View History

2026-08-26 06:41:07 +08:00
package httpapi
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
"time"
"data-terminal-cloud/internal/audit"
"data-terminal-cloud/internal/billing"
"data-terminal-cloud/internal/concurrency"
"data-terminal-cloud/internal/identity"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
func TestBillingHandlersPriceQuotaAndSummary(t *testing.T) {
databaseURL := os.Getenv("TEST_DATABASE_URL")
if databaseURL == "" {
t.Skip("TEST_DATABASE_URL is not set")
}
ctx := context.Background()
pool, err := pgxpool.New(ctx, databaseURL)
if err != nil {
t.Fatal(err)
}
defer pool.Close()
service, err := billing.NewService(pool, audit.NewRepository())
if err != nil {
t.Fatal(err)
}
admin := identity.Principal{Issuer: "http://localhost:58180/realms/voice-local", Subject: "00000000-0000-0000-0000-000000000104"}
approver := identity.Principal{Issuer: admin.Issuer, Subject: "00000000-0000-0000-0000-000000000105"}
2026-08-26 06:41:07 +08:00
tenantID := "10000000-0000-0000-0000-000000000010"
priceKey, quotaKey := "http-billing-price-"+uuid.NewString(), "http-billing-quota-"+uuid.NewString()
sourceRef := "integration://http-billing/" + uuid.NewString()
changePrefix := "HTTP费用集成-" + uuid.NewString()
defer func() {
_, _ = pool.Exec(ctx, `DELETE FROM audit_event WHERE request_id LIKE 'http-billing-%'`)
_, _ = pool.Exec(ctx, `DELETE FROM idempotency_record WHERE idempotency_key=ANY($1::text[])`, []string{priceKey, quotaKey})
_, _ = pool.Exec(ctx, `DELETE FROM usage_quota_policy WHERE change_summary LIKE $1`, changePrefix+"%")
_, _ = pool.Exec(ctx, `DELETE FROM billing_price_book WHERE source_ref=$1`, sourceRef)
}()
setHeaders := func(request *http.Request, key string) {
request.Header.Set("X-Tenant-Id", tenantID)
if key != "" {
request.Header.Set("Idempotency-Key", key)
}
}
withID := func(request *http.Request, id string) *http.Request {
return request.WithContext(context.WithValue(request.Context(), requestIDKey, id))
}
priceBody, _ := json.Marshal(map[string]any{"provider": "mock-http", "currency": "CNY", "source_ref": sourceRef, "change_summary": changePrefix + "价格", "effective_from": time.Now().UTC().Add(-time.Hour), "rates": []map[string]any{{"usage_category": "llm", "metric_code": "input_tokens", "unit": "token", "unit_price_micros": 2}}})
request := withID(authenticatedRequest(http.MethodPost, "/api/v1/ops/price-books", string(priceBody), admin), "http-billing-price-create")
setHeaders(request, priceKey)
recorder := httptest.NewRecorder()
CreateOpsPriceBookHandler(service).ServeHTTP(recorder, request)
if recorder.Code != http.StatusCreated || recorder.Header().Get("ETag") != concurrency.FormatETag(1) {
t.Fatalf("create price status=%d body=%s", recorder.Code, recorder.Body.String())
}
var priceResponse struct {
Data billing.PriceBook `json:"data"`
}
if err := json.Unmarshal(recorder.Body.Bytes(), &priceResponse); err != nil {
t.Fatal(err)
}
price := priceResponse.Data
request = authenticatedRequest(http.MethodGet, "/api/v1/ops/price-books", "", admin)
setHeaders(request, "")
recorder = httptest.NewRecorder()
ListOpsPriceBooksHandler(service).ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), price.ID) {
t.Fatalf("list prices status=%d body=%s", recorder.Code, recorder.Body.String())
}
request = withID(authenticatedRequest(http.MethodPost, "/api/v1/ops/price-books/"+price.ID+"/publish", `{"reason":"本人不得发布"}`, admin), "http-billing-self-publish")
request.SetPathValue("id", price.ID)
setHeaders(request, "")
request.Header.Set("If-Match", concurrency.FormatETag(1))
recorder = httptest.NewRecorder()
PublishOpsPriceBookHandler(service).ServeHTTP(recorder, request)
if recorder.Code != http.StatusConflict || !strings.Contains(recorder.Body.String(), "SELF_APPROVAL_FORBIDDEN") {
t.Fatalf("self publish status=%d body=%s", recorder.Code, recorder.Body.String())
}
request = withID(authenticatedRequest(http.MethodPost, "/api/v1/ops/price-books/"+price.ID+"/publish", `{"reason":"另一审批人复核发布"}`, approver), "http-billing-price-publish")
request.SetPathValue("id", price.ID)
setHeaders(request, "")
request.Header.Set("If-Match", concurrency.FormatETag(1))
recorder = httptest.NewRecorder()
PublishOpsPriceBookHandler(service).ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK || recorder.Header().Get("ETag") != concurrency.FormatETag(2) {
t.Fatalf("publish price status=%d body=%s", recorder.Code, recorder.Body.String())
}
quotaBody, _ := json.Marshal(map[string]any{"scope_type": "TENANT", "billing_timezone": "Asia/Shanghai", "currency": "CNY", "daily_limit_micros": 10000000, "monthly_limit_micros": 100000000, "max_concurrent_calls": 10, "max_call_duration_sec": 3600, "in_progress_action": "COMPLETE_CURRENT_TURN", "change_summary": changePrefix + "额度"})
request = withID(authenticatedRequest(http.MethodPost, "/api/v1/ops/quota-policies", string(quotaBody), approver), "http-billing-quota-create")
setHeaders(request, quotaKey)
recorder = httptest.NewRecorder()
CreateOpsQuotaPolicyHandler(service).ServeHTTP(recorder, request)
if recorder.Code != http.StatusCreated || recorder.Header().Get("ETag") != concurrency.FormatETag(1) {
t.Fatalf("create quota status=%d body=%s", recorder.Code, recorder.Body.String())
}
request = authenticatedRequest(http.MethodGet, "/api/v1/ops/quota-policies", "", admin)
setHeaders(request, "")
recorder = httptest.NewRecorder()
ListOpsQuotaPoliciesHandler(service).ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), changePrefix) {
t.Fatalf("list quotas status=%d body=%s", recorder.Code, recorder.Body.String())
}
request = authenticatedRequest(http.MethodGet, "/api/v1/ops/cost-summary?from=bad&to=bad", "", admin)
setHeaders(request, "")
recorder = httptest.NewRecorder()
GetOpsCostSummaryHandler(service).ServeHTTP(recorder, request)
if recorder.Code != http.StatusBadRequest {
t.Fatalf("invalid summary range status=%d body=%s", recorder.Code, recorder.Body.String())
}
from, to := time.Now().UTC().Add(-time.Hour).Format(time.RFC3339), time.Now().UTC().Format(time.RFC3339)
request = authenticatedRequest(http.MethodGet, "/api/v1/ops/cost-summary?from="+url.QueryEscape(from)+"&to="+url.QueryEscape(to), "", admin)
setHeaders(request, "")
recorder = httptest.NewRecorder()
GetOpsCostSummaryHandler(service).ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK || recorder.Header().Get("Cache-Control") != "no-store" {
t.Fatalf("cost summary status=%d body=%s", recorder.Code, recorder.Body.String())
}
}