package repositories import ( "testing" "nearle/models" ) // A PIN has to survive a round trip through a `bigint` column, and has to be // hard enough to guess to be worth having. These cover both, because the schema // makes the first one non-obvious. func TestAPinMustSurviveTheColumnItIsStoredIn(t *testing.T) { // `app_users.pin` is a bigint. "0451" stored there comes back as 451, so a // cashier would type four digits and be refused for ever. Live data already // holds one such account. if _, err := validatePosPin("0451"); err == nil { t.Fatal("a PIN starting with zero was accepted; it cannot round-trip through a bigint") } value, err := validatePosPin("4821") if err != nil { t.Fatalf("a good PIN was refused: %v", err) } if value != 4821 { t.Fatalf("PIN parsed to %d, want 4821", value) } } func TestAPinIsExactlyFourDigits(t *testing.T) { for _, pin := range []string{"123", "12345", "abcd", "12a4", " 12 "} { if _, err := validatePosPin(pin); err == nil { t.Errorf("PIN %q was accepted", pin) } } } // The first thing anyone tries. Live data has 1234 on eleven accounts and 1111 // on nine, which is exactly the outcome this prevents repeating. func TestAnObviousPinIsRefused(t *testing.T) { for _, pin := range []string{"1234", "1111", "2345", "4321", "9999", "2222"} { if _, err := validatePosPin(pin); err == nil { t.Errorf("PIN %q was accepted despite being one of the first guessed", pin) } } } // An empty PIN is not an error — somebody may be given a password instead. The // caller decides whether having neither is a problem. func TestAnAbsentPinIsNotAnError(t *testing.T) { value, err := validatePosPin("") if err != nil { t.Fatalf("an absent PIN was treated as invalid: %v", err) } if value != 0 { t.Fatalf("an absent PIN parsed to %d, want 0", value) } } // A stored PIN the schema cannot represent as four digits comes back empty // rather than short, because a three-digit PIN on screen is one a cashier // cannot type — and they would have no way to describe the fault. func TestAnUnrepresentablePinIsNotShown(t *testing.T) { if got := posPinString(451); got != "" { t.Fatalf("a three-digit PIN rendered as %q, want empty", got) } if got := posPinString(0); got != "" { t.Fatalf("an unset PIN rendered as %q, want empty", got) } if got := posPinString(4821); got != "4821" { t.Fatalf("PIN rendered as %q, want 4821", got) } } func TestANameIsSplitAcrossTheTwoColumnsThisSchemaHas(t *testing.T) { cases := []struct { in string first, last string }{ {"Asha", "Asha", ""}, {"Asha Kumar", "Asha", "Kumar"}, {"Ragul Kannan Selvam", "Ragul", "Kannan Selvam"}, {" Divya R ", "Divya", "R"}, {"", "", ""}, } for _, tc := range cases { first, last := splitName(tc.in) if first != tc.first || last != tc.last { t.Errorf("splitName(%q) = (%q, %q), want (%q, %q)", tc.in, first, last, tc.first, tc.last) } } } // Only a role that can actually be checked should grant anything. Zero is the // one that matters: it is not a role, it is what an account carries when nobody // set one, and live data has riders and shop accounts sharing it. func TestOnlyRealRolesCanManageStaff(t *testing.T) { if models.PosRoleCanManageStaff(0) { t.Error("roleid 0 was allowed to manage staff; it is unset, not a role") } if models.PosRoleCanManageStaff(models.PosRoleCashier) { t.Error("a cashier was allowed to manage staff, so could promote themselves") } if !models.PosRoleCanManageStaff(models.PosRoleSupervisor) { t.Error("a supervisor was refused staff management, which is their whole purpose") } // Somebody who already administers the shop from a browser is not made less // privileged by standing at the counter. for _, role := range []int{1, 2, 3, 4, 5, 6} { if !models.PosRoleCanManageStaff(role) { t.Errorf("back-office role %d was refused staff management", role) } } } func TestARoleIsReadFromItsNameNotItsNumber(t *testing.T) { if got := models.PosRoleFromName("supervisor"); got != models.PosRoleSupervisor { t.Errorf("supervisor = %d, want %d", got, models.PosRoleSupervisor) } if got := models.PosRoleFromName(" Cashier "); got != models.PosRoleCashier { t.Errorf("cashier = %d, want %d", got, models.PosRoleCashier) } // Anything unrecognised is zero, and every caller treats zero as a refusal // rather than as a default — an unknown role must never become a supervisor. for _, name := range []string{"", "admin", "manager", "owner", "7"} { if got := models.PosRoleFromName(name); got != 0 { t.Errorf("PosRoleFromName(%q) = %d, want 0", name, got) } } } // The web console writes `app_users` too, through `tenants/createstaff`, and // that path had no validation at all. These cover the shared rule set, so a // person created from a browser is subject to the same constraints as one // created at a till — two paths writing one table is how they drift. func TestStaffFromTheWebConsoleObeysTheTillsRules(t *testing.T) { cases := []struct { name string user models.User ok bool }{ { name: "a usable cashier", user: models.User{Firstname: "Asha", Roleid: models.PosRoleCashier, Pin: 7391}, ok: true, }, { name: "a password instead of a PIN is fine", user: models.User{Firstname: "Asha", Roleid: models.PosRoleCashier, Password: "s3cret"}, ok: true, }, { name: "no name", user: models.User{Roleid: models.PosRoleCashier, Pin: 7391}, }, { name: "no role — 0 is unset, not a role", user: models.User{Firstname: "Asha", Pin: 7391}, }, { name: "no way at all to sign in", user: models.User{Firstname: "Asha", Roleid: models.PosRoleCashier}, }, { // 451 is what "0451" becomes in a bigint column. Accepting it here // creates somebody who types four digits and is refused for ever. name: "a PIN the column cannot hold", user: models.User{Firstname: "Asha", Roleid: models.PosRoleCashier, Pin: 451}, }, { name: "a PIN anyone would guess first", user: models.User{Firstname: "Asha", Roleid: models.PosRoleCashier, Pin: 1234}, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { user := tc.user _, err := ValidateStaffUser(&user) if tc.ok && err != nil { t.Fatalf("refused a valid staff row: %v", err) } if !tc.ok && err == nil { t.Fatal("accepted a staff row the till could not use") } }) } }