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) } }