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>
140 lines
4.9 KiB
Go
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)
|
|
}
|
|
}
|