123 lines
4.1 KiB
Go
123 lines
4.1 KiB
Go
package middlewares
|
|
|
|
import (
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
)
|
|
|
|
// The idempotency key namespace is a security boundary, not bookkeeping.
|
|
//
|
|
// POST /customer/auth/otp/verify is UNAUTHENTICATED, so c.Locals("userid") is
|
|
// absent there. Scoping on it anyway put every anonymous caller in one
|
|
// namespace: two customers picking the same Idempotency-Key would collide and
|
|
// the second would be handed the first's access token, refresh token and
|
|
// customer record. These tests pin the fix.
|
|
|
|
// scopeFor runs idempotencyScope inside a real request and returns what it
|
|
// produced.
|
|
func scopeFor(t *testing.T, authedUserID int, path, body string) string {
|
|
t.Helper()
|
|
|
|
app := fiber.New(fiber.Config{DisableStartupMessage: true})
|
|
app.Post("/*", func(c *fiber.Ctx) error {
|
|
if authedUserID != 0 {
|
|
c.Locals("userid", authedUserID)
|
|
}
|
|
return c.SendString(idempotencyScope(c))
|
|
})
|
|
|
|
req := httptest.NewRequest("POST", path, strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := app.Test(req, 5000)
|
|
if err != nil {
|
|
t.Fatalf("test request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
buf := make([]byte, 256)
|
|
n, _ := resp.Body.Read(buf)
|
|
return string(buf[:n])
|
|
}
|
|
|
|
// Two anonymous callers sending DIFFERENT bodies must never share a namespace,
|
|
// even with an identical Idempotency-Key. This is the session-handover bug.
|
|
func TestAnonymousCallersNeverShareAnIdempotencyNamespace(t *testing.T) {
|
|
alice := scopeFor(t, 0, "/customer/auth/otp/verify",
|
|
`{"identifier":"+919876543210","code":"1111"}`)
|
|
bob := scopeFor(t, 0, "/customer/auth/otp/verify",
|
|
`{"identifier":"+919000000001","code":"2222"}`)
|
|
|
|
if alice == bob {
|
|
t.Fatalf("two different anonymous requests share the scope %q — "+
|
|
"one customer's session could be replayed to another", alice)
|
|
}
|
|
if alice == "" || bob == "" {
|
|
t.Fatal("empty scope: every request must land in some namespace")
|
|
}
|
|
}
|
|
|
|
// The same anonymous caller repeating the SAME request must replay — that is
|
|
// the whole point of idempotency.
|
|
func TestIdenticalAnonymousRequestReplays(t *testing.T) {
|
|
body := `{"identifier":"+919876543210","code":"4821"}`
|
|
first := scopeFor(t, 0, "/customer/auth/otp/verify", body)
|
|
again := scopeFor(t, 0, "/customer/auth/otp/verify", body)
|
|
|
|
if first != again {
|
|
t.Errorf("the same request produced two scopes (%q, %q) — a retry would "+
|
|
"re-execute instead of replaying", first, again)
|
|
}
|
|
}
|
|
|
|
// The authenticated format is byte-identical to what this middleware has always
|
|
// produced. Changing it would orphan every in-flight key in Redis at deploy
|
|
// time, and a rider retrying a pickup-complete across that boundary would
|
|
// collect COD twice instead of replaying.
|
|
func TestAuthenticatedScopeFormatIsUnchanged(t *testing.T) {
|
|
got := scopeFor(t, 42, "/miler/bookings/7/pickup-complete", `{}`)
|
|
if got != "42" {
|
|
t.Errorf("authenticated scope = %q, want %q — the stored key format must "+
|
|
"not change across a deploy", got, "42")
|
|
}
|
|
}
|
|
|
|
// An anonymous scope can never collide with an authenticated one: the old
|
|
// format is always numeric, the new one never is.
|
|
func TestAnonymousScopeCannotCollideWithAnAuthenticatedOne(t *testing.T) {
|
|
anon := scopeFor(t, 0, "/customer/auth/otp/verify", `{"code":"1"}`)
|
|
if !strings.HasPrefix(anon, "anon-") {
|
|
t.Errorf("anonymous scope = %q, want an 'anon-' prefix so it cannot look "+
|
|
"like a user id", anon)
|
|
}
|
|
}
|
|
|
|
// The operating-city gate, exported because the customer booking shape carries
|
|
// no pincode for CityGateMiddleware to sniff.
|
|
func TestPincodeInOperatingCity(t *testing.T) {
|
|
cases := []struct {
|
|
pincode string
|
|
wantCity string
|
|
wantOK bool
|
|
}{
|
|
{"641012", "Coimbatore", true},
|
|
{"600001", "Chennai", true},
|
|
{"560034", "Bengaluru", true},
|
|
{"500081", "Hyderabad", true},
|
|
{"629001", "Nagercoil", true},
|
|
{"110001", "", false}, // Delhi — not an operating city
|
|
{"12", "", false}, // too short to classify
|
|
{"", "", false},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
city, ok := PincodeInOperatingCity(tc.pincode)
|
|
if ok != tc.wantOK || city != tc.wantCity {
|
|
t.Errorf("PincodeInOperatingCity(%q) = (%q, %v), want (%q, %v)",
|
|
tc.pincode, city, ok, tc.wantCity, tc.wantOK)
|
|
}
|
|
}
|
|
}
|