mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
mana-sync's billing middleware short-circuited every push/pull with
402 for users without a sync subscription. Cards promises free Sync
in its Phase-1 GUIDELINES, so it shouldn't gate its own users on a
mana-credits subscription it never sells.
Implementation:
• billing.NewChecker now takes an exemptApps slice. The middleware
extracts {appId} from the URL path and short-circuits before the
user lookup if the app is in the set.
• Configurable via the BILLING_EXEMPT_APPS env var (comma-separated).
• Set BILLING_EXEMPT_APPS=cards on the mana-sync container so the
cards.mana.how Sync loop stops 402-ing.
• Tests cover the exemption + the empty/whitespace edge cases. All
other apps keep the original behaviour (fail-open if mana-credits
is unreachable, 402 if it explicitly says inactive).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
72 lines
1.9 KiB
Go
72 lines
1.9 KiB
Go
package billing
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
type stubValidator struct {
|
|
uid string
|
|
err error
|
|
}
|
|
|
|
func (s stubValidator) UserIDFromRequest(_ *http.Request) (string, error) {
|
|
return s.uid, s.err
|
|
}
|
|
|
|
// Routes for an exempt appID short-circuit before the user lookup
|
|
// happens. Asserting via downstream handler reachability + no
|
|
// mana-credits round-trip (the checker has no creditsURL configured —
|
|
// any fetch would fail).
|
|
func TestMiddleware_AppExemption(t *testing.T) {
|
|
c := NewChecker("http://invalid.invalid", "stub", []string{"cards"})
|
|
|
|
// Wire a mux that surfaces {appId} as a path value, like main.go does.
|
|
mux := http.NewServeMux()
|
|
called := false
|
|
mux.Handle("POST /sync/{appId}", c.Middleware(stubValidator{uid: "user-1"})(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
called = true
|
|
w.WriteHeader(http.StatusOK)
|
|
})))
|
|
|
|
cases := []struct {
|
|
name string
|
|
path string
|
|
wantStatus int
|
|
wantCalled bool
|
|
}{
|
|
{"exempt app passes without billing check", "/sync/cards", http.StatusOK, true},
|
|
{"non-exempt app reaches the (failing) billing check", "/sync/todo", http.StatusOK, true}, // fail-open keeps it open
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
called = false
|
|
req := httptest.NewRequest("POST", tc.path, nil)
|
|
rec := httptest.NewRecorder()
|
|
mux.ServeHTTP(rec, req)
|
|
if rec.Code != tc.wantStatus {
|
|
t.Errorf("status=%d want=%d", rec.Code, tc.wantStatus)
|
|
}
|
|
if called != tc.wantCalled {
|
|
t.Errorf("downstream called=%v want=%v", called, tc.wantCalled)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsAppExempt(t *testing.T) {
|
|
c := NewChecker("", "", []string{"cards", " ", "todo"})
|
|
if !c.IsAppExempt("cards") {
|
|
t.Error("expected cards to be exempt")
|
|
}
|
|
if !c.IsAppExempt("todo") {
|
|
t.Error("expected todo to be exempt")
|
|
}
|
|
if c.IsAppExempt("notes") {
|
|
t.Error("notes should not be exempt")
|
|
}
|
|
if c.IsAppExempt("") {
|
|
t.Error("empty appID should never be exempt")
|
|
}
|
|
}
|