Files
backend_fiesta/middleware/posauthadoption_test.go
2026-09-30 18:01:59 +05:30

298 lines
9.6 KiB
Go

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
}