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) } } }