Files
commerce/api/billing/osspayout_http_test.go
zeekay 1943f1c093 Hanzo Commerce
Multi-tenant commerce and billing: orders, subscriptions, invoices, plans,
usage metering and a credit ledger. One Go binary. MIT OR Apache-2.0.

Claude-Session: https://claude.ai/code/session_01SAtaDFm8VDiWrhYHNNtxMw
2026-09-23 21:52:16 -07:00

142 lines
5.6 KiB
Go

package billing
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/zap-proto/zip"
"github.com/hanzoai/commerce/billing/engine"
"github.com/hanzoai/commerce/datastore"
"github.com/hanzoai/commerce/util/test/ae"
)
// withSystemCtx wires a request so systemDB(c) resolves to the ae-backed SQLite
// datastore in the "system" namespace.
func withSystemCtx(c *zip.Ctx) {
ctx := context.Background()
c.SetContext(ctx)
}
// TestOSSPayout_HTTP_EndToEnd drives the real HTTP handlers through a gin
// router against a live SQLite datastore:
//
// POST /sbom (arcd ingest) ->
// engine.AccrueOSSPayout (the usage hook, called synchronously here) ->
// GET /oss-payout/summary shows per-package owed + resolved maintainer.
//
// This is the sandbox proof of the HTTP surface (routing + JSON + storage +
// rollup), complementing the engine-level integration test.
func TestOSSPayout_HTTP_EndToEnd(t *testing.T) {
tc := ae.NewContext()
defer tc.Close()
// ── 1. Ingest an SBOM via the HTTP handler ───────────────────────────
ingestBody, _ := json.Marshal(sbomIngestRequest{
ImageRef: "ghcr.io/hanzoai/commerce:1.785.15",
ImageDigest: "sha256:httpproof",
Service: "commerce",
Tool: "syft",
Components: []sbomComponent{
{PURL: "pkg:golang/github.com/gin-gonic/[email protected]", Name: "gin", Ecosystem: "golang", Version: "v1.12.0", Scope: "direct"},
{PURL: "pkg:golang/github.com/google/[email protected]", Name: "uuid", Ecosystem: "golang", Version: "v1.6.0", Scope: "direct"},
{PURL: "pkg:npm/[email protected]", Name: "react", Ecosystem: "npm", Version: "18.3.1", Scope: "transitive"},
},
})
ingestReq := httptest.NewRequest(http.MethodPost, "/v1/billing/sbom", bytes.NewReader(ingestBody))
ingestReq.Header.Set("Content-Type", "application/json")
w := driveSeeded(withSystemCtx, "/v1/billing/sbom", ingestReq, IngestSBOM)
if w.StatusCode != 201 {
t.Fatalf("ingest status = %d, body=%s", w.StatusCode, func() string { b, _ := io.ReadAll(w.Body); return string(b) }())
}
var ingestResp map[string]any
_ = json.Unmarshal(func() []byte { b, _ := io.ReadAll(w.Body); return b }(), &ingestResp)
if ingestResp["componentCount"].(float64) != 3 {
t.Fatalf("ingest componentCount = %v, want 3", ingestResp["componentCount"])
}
// ── 2. Accrue against a $100 org charge (the usage hook, sync) ────────
orgDB := datastore.New(context.Background())
orgDB.SetNamespace("acme")
engine.AccrueOSSPayout(orgDB, "acme", "acme/alice", 10_000, "usd", "txn_http_proof", false)
// ── 3. Read the payout summary via the HTTP handler ──────────────────
w2 := driveSeeded(withSystemCtx, "/v1/billing/oss-payout/summary",
httptest.NewRequest(http.MethodGet, "/v1/billing/oss-payout/summary", nil), GetOSSPayoutSummary)
if w2.StatusCode != 200 {
t.Fatalf("summary status = %d, body=%s", w2.StatusCode, func() string { b, _ := io.ReadAll(w2.Body); return string(b) }())
}
var summary struct {
TotalAccruedCents int64 `json:"totalAccruedCents"`
HeldCents int64 `json:"heldCents"`
PackageCount int `json:"packageCount"`
Maintainers []struct {
PURL string `json:"purl"`
AccruedCents int64 `json:"accruedCents"`
FundingTarget string `json:"fundingTarget"`
FundingKind string `json:"fundingKind"`
Status string `json:"status"`
} `json:"maintainers"`
}
if err := json.Unmarshal(func() []byte { b, _ := io.ReadAll(w2.Body); return b }(), &summary); err != nil {
t.Fatalf("decode summary: %v; body=%s", err, func() string { b, _ := io.ReadAll(w2.Body); return string(b) }())
}
// 25% of $100 = $25.00 = 2500 cents. Weights: 2 direct (1.0) + 1 transitive
// (0.25) = 2.25 total. direct = 2500*(1/2.25)=1111 (largest-remainder rounds
// to keep the pool exact), transitive react = 2500*(0.25/2.25)=277.
if summary.TotalAccruedCents != 2500 {
t.Fatalf("total accrued = %d, want 2500 (25%% cap)", summary.TotalAccruedCents)
}
if summary.PackageCount != 3 {
t.Fatalf("package count = %d, want 3", summary.PackageCount)
}
byPURL := map[string]int64{}
target := map[string]string{}
status := map[string]string{}
for _, m := range summary.Maintainers {
byPURL[m.PURL] = m.AccruedCents
target[m.PURL] = m.FundingTarget
status[m.PURL] = m.Status
}
// gin resolves to github_sponsors:gin-gonic, status pending.
if target["pkg:golang/github.com/gin-gonic/[email protected]"] != "github_sponsors:gin-gonic" {
t.Errorf("gin funding target = %q", target["pkg:golang/github.com/gin-gonic/[email protected]"])
}
if status["pkg:golang/github.com/gin-gonic/[email protected]"] != "pending" {
t.Errorf("gin status = %q, want pending", status["pkg:golang/github.com/gin-gonic/[email protected]"])
}
// react (npm, no VCS coordinate) is held.
if status["pkg:npm/[email protected]"] != "held" {
t.Errorf("react status = %q, want held", status["pkg:npm/[email protected]"])
}
if summary.HeldCents != byPURL["pkg:npm/[email protected]"] {
t.Errorf("heldCents = %d, want react's %d", summary.HeldCents, byPURL["pkg:npm/[email protected]"])
}
// Conservation across the HTTP rollup: directs + held == total.
var sum int64
for _, v := range byPURL {
sum += v
}
if sum != 2500 {
t.Fatalf("summary rollup sum = %d, want 2500", sum)
}
t.Logf("PROOF: $100 spend -> %d cents (25%%) across %d packages; gin=%dc->%s react=%dc->held",
summary.TotalAccruedCents, summary.PackageCount,
byPURL["pkg:golang/github.com/gin-gonic/[email protected]"], target["pkg:golang/github.com/gin-gonic/[email protected]"],
byPURL["pkg:npm/[email protected]"])
}