package middleware import ( "net/http/httptest" "strings" "testing" "time" "nearle/services" "nearle/utils" "github.com/gofiber/fiber/v2" ) /* Counting the till fleet's adoption of the session token. This exists to answer one question — is it safe to set POS_AUTH_REQUIRED=true — and the cost of answering it wrong is a cashier at a counter who cannot ring a bill. So these are mostly about the figure being honest: not flattering, not alarmist, and never able to change whether a request is served. */ // resetAdoption puts the counters back, since they are process-wide. func resetAdoption(t *testing.T) { t.Helper() posAdoption.Lock() posAdoption.since = time.Now() posAdoption.tokened = 0 posAdoption.untokened = 0 posAdoption.outlets = map[int]*posOutletSeen{} posAdoption.paths = map[string]int64{} posAdoption.truncated = false posAdoption.Unlock() } func TestAnUntokenedTillIsNamedByItsOutlet(t *testing.T) { // The whole point. Without this list, switching enforcement on is a guess // about which shops stop trading. resetAdoption(t) recordPosUntokened(1185, "/live/api/v1/pos/orders") recordPosUntokened(1185, "/live/api/v1/pos/orders") recordPosUntokened(1170, "/live/api/v1/pos/catalogue") recordPosToken() report := PosAdoptionReport() if report.Untokened != 3 || report.Tokened != 1 { t.Fatalf("counts wrong: %+v", report) } if len(report.Outlets) != 2 { t.Fatalf("outlets: %+v", report.Outlets) } // Busiest first — the outlet ringing the most bills is the one that hurts // most if enforcement goes on before it has adopted. if report.Outlets[0].Locationid != 1185 || report.Outlets[0].Requests != 2 { t.Errorf("not ordered by traffic: %+v", report.Outlets) } } func TestTheAdoptedPercentageIsOfEverything(t *testing.T) { resetAdoption(t) for i := 0; i < 3; i++ { recordPosToken() } recordPosUntokened(1185, "/pos/orders") if got := PosAdoptionReport().AdoptedPercent; got != 75 { t.Fatalf("adopted = %v%%, want 75", got) } } func TestNoTrafficIsNotAHundredPercent(t *testing.T) { // A fleet nobody has used is not a fleet that has adopted. Reporting 100% // here is the single most dangerous rounding this file could do — it would // green-light the flag on an empty window. resetAdoption(t) report := PosAdoptionReport() if report.AdoptedPercent != 0 { t.Fatalf("empty window reported as %v%%", report.AdoptedPercent) } if !strings.Contains(report.Verdict, "nothing to conclude") { t.Errorf("verdict oversells an empty window: %q", report.Verdict) } } func TestACleanWindowStillSaysToKeepWatching(t *testing.T) { // Zero untokened requests in one hour is not an adopted fleet — a shop that // is shut has no traffic either. The verdict has to say so, because the // number on its own reads as permission. resetAdoption(t) recordPosToken() verdict := PosAdoptionReport().Verdict if !strings.Contains(verdict, "trading days") { t.Errorf("a one-request window was treated as proof: %q", verdict) } } func TestAnUntokenedFleetIsNotDescribedAsReady(t *testing.T) { resetAdoption(t) recordPosToken() recordPosUntokened(1185, "/pos/orders") verdict := PosAdoptionReport().Verdict if !strings.Contains(verdict, "stop being able to trade") { t.Errorf("the consequence is not stated: %q", verdict) } } func TestARequestThatNamesNoOutletIsStillCounted(t *testing.T) { // `/pos/staff` deliberately takes no location parameter. Such a request is // still an untokened till, and dropping it would understate the problem. resetAdoption(t) recordPosUntokened(0, "/live/api/v1/pos/staff") report := PosAdoptionReport() if report.Untokened != 1 { t.Fatalf("not counted: %+v", report) } if len(report.Outlets) != 0 { t.Errorf("invented an outlet for a request that named none: %+v", report.Outlets) } if len(report.Paths) != 1 || report.Paths[0].Path != "/live/api/v1/pos/staff" { t.Errorf("the route was lost: %+v", report.Paths) } if !strings.Contains(report.Verdict, "cannot be traced") { t.Errorf("verdict does not explain the blind spot: %q", report.Verdict) } } func TestOutletsCannotGrowWithoutBound(t *testing.T) { // `store_id` comes off the wire. Without a cap an untokened caller could // name ten thousand outlets and spend the server's memory doing it. resetAdoption(t) for i := 1; i <= posAdoptionCap+50; i++ { recordPosUntokened(i, "/pos/orders") } report := PosAdoptionReport() if len(report.Outlets) > posAdoptionCap { t.Fatalf("listed %d outlets, cap is %d", len(report.Outlets), posAdoptionCap) } if report.Untokened != int64(posAdoptionCap+50) { // The total must stay true even when the list is trimmed. t.Errorf("total under-reported: %d", report.Untokened) } if !report.Truncated { t.Error("a trimmed list was presented as complete") } } func TestFirstAndLastSeenAreBothKept(t *testing.T) { // "This till stopped calling untokened three days ago" and "it did so a // minute ago" are different facts, and only one of them means it has been // updated. So the first sighting must stick and the last must move. // // The clock is wound back rather than slept through: the report formats to // RFC3339, which is second-precision, and a test that waits a second to // prove an assignment is a second every run forever. resetAdoption(t) recordPosUntokened(1185, "/pos/orders") posAdoption.Lock() seen := posAdoption.outlets[1185] seen.FirstSeen = seen.FirstSeen.Add(-48 * time.Hour) seen.LastSeen = seen.LastSeen.Add(-48 * time.Hour) posAdoption.Unlock() recordPosUntokened(1185, "/pos/orders") out := PosAdoptionReport().Outlets[0] if out.FirstSeen == "" || out.LastSeen == "" { t.Fatalf("timestamps missing: %+v", out) } if out.Requests != 2 { t.Errorf("requests = %d, want 2", out.Requests) } if out.FirstSeen == out.LastSeen { t.Errorf("last seen never moved: %+v", out) } if out.FirstSeen > out.LastSeen { // RFC3339 sorts lexically, so this comparison is meaningful. t.Errorf("first seen is after last seen: %+v", out) } } func TestTheReportSaysWhetherEnforcementIsAlreadyOn(t *testing.T) { resetAdoption(t) t.Setenv("POS_AUTH_REQUIRED", "true") report := PosAdoptionReport() if !report.Enforced { t.Fatal("enforcement is on and the report says otherwise") } if !strings.Contains(report.Verdict, "already on") { t.Errorf("verdict ignores that the work is done: %q", report.Verdict) } } /* ── The guard still behaves exactly as it did ───────────────────────────── */ func TestCountingNeverChangesWhetherARequestIsServed(t *testing.T) { // This whole file is instrumentation. If it can refuse a request, or let // one through that should have been refused, it has become the thing it was // built to make safe. // // Both sides of the flag, against the real middleware. t.Setenv("POS_TOKEN_SECRET", posTestSecret) for _, tc := range []struct { name string required string want int }{ {"off: an untokened till still trades", "", 200}, {"on: an untokened till is refused", "true", 401}, } { t.Run(tc.name, func(t *testing.T) { resetAdoption(t) t.Setenv("POS_AUTH_REQUIRED", tc.required) got := callPos(t, "GET", "/live/api/v1/pos/catalogue?store_id=1185", "") if got != tc.want { t.Fatalf("status %d, want %d", got, tc.want) } // Counted either way: the figure is about what the fleet is doing, // not about what the flag currently allows. if report := PosAdoptionReport(); report.Untokened != 1 { t.Errorf("untokened = %d, want 1", report.Untokened) } }) } } func TestOnlyARequestThatWouldSurviveEnforcementCountsAsAdopted(t *testing.T) { // A token that verifies but names somebody else's outlet is refused, and // must NOT be counted as adopted — otherwise a misconfigured till inflates // the very number used to decide the flag is safe to set. t.Setenv("POS_TOKEN_SECRET", posTestSecret) t.Setenv("POS_AUTH_REQUIRED", "") resetAdoption(t) token := posTokenFor(t, 1147, 1185) if got := callPos(t, "GET", "/live/api/v1/pos/catalogue?store_id=9999", token); got != 403 { t.Fatalf("a token was allowed to name another tenant's outlet: %d", got) } if report := PosAdoptionReport(); report.Tokened != 0 { t.Errorf("a refused request was counted as adopted: %+v", report) } } /* ── Harness ─────────────────────────────────────────────────────────────── */ const posTestSecret = "a-pos-signing-secret-of-ample-length" // posLocations answers the tenant-owns-outlet question without a database. // Only LocationAllowed is real; anything else the guard touched would panic, // which is the signal wanted. type posLocations struct { services.PosService } func (posLocations) LocationAllowed(tenantID, locationID int) (bool, error) { // Tenant 1147 owns 1185 and nothing else, which is all these tests need. return tenantID == 1147 && locationID == 1185, nil } func posTokenFor(t *testing.T, tenantID, locationID int) string { t.Helper() token, _, err := utils.MintPosToken(utils.PosClaims{ Tenantid: tenantID, Locationid: locationID, Configid: 1, }, time.Now()) if err != nil { t.Fatalf("minting a terminal session: %v", err) } return token } func callPos(t *testing.T, method, target, token string) int { t.Helper() app := fiber.New() app.Use("/live/api/v1/pos", PosAuth(posLocations{})) app.All("/live/api/v1/pos/*", func(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusOK) }) req := httptest.NewRequest(method, target, nil) if token != "" { req.Header.Set("Authorization", "Bearer "+token) } resp, err := app.Test(req) if err != nil { t.Fatalf("calling: %v", err) } return resp.StatusCode }