mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-24 19:24:16 +00:00
server.test_mode survived the build-tag refactor only to feed five behavioral branches: the registry's fall-back to the default hold when the user's hold is unreachable, backfill warning suppression for external holds, the appview listener close on shutdown, the hold's relay-crawl skip, and the hold's appview-issuer tolerance. Every one of them is a "this is a local development build" decision, which is what the tag already says, and local development has to build with the tag or nothing resolves. So they read atproto.TestModeBuild now, and the flag, SetTestMode, IsTestMode, the middleware option, the backfill constructor parameter, the never-read field on RemoteHoldAuthorizer, the example and template YAML lines, and the docker-compose env vars are gone. The registry keeps the fallback as a field seeded from the constant so the production-path tests can pin it off under the tag. The 24 SetTestMode calls in tests were dead already: stripping them and running the affected packages tagged changed nothing. Tests that resolve a loopback did:web used to t.Fatal naming the tag, which left a bare `go test ./...` permanently red in five packages. They now live under `//go:build testmode`: whole-file constraints where every test needs it, and sibling *_testmode_test.go files holding the moved tests plus their fixtures where a file mixed. The harness carries the constraint too, with its package doc in an untagged doc.go so the package still exists without it. An untagged run compiles those tests out and passes; make test keeps the tag and runs everything. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwYzaG3Yy7uA8FbZ5qk3tQ
109 lines
3.5 KiB
Go
109 lines
3.5 KiB
Go
//go:build billing && testmode
|
|
|
|
package billing
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"atcr.io/pkg/testpds"
|
|
"github.com/bluesky-social/indigo/atproto/atcrypto"
|
|
"github.com/stripe/stripe-go/v84"
|
|
)
|
|
|
|
// managedHoldServer starts an httptest server standing in for a managed hold
|
|
// and returns it with the did:web the tier fan-out resolves back to it. The
|
|
// server serves its own DID document at /.well-known/did.json; handler gets
|
|
// every other request. Resolving a loopback did:web needs a `-tags testmode`
|
|
// build, which this file's build constraint guarantees.
|
|
func managedHoldServer(t *testing.T, handler http.HandlerFunc) (*httptest.Server, string) {
|
|
t.Helper()
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/.well-known/did.json", func(w http.ResponseWriter, r *http.Request) {
|
|
base := "http://" + r.Host
|
|
testpds.HoldDIDDocumentHandler(testpds.DIDWebForURL(base), base)(w, r)
|
|
})
|
|
mux.Handle("/", handler)
|
|
srv := httptest.NewServer(mux)
|
|
t.Cleanup(srv.Close)
|
|
return srv, testpds.DIDWebForURL(srv.URL)
|
|
}
|
|
|
|
// TestHandleSubscriptionChange_GrandfatheredSubscriberKeepsTier runs the same
|
|
// property through the real webhook path, so it covers the payload shape too:
|
|
// Stripe sends price.product as a bare ID string, and this fails if that is
|
|
// ever read wrongly.
|
|
//
|
|
// It asserts on the rank the managed hold is actually asked to apply. An
|
|
// earlier draft asserted that the event was recorded as processed, and passed
|
|
// against price-only resolution — because an unresolved tier ALSO records the
|
|
// event and returns nil. That is the defect itself, so any assertion it
|
|
// satisfies cannot be measuring the fix.
|
|
func TestHandleSubscriptionChange_GrandfatheredSubscriberKeepsTier(t *testing.T) {
|
|
const secret = "whsec_grandfather_test"
|
|
m, _ := newTestManager(t, secret)
|
|
m.cfg.Tiers = tierTestConfig().Tiers
|
|
|
|
stripeAPIReturning(t, http.StatusOK,
|
|
`{"id":"cus_gf","object":"customer","metadata":{"user_did":"did:plc:grandfathered"}}`)
|
|
|
|
type tierPush struct {
|
|
UserDID string `json:"userDid"`
|
|
TierRank int `json:"tierRank"`
|
|
}
|
|
pushes := make(chan tierPush, 4)
|
|
_, holdDID := managedHoldServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
var got tierPush
|
|
_ = json.NewDecoder(r.Body).Decode(&got)
|
|
pushes <- got
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
m.managedHolds = []string{holdDID}
|
|
|
|
priv, err := atcrypto.GeneratePrivateKeyP256()
|
|
if err != nil {
|
|
t.Fatalf("generate key: %v", err)
|
|
}
|
|
m.privateKey = priv
|
|
|
|
// price_supporter_v1_retired appears nowhere in the config. Only its
|
|
// product does — this is the subscriber a price change left behind.
|
|
payload := fmt.Sprintf(`{
|
|
"id": "evt_grandfathered",
|
|
"object": "event",
|
|
"api_version": %q,
|
|
"type": "customer.subscription.updated",
|
|
"created": %d,
|
|
"data": {"object": {
|
|
"id": "sub_gf",
|
|
"object": "subscription",
|
|
"status": "active",
|
|
"customer": "cus_gf",
|
|
"items": {"object":"list","data":[{"price":{
|
|
"id":"price_supporter_v1_retired",
|
|
"product":"prod_supporter"
|
|
}}]}
|
|
}}
|
|
}`, stripe.APIVersion, time.Now().Unix())
|
|
|
|
if err := postWebhook(t, m, secret, []byte(payload)); err != nil {
|
|
t.Fatalf("webhook: %v", err)
|
|
}
|
|
|
|
select {
|
|
case got := <-pushes:
|
|
if got.TierRank != 1 {
|
|
t.Errorf("hold was asked for tierRank %d, want 1 (supporter)", got.TierRank)
|
|
}
|
|
if got.UserDID != "did:plc:grandfathered" {
|
|
t.Errorf("hold was asked to update %q, want did:plc:grandfathered", got.UserDID)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("no tier push reached the hold — the grandfathered subscriber's tier was never applied")
|
|
}
|
|
}
|