eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3// πŸ€– Managed-payments provider core (docs/managed-payments.md phase 1): the
4// destination-charge split, the space binding on platform webhooks, and the
5// env selection order. The entitlement state machine is provider-independent
6// and already covered; these tests pin what stripePlatform sends and drops.
7
8import (
9 "context"
10 "encoding/json"
11 "net/http"
12 "net/http/httptest"
13 "net/url"
14 "strings"
15 "testing"
16 "time"
17)
18
19// fakePlatformStripe captures the last form posted to any path and answers
20// with a checkout URL. It also records whether a Stripe-Account header ever
21// arrived β€” a platform provider must never send one.
22func fakePlatformStripe(t *testing.T) (*httptest.Server, *url.Values, *[]string) {
23 t.Helper()
24 captured := &url.Values{}
25 var headers []string
26 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
27 if h := r.Header.Get("Stripe-Account"); h != "" {
28 headers = append(headers, h)
29 }
30 if err := r.ParseForm(); err != nil {
31 w.WriteHeader(http.StatusBadRequest)
32 return
33 }
34 *captured = r.PostForm
35 writeJSON(w, http.StatusOK, map[string]any{
36 "id": "cs_test_1", "url": "https://checkout.stripe.com/c/pay/cs_test_1",
37 })
38 }))
39 t.Cleanup(srv.Close)
40 return srv, captured, &headers
41}
42
43func newTestPlatform(t *testing.T, srv *Server, apiBase string) *stripePlatform {
44 t.Helper()
45 return &stripePlatform{
46 db: srv.db, key: "sk_test_platform", webhookSecret: "whsec_platform",
47 apiBase: apiBase, client: http.DefaultClient,
48 }
49}
50
51func TestManagedCheckoutIsDestinationCharge(t *testing.T) {
52 _, srv := newTestServer(t)
53 stripe, captured, headers := fakePlatformStripe(t)
54 p := newTestPlatform(t, srv, stripe.URL)
55
56 // Dark until the Express account exists: not configured, checkout refused.
57 if p.Configured() {
58 t.Fatal("Configured() = true with no Express account β€” the dark gate is the point of phase 1")
59 }
60 if _, err := p.Checkout(context.Background(), CheckoutReq{}); err != errBillingOff {
61 t.Fatalf("checkout without account: %v, want errBillingOff", err)
62 }
63
64 if err := srv.db.setConfig("stripe_express_account", "acct_express_1"); err != nil {
65 t.Fatal(err)
66 }
67 u, err := p.Checkout(context.Background(), CheckoutReq{
68 TierID: "tier1", PriceRef: "price_1", UserRef: "uid1",
69 SuccessURL: "https://s.example/ok", CancelURL: "https://s.example/no",
70 })
71 if err != nil || !strings.Contains(u, "checkout.stripe.com") {
72 t.Fatalf("checkout: %q, %v", u, err)
73 }
74 f := *captured
75 // The structural split at the suggested 11: 89% transfers, no
76 // application fee anywhere.
77 if f.Get("subscription_data[transfer_data][destination]") != "acct_express_1" ||
78 f.Get("subscription_data[transfer_data][amount_percent]") != "89" {
79 t.Errorf("transfer_data = %v, want acct_express_1 at 89%%", f)
80 }
81 for k := range f {
82 if strings.Contains(k, "application_fee") {
83 t.Errorf("managed checkout sent an application fee field: %s", k)
84 }
85 }
86 // The space binding rides both the session and the subscription.
87 if f.Get("metadata[space_ref]") != "acct_express_1" || f.Get("subscription_data[metadata][space_ref]") != "acct_express_1" {
88 t.Errorf("space binding missing: %v", f)
89 }
90 // Platform account means no Stripe-Account header, ever.
91 if len(*headers) != 0 {
92 t.Errorf("Stripe-Account header sent on a platform call: %v", *headers)
93 }
94}
95
96// The commission is pay-what-you-want within [5.5, 11]: the config picks,
97// out-of-range clamps, garbage falls back to the suggested 11.
98func TestManagedCommissionRange(t *testing.T) {
99 _, srv := newTestServer(t)
100 p := newTestPlatform(t, srv, "http://unused")
101 for cfg, want := range map[string]string{
102 "": "89", // unset β†’ suggested 11
103 "11": "89", // explicit suggestion
104 "5.5": "94.5", // the floor
105 "8": "92", // anywhere between
106 "3": "94.5", // below the floor clamps up
107 "50": "89", // above the range clamps to 11
108 "junk": "89", // unparseable β†’ suggested, never a freebie
109 } {
110 if cfg != "" {
111 if err := srv.db.setConfig("managed_commission_pct", cfg); err != nil {
112 t.Fatal(err)
113 }
114 }
115 if got := p.managedTransferPercent(); got != want {
116 t.Errorf("commission %q β†’ transfer %s, want %s", cfg, got, want)
117 }
118 }
119}
120
121// platformEvent builds a signed platform webhook body.
122func platformEvent(t *testing.T, evType string, object map[string]any) (body []byte, sig string) {
123 t.Helper()
124 raw, _ := json.Marshal(map[string]any{
125 "type": evType, "created": 1000,
126 "data": map[string]any{"object": object},
127 })
128 return raw, stripeSign(raw, "whsec_platform", time.Now())
129}
130
131func TestManagedWebhookBindsToSpace(t *testing.T) {
132 _, srv := newTestServer(t)
133 p := newTestPlatform(t, srv, "http://unused")
134 if err := srv.db.setConfig("stripe_express_account", "acct_ours"); err != nil {
135 t.Fatal(err)
136 }
137
138 ours := map[string]any{
139 "mode": "subscription", "customer": "cus1", "subscription": "sub1",
140 "metadata": map[string]any{"user_ref": "u1", "tier_ref": "t1", "space_ref": "acct_ours"},
141 }
142 body, sig := platformEvent(t, "checkout.session.completed", ours)
143 evs, err := p.ParseWebhook(sig, body)
144 if err != nil || len(evs) != 1 || evs[0].ExtSub != "sub1" || !evs[0].AllowCreate || !evs[0].Checkout {
145 t.Fatalf("our event: %+v, %v", evs, err)
146 }
147
148 // Foreign space's event (same platform account, valid signature): dropped.
149 foreign := map[string]any{
150 "mode": "subscription", "customer": "cus2", "subscription": "sub2",
151 "metadata": map[string]any{"user_ref": "u2", "tier_ref": "t2", "space_ref": "acct_theirs"},
152 }
153 body, sig = platformEvent(t, "checkout.session.completed", foreign)
154 if evs, err := p.ParseWebhook(sig, body); err != nil || evs != nil {
155 t.Fatalf("foreign event must drop silently: %+v, %v", evs, err)
156 }
157
158 // Unstamped event (no space_ref at all): dropped, not guessed.
159 unstamped := map[string]any{
160 "id": "sub3", "customer": "cus3", "status": "active",
161 "metadata": map[string]any{"user_ref": "u3", "tier_ref": "t3"},
162 }
163 body, sig = platformEvent(t, "customer.subscription.updated", unstamped)
164 if evs, err := p.ParseWebhook(sig, body); err != nil || evs != nil {
165 t.Fatalf("unstamped event must drop: %+v, %v", evs, err)
166 }
167
168 // Our subscription lifecycle event: normalized like the BYO provider's.
169 subUpd := map[string]any{
170 "id": "sub1", "customer": "cus1", "status": "past_due",
171 "current_period_end": 99999, "cancel_at_period_end": true,
172 "metadata": map[string]any{"user_ref": "u1", "tier_ref": "t1", "space_ref": "acct_ours"},
173 }
174 body, sig = platformEvent(t, "customer.subscription.updated", subUpd)
175 evs, err = p.ParseWebhook(sig, body)
176 if err != nil || len(evs) != 1 {
177 t.Fatalf("sub event: %+v, %v", evs, err)
178 }
179 if e := evs[0]; e.Status != "past_due" || !e.CancelAtPeriodEnd || e.CurrentPeriodEnd != 99999 || e.AllowCreate {
180 t.Errorf("normalized = %+v", e)
181 }
182
183 // Bad signature: hard error (Stripe retries), never a silent drop.
184 if _, err := p.ParseWebhook("t=1,v1=deadbeef", body); err == nil {
185 t.Error("bad signature accepted")
186 }
187}
188
189func TestManagedProviderSelection(t *testing.T) {
190 _, srv := newTestServer(t)
191
192 // Managed creds win over Connect creds β€” managed is the default provider.
193 t.Setenv("LCHAT_STRIPE_MANAGED_KEY", "sk_m")
194 t.Setenv("LCHAT_STRIPE_MANAGED_WEBHOOK_SECRET", "whsec_m")
195 t.Setenv("LCHAT_STRIPE_CONNECT_KEY", "sk_c")
196 t.Setenv("LCHAT_STRIPE_CONNECT_WEBHOOK_SECRET", "whsec_c")
197 if _, ok := newPayProvider(srv.db).(*stripePlatform); !ok {
198 t.Error("managed creds should select stripePlatform")
199 }
200
201 // Partial managed config is a deployment mistake: dormant, never half-working
202 // (and never a silent fallback to BYO β€” that would sell on the wrong model).
203 t.Setenv("LCHAT_STRIPE_MANAGED_WEBHOOK_SECRET", "")
204 if _, ok := newPayProvider(srv.db).(noPay); !ok {
205 t.Error("partial managed config should be dormant")
206 }
207
208 // No managed creds: BYO Connect as today.
209 t.Setenv("LCHAT_STRIPE_MANAGED_KEY", "")
210 if _, ok := newPayProvider(srv.db).(*stripeConnect); !ok {
211 t.Error("connect creds should select stripeConnect")
212 }
213}
214