Files
doormile_backend/controllers/milerController_test.go
Suriya 2c26cbe4ba fix: panic recovery, rate limiting, transaction error handling, pagination
Hardening pass over the API surface. No route's auth requirements change.

Resilience:
- Add recover middleware. There was none, so an unhandled panic in any
  handler propagated out of the process instead of becoming a 500.
- Add a centralized ErrorHandler so errors and recovered panics return the
  same {success,message} envelope as the utils helpers, not Fiber's default
  plain-text body. 5xx responses are logged with method and path.

Rate limiting:
- Global 300/min per IP as an abuse backstop, exempting health/readiness
  probes and websocket upgrades.
- 10/min shared across every credential endpoint (customer/miler/admin/hub
  login, verify-pin, reset-pin, email OTP). PINs are 4 digits, so the whole
  keyspace was previously walkable in seconds. One shared limiter instance
  means rotating between endpoints doesn't reset the budget.
- Add TRUSTED_PROXIES config. Limits key on c.IP(), which behind a TLS
  terminator is the proxy, collapsing every client into one bucket. When set,
  X-Forwarded-For is honoured only from those proxies so the header can't be
  spoofed to dodge the limit. Logs a warning when unset.

Transactions:
- Check the error on all 51 previously-unchecked tx.Save/Create/Delete/
  Model(...).Update/Commit calls across 6 controllers. A failed write inside
  a transaction was silently ignored and the request still reported success;
  an unchecked Commit could fail with the caller told everything worked.
  Each site now rolls back and returns a specific message.

Pagination:
- Add utils.ParsePage/Paginated, reusing the pageno/pagesize convention
  GetAdminBookings already established. Default 500, hard cap 1000.
- Apply to the previously unbounded consignments, tripsheets, exceptions,
  app-users and clients endpoints. Defaults are high so existing consoles
  that don't paginate keep working; the cap only stops a growing table from
  being loaded wholesale. total is now a real COUNT, not len(data).
- GetClients also loaded the entire auth table to join in memory; it now
  fetches only the current page's rows.

Tests (first in the repo):
- Extract the hyperlocal pincode rule out of BookingPickupComplete into
  isHyperlocal so it is testable, covering the short/empty pincode fallback.
- Cover calculateVolumetricWeight and the ParsePage clamping rules.

Repo hygiene:
- Tag scratch/*.go with //go:build ignore. Each declared its own main(), so
  `go build ./...` failed on redeclaration; it now passes repo-wide.
- Untrack scratch/node_modules (216 files) and ignore node_modules, test
  artifacts, and the `doormile` binary `go build .` emits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 12:13:39 +05:30

95 lines
2.2 KiB
Go

package controllers
import "testing"
func TestIsHyperlocal(t *testing.T) {
cases := []struct {
name string
pickup string
delivery string
want bool
}{
{
name: "same coimbatore postal area routes hyperlocal",
pickup: "641012", // Gandhipuram
delivery: "641004", // Peelamedu
want: true,
},
{
name: "identical pincode routes hyperlocal",
pickup: "641012",
delivery: "641012",
want: true,
},
{
name: "coimbatore to chennai is not hyperlocal",
pickup: "641012",
delivery: "600001",
want: false,
},
{
name: "adjacent prefixes are not hyperlocal",
pickup: "641012",
delivery: "642012",
want: false,
},
{
// Bad data must fall back to the hub route rather than sending a
// cross-city parcel out for local delivery.
name: "short pickup pincode is not hyperlocal",
pickup: "64",
delivery: "641012",
want: false,
},
{
name: "short delivery pincode is not hyperlocal",
pickup: "641012",
delivery: "64",
want: false,
},
{
name: "empty pincodes are not hyperlocal",
pickup: "",
delivery: "",
want: false,
},
{
name: "exactly three digits is enough to match",
pickup: "641",
delivery: "641999",
want: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := isHyperlocal(tc.pickup, tc.delivery); got != tc.want {
t.Errorf("isHyperlocal(%q, %q) = %v, want %v", tc.pickup, tc.delivery, got, tc.want)
}
})
}
}
func TestCalculateVolumetricWeight(t *testing.T) {
cases := []struct {
name string
l, w, h float64
want float64
}{
{name: "zero dimensions weigh nothing", l: 0, w: 0, h: 0, want: 0},
{name: "standard divisor of 5000", l: 50, w: 40, h: 30, want: 12},
{name: "one centimetre cube", l: 1, w: 1, h: 1, want: 1.0 / 5000.0},
{name: "large parcel", l: 100, w: 100, h: 100, want: 200},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := calculateVolumetricWeight(tc.l, tc.w, tc.h)
if got != tc.want {
t.Errorf("calculateVolumetricWeight(%v, %v, %v) = %v, want %v",
tc.l, tc.w, tc.h, got, tc.want)
}
})
}
}