Files
backend_fiesta/services/userLogin_test.go
Suriyakumarvijayanayagam 1633617dc4 Stop reporting a failed login lookup as "Invalid Email"
GetUserByAuthname / GetUserByContactNo / GetUserLogin discarded the
Scan error, so a database that could not answer — down, pool exhausted,
or booted without its config (2026-07-20) — came back as uid 0 and every
user was told their email was wrong.

One lookup, GetUserLogin, now returns an error; sql.ErrNoRows is "not
found" and anything else reaches the service, which answers 500 "Login
is temporarily unavailable" and logs the cause. 409 "Invalid Email" is
unchanged for a genuine no-match: the console reads that exact shape as
"not registered". NULL password/role columns scan through sql.Null* so
they do not become 500s.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 17:04:33 +05:30

140 lines
4.9 KiB
Go

package services
import (
"errors"
"testing"
"nearle/models"
"nearle/repositories"
)
/*
"Invalid Email" has to mean one thing: the sign-in query ran and matched
nobody.
It used to also mean "the database did not answer". The repository discarded
the Scan error, so an outage, an exhausted pool or a deployment that had lost
its environment all surfaced as uid 0 — and every user on the platform was told
their email was wrong (2026-07-20). The console makes it worse: it reads a 409
as "this address is not registered" and sends the person to sign-up.
*/
// loginRepo is a UserRepository that answers only the sign-in path. Anything
// else panics on the nil embedded interface, which is the point: these tests
// must not reach further than the lookup.
type loginRepo struct {
repositories.UserRepository
uid int
password string
status string
roleid int
err error
field, value string
configid int
}
func (r *loginRepo) GetUserLogin(field, value string, configid int) (int, string, string, int, error) {
r.field, r.value, r.configid = field, value, configid
if r.err != nil {
return 0, "", "", 0, r.err
}
return r.uid, r.password, r.status, r.roleid, nil
}
func (r *loginRepo) UpdateFCMToken(int, string) error { return nil }
func (r *loginRepo) UpdateUserFcmToken(int, string) error { return nil }
func (r *loginRepo) GetTenantUserById(uid int) models.TenantUserInfo {
return models.TenantUserInfo{Userid: uid}
}
func TestADatabaseFailureIsNotAnInvalidEmail(t *testing.T) {
repo := &loginRepo{err: errors.New("dial tcp 10.0.0.5:5433: connection refused")}
svc := NewUserService(repo)
user := models.User{Authname: "owner@shop.example", Password: "pw", Configid: 1}
t.Run("app login", func(t *testing.T) {
_, resp, err := svc.AppLogin(user)
if err == nil {
t.Fatal("a failed lookup must be an error to the controller")
}
if resp["code"] != 500 || resp["status"] != false {
t.Fatalf("want a 500/false envelope, got %v", resp)
}
if resp["message"] == "Invalid Email" {
t.Fatal("the database being down was reported as a wrong email")
}
})
t.Run("tenant web login", func(t *testing.T) {
_, resp := svc.TenantWebLogin(user)
if resp["code"] != 500 || resp["status"] != false {
t.Fatalf("want a 500/false envelope, got %v", resp)
}
if resp["message"] == "Invalid Email" {
t.Fatal("the database being down was reported as a wrong email")
}
})
}
// The console depends on this exact shape — code 409 — to tell "not
// registered" from every other failure, so it must survive the refactor.
func TestNobodyMatchingIsStillInvalidEmail(t *testing.T) {
repo := &loginRepo{} // uid 0, no error: the query ran and found no row
svc := NewUserService(repo)
user := models.User{Authname: "nobody@shop.example", Password: "pw", Configid: 1}
_, resp, err := svc.AppLogin(user)
if err == nil || resp["code"] != 409 || resp["message"] != "Invalid Email" {
t.Fatalf("app login: want 409 Invalid Email, got %v / %v", resp, err)
}
_, wresp := svc.TenantWebLogin(user)
if wresp["code"] != 409 || wresp["message"] != "Invalid Email" {
t.Fatalf("web login: want 409 Invalid Email, got %v", wresp)
}
}
func TestLookupPrefersAuthnameAndFallsBackToContactNo(t *testing.T) {
repo := &loginRepo{uid: 7, password: "pw", status: "Active"}
svc := NewUserService(repo)
svc.AppLogin(models.User{Authname: "owner@shop.example", Contactno: "9999999999", Password: "pw", Configid: 1})
if repo.field != "authname" || repo.value != "owner@shop.example" || repo.configid != 1 {
t.Fatalf("authname should win when both are sent, looked up %s=%q configid=%d", repo.field, repo.value, repo.configid)
}
svc.AppLogin(models.User{Contactno: "9999999999", Password: "pw", Configid: 1})
if repo.field != "contactno" || repo.value != "9999999999" {
t.Fatalf("contactno should be used when authname is blank, looked up %s=%q", repo.field, repo.value)
}
}
func TestNeitherIdentifierIsRefusedBeforeTheLookup(t *testing.T) {
repo := &loginRepo{err: errors.New("must not be called")}
svc := NewUserService(repo)
_, resp, err := svc.AppLogin(models.User{Password: "pw", Configid: 1})
if err == nil || resp["code"] != 400 {
t.Fatalf("want 400, got %v / %v", resp, err)
}
if repo.field != "" {
t.Fatal("the repository was queried with nothing to match on")
}
}
func TestAMatchedAccountStillSignsIn(t *testing.T) {
repo := &loginRepo{uid: 42, password: "secret", status: "Active", roleid: 2}
svc := NewUserService(repo)
info, resp, err := svc.AppLogin(models.User{Authname: "owner@shop.example", Password: "secret", Configid: 1})
if err != nil || resp["code"] != 200 || info.Userid != 42 {
t.Fatalf("app login: want success for userid 42, got %v / %v / %+v", resp, err, info)
}
winfo, wresp := svc.TenantWebLogin(models.User{Authname: "owner@shop.example", Password: "secret", Configid: 1, Roleid: 2})
if wresp["code"] != 200 || winfo.Userid != 42 {
t.Fatalf("web login: want success for userid 42, got %v / %+v", wresp, winfo)
}
}