package repositories import ( "encoding/json" "strings" "testing" "nearle/models" ) // Sign-in at a till is a mobile number and a four-digit PIN. These cover the // two halves separately — which account, and which secret — because the failure // that matters is not "a wrong PIN is refused" but "a right one is refused", // and every way that happens is a shop that cannot open. // The rule that decides whether a PIN may be *issued* is not the rule that // decides whether one may be *typed*. Live data holds 1234 on eleven accounts // and 1111 on nine; running the creation rule at sign-in would lock all twenty // out of the terminal this system signed them up to. func TestAPinTooWeakToIssueStillSignsIn(t *testing.T) { for _, pin := range []string{"1234", "1111", "9999", "4321"} { if _, err := validatePosPin(pin); err == nil { t.Errorf("PIN %q may now be issued; this test is checking the wrong rule", pin) } value, err := posLoginPin(pin) if err != nil { t.Errorf("PIN %q was refused at sign-in: %v — that account is locked out", pin, err) } if got := strings.TrimSpace(pin); value == 0 { t.Errorf("PIN %q parsed to 0", got) } } } func TestAPinOfferedAtSignInIsFourDigitsTheColumnCanHold(t *testing.T) { // Empty is a refusal here, unlike at creation where it means "this person // gets a password instead". An empty PIN reaching the comparison would ask // the database for `pin = 0`, which is what every account without one holds. for _, pin := range []string{"", " ", "123", "12345", "abcd", "12a4", "0451"} { if _, err := posLoginPin(pin); err == nil { t.Errorf("PIN %q was accepted at sign-in", pin) } } value, err := posLoginPin(" 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) } } // A number is typed by a person, not generated. It has to match the ten digits // the console stored however they wrote it down. func TestAMobileNumberIsMatchedInTheFormItIsStored(t *testing.T) { for _, typed := range []string{"9876543210", "+91 98765 43210", "098765-43210", " 91-9876543210 "} { field, value, err := posLoginIdentity(models.PosLoginRequest{Contactno: typed, Pin: "4821"}) if err != nil { t.Errorf("number %q was refused: %v", typed, err) continue } if field != "contactno" { t.Errorf("number %q was looked up by %q", typed, field) } if value != "9876543210" { t.Errorf("number %q normalised to %q, want 9876543210", typed, value) } } } func TestAnUnusableNumberIsAPlainRejection(t *testing.T) { // Not "that is not a mobile number" — this endpoint is unauthenticated, and // a distinct answer for a well-formed number is the first half of a // directory of who banks here. for _, typed := range []string{"12345", "98765432101234", "9876543210123"} { _, _, err := posLoginIdentity(models.PosLoginRequest{Contactno: typed, Pin: "4821"}) if err != errPosLoginRejected { t.Errorf("number %q answered %v, want the standard rejection", typed, err) } } // A field holding no digits at all is not a wrong number, it is an empty // one — and saying so is more use to somebody who fumbled the keyboard than // "those sign-in details were not recognised". for _, typed := range []string{"", " ", "abcdefghij"} { _, _, err := posLoginIdentity(models.PosLoginRequest{Contactno: typed, Pin: "4821"}) if err == nil || err == errPosLoginRejected { t.Errorf("number %q answered %v, want a request error naming the missing field", typed, err) } } } // A username still resolves an account, so terminals that have not shipped the // new screen keep working through the backfill. func TestAUsernameStillNamesAnAccount(t *testing.T) { field, value, err := posLoginIdentity(models.PosLoginRequest{ Authname: " supervisor.1135@pos.nearle.in ", Contactno: "9876543210", }) if err != nil { t.Fatalf("a username was refused: %v", err) } if field != "authname" || value != "supervisor.1135@pos.nearle.in" { t.Fatalf("looked up by %q = %q, want authname", field, value) } } func TestThePinIsCheckedAgainstTheAccountsOwn(t *testing.T) { secret, err := newPosLoginSecret(models.PosLoginRequest{Contactno: "9876543210", Pin: "4821"}) if err != nil { t.Fatalf("a well-formed PIN was refused: %v", err) } if !secret.byPin { t.Fatal("a request carrying a PIN was read as a password sign-in") } if !secret.matches(posLoginRow{Pin: 4821}) { t.Error("the right PIN was refused") } if secret.matches(posLoginRow{Pin: 4822}) { t.Error("a wrong PIN was accepted") } // The one that would matter most: an account with no PIN holds 0 in that // column, and every account on the platform did until this shipped. if secret.set(posLoginRow{Pin: 0}) { t.Error("an account with no PIN was treated as having one") } if secret.matches(posLoginRow{Pin: 0}) { t.Error("an account with no PIN was signed in") } // A password on the row is not a PIN, and must not stand in for one. if secret.matches(posLoginRow{Pin: 0, Password: "4821"}) { t.Error("a password was accepted as a PIN") } if !strings.Contains(secret.missing().Error(), "PIN") { t.Errorf("an account without a PIN was told %q", secret.missing()) } } func TestAPasswordStillOpensATillWhileNumbersAreBackfilled(t *testing.T) { secret, err := newPosLoginSecret(models.PosLoginRequest{ Authname: "supervisor.1135@pos.nearle.in", Password: "xHegDaH55ccWic", }) if err != nil { t.Fatalf("a password sign-in was refused: %v", err) } if secret.byPin { t.Fatal("a request carrying no PIN was read as a PIN sign-in") } if !secret.matches(posLoginRow{Password: "xHegDaH55ccWic"}) { t.Error("the right password was refused") } if secret.matches(posLoginRow{Password: "xHegDaH55ccWid"}) { t.Error("a wrong password was accepted") } if secret.matches(posLoginRow{Password: ""}) { t.Error("an account with no password was signed in") } // A PIN on the row is not a password. Symmetric to the check above, and the // reason both live in one function: two credentials checked in two places // is how one of them ends up satisfying the other. if secret.matches(posLoginRow{Pin: 4821}) { t.Error("a PIN was accepted as a password") } } func TestASignInWithNoCredentialAtAllIsRefused(t *testing.T) { if _, err := newPosLoginSecret(models.PosLoginRequest{Contactno: "9876543210"}); err == nil { t.Fatal("a sign-in offering neither a PIN nor a password was accepted") } // A malformed PIN answers the same as a wrong one, rather than confirming // that the number it was sent with exists. if _, err := newPosLoginSecret(models.PosLoginRequest{Contactno: "9876543210", Pin: "12"}); err != errPosLoginRejected { t.Errorf("a malformed PIN answered %v, want the standard rejection", err) } } // The session hands the terminal its outlet's staff so it can trade at once. // Now that a PIN is half of the sign-in, that list must not carry them: it // would hand every cashier their supervisor's credentials, and a supervisor // carries can_manage_staff. func TestTheStaffListDoesNotCarryPins(t *testing.T) { body, err := json.Marshal(models.PosSession{ Staff: []models.PosStaffMember{{Userid: 42, Fullname: "Priya Raman", Role: "Cashier", Pin: "4821"}}, }) if err != nil { t.Fatalf("session did not marshal: %v", err) } if strings.Contains(string(body), "4821") || strings.Contains(string(body), `"pin"`) { t.Fatalf("the login response carried a staff PIN: %s", body) } }