pos login with the ph number and pin

This commit is contained in:
2026-08-12 17:10:27 +05:30
parent ff72af9a8a
commit d566ca5591
9 changed files with 1007 additions and 91 deletions

View File

@@ -0,0 +1,197 @@
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)
}
}