package routes_test import ( "fmt" "net/http" "os" "strings" "testing" "doormile/constants" "doormile/db" "doormile/internal/testpg" "doormile/models" "doormile/utils" "gorm.io/gorm" ) // Rider accounts end to end against a real Postgres: create-time checks, // duplicate phones, editing the phone, and block / unblock actually holding. // Postgres-gated; the DSN must be a THROWAWAY database. func milerAccountDB(t *testing.T) *gorm.DB { t.Helper() dsn := os.Getenv("REGISTRY_TEST_DSN") if dsn == "" { t.Skip("REGISTRY_TEST_DSN not set; skipping Postgres miler account test") } gdb := testpg.Open(t, dsn, "miler_account_routes_test") all := []any{&models.AppUser{}, &models.MilerProfile{}, &models.MilerDutyLog{}, &models.Hub{}, &models.AppLocation{}, &models.Tenant{}} if err := gdb.Migrator().DropTable(all...); err != nil { t.Fatal(err) } if err := gdb.AutoMigrate(all...); err != nil { t.Fatal(err) } prev := db.DB db.DB = gdb t.Cleanup(func() { db.DB = prev }) gdb.Create(&models.AppLocation{Applocationid: 1, Applocationname: "Coimbatore", Status: "Active"}) gdb.Create(&models.AppLocation{Applocationid: 2, Applocationname: "Hyderabad", Status: "Active"}) gdb.Create(&models.Hub{Hubid: 11, Hubname: "Coimbatore Neptune Hub", Hubtype: "delivery_hub", Applocationid: 1, Status: "Active"}) gdb.Create(&models.Hub{Hubid: 21, Hubname: "Hyderabad Mars Hub", Hubtype: "delivery_hub", Applocationid: 2, Status: "Active"}) gdb.Create(&models.Tenant{Tenantid: 5, Tenantname: "Sai's Kitchen", Primaryemail: "sai@k.test", Primarycontact: "9000000005", Status: "Active"}) // A client login whose email a rider must not reuse. gdb.Create(&models.AppUser{Authname: "Sai", Email: "sai@k.test", Contactno: "9000000005", Password: "x", Roleid: 3, Status: "Active", Configid: 1}) return gdb } func riderBody(name, phone, email, extra string) string { return fmt.Sprintf(`{"authname":%q,"displayname":%q,"contactno":%q,"email":%q,"tenantid":5,"applocationid":1%s}`, name, name, phone, email, extra) } func TestCreateMilerChecksEverythingAndRefusesDuplicates(t *testing.T) { gdb := milerAccountDB(t) app := onboardingApp() staff := consoleToken(t, "ops@doormile.com", 1, 0) refused := []struct{ name, body, want string }{ {"bad phone", riderBody("Ravi", "12345", "ravi@r.test", ""), "10-digit"}, {"no email", riderBody("Ravi", "9876500001", "", ""), "valid email"}, {"no name", riderBody("", "9876500001", "ravi@r.test", ""), "login name"}, {"unknown vehicle", riderBody("Ravi", "9876500001", "ravi@r.test", `,"defaultvehicletype":"Rocket"`), "vehicle type"}, {"hub in another city", riderBody("Ravi", "9876500001", "ravi@r.test", `,"hubid":21`), "different city"}, {"unknown client", strings.Replace(riderBody("Ravi", "9876500001", "ravi@r.test", ""), `"tenantid":5`, `"tenantid":99`, 1), "client does not exist"}, {"email used by a client login", riderBody("Ravi", "9876500001", "SAI@k.test", ""), "already used by another login"}, } for _, r := range refused { code, body := do(t, app, http.MethodPost, "/api/v1/admin/milers", staff, r.body) if code < 400 || code >= 500 || !strings.Contains(body, r.want) { t.Errorf("%s: %d %s, want a 4xx containing %q", r.name, code, body, r.want) } } // A good rider, typed the way people type numbers, with a lower-case vehicle. code, body := do(t, app, http.MethodPost, "/api/v1/admin/milers", staff, riderBody("Ravi", "+91 98765 00001", "Ravi@R.test", `,"defaultvehicletype":"bike","hubid":11`)) if code != 201 { t.Fatalf("create = %d %s", code, body) } var u models.AppUser gdb.Where("roleid = 5").First(&u) var p models.MilerProfile gdb.Where("userid = ?", u.Userid).First(&p) if u.Contactno != "9876500001" || u.Email != "ravi@r.test" || u.Configid != 1001 || p.Defaultvehicletype != "Bike" || p.Phone != "9876500001" { t.Fatalf("stored rider = %+v / %+v", u, p) } // The same number again, however it is written, is refused. code, body = do(t, app, http.MethodPost, "/api/v1/admin/milers", staff, riderBody("Ravi Two", "098765 00001", "ravi2@r.test", "")) if code != 409 || !strings.Contains(body, "phone number already exists") { t.Fatalf("duplicate phone = %d %s, want 409", code, body) } } func TestMilerLoginPrefersTheActiveRiderWhenAPhoneIsShared(t *testing.T) { gdb := milerAccountDB(t) app := onboardingApp() // Rows from before duplicates were refused: an old blocked rider first. gdb.Create(&models.AppUser{Authname: "Old", Email: "old@r.test", Contactno: "9876500002", Password: "", Roleid: 5, Status: "Blocked", Configid: 1001}) gdb.Create(&models.AppUser{Authname: "New", Email: "new@r.test", Contactno: "9876500002", Password: "", Roleid: 5, Status: "Active", Configid: 1001}) code, body := do(t, app, http.MethodPost, "/api/v1/miler/login", "", `{"phone":"9876500002"}`) if code != 200 || !strings.Contains(body, `"pin_set":false`) { t.Fatalf("login = %d %s, want the active rider found", code, body) } // Two Active rows: the rider's real account (has a PIN) and a later // PIN-less duplicate. The real one must win, or verify-pin says // "incorrect PIN" and set-pin could claim the duplicate. hash, _ := utils.HashPassword("1234") gdb.Create(&models.AppUser{Authname: "Real", Email: "real@r.test", Contactno: "9876500009", Password: hash, Roleid: 5, Status: "Active", Configid: 1001}) gdb.Create(&models.AppUser{Authname: "Dup", Email: "dup@r.test", Contactno: "9876500009", Password: "", Roleid: 5, Status: "Active", Configid: 1001}) code, body = do(t, app, http.MethodPost, "/api/v1/miler/login", "", `{"phone":"9876500009"}`) if code != 200 || !strings.Contains(body, `"pin_set":true`) { t.Fatalf("login with a PIN-less duplicate = %d %s, want the real account (pin_set true)", code, body) } if code, body := do(t, app, http.MethodPost, "/api/v1/miler/set-pin", "", `{"phone":"9876500009","new_pin":"9999"}`); code != 409 { t.Fatalf("set-pin on a phone whose real account has a PIN = %d %s, want 409", code, body) } // A phone that belongs only to a blocked rider says so. gdb.Create(&models.AppUser{Authname: "Gone", Email: "gone@r.test", Contactno: "9876500003", Password: "", Roleid: 5, Status: "Blocked", Configid: 1001}) code, body = do(t, app, http.MethodPost, "/api/v1/miler/login", "", `{"phone":"9876500003"}`) if code != 403 || !strings.Contains(body, "blocked") { t.Fatalf("blocked login = %d %s, want 403 naming the block", code, body) } } func TestBlockHoldsForASignedInRiderAndUnblockLiftsIt(t *testing.T) { gdb := milerAccountDB(t) app := onboardingApp() staff := consoleToken(t, "ops@doormile.com", 1, 0) if code, body := do(t, app, http.MethodPost, "/api/v1/admin/milers", staff, riderBody("Nagalakshmi", "9876500004", "naga@r.test", "")); code != 201 { t.Fatalf("create = %d %s", code, body) } var u models.AppUser gdb.Where("contactno = ?", "9876500004").First(&u) var p models.MilerProfile gdb.Where("userid = ?", u.Userid).First(&p) // The token she already holds from before the block. rider, err := utils.GenerateToken(u.Userid, u.Email, 5, 0, 1001, jwtSecret) if err != nil { t.Fatal(err) } if code, body := do(t, app, http.MethodPut, fmt.Sprintf("/api/v1/admin/milers/%d/block", p.Milerprofileid), staff, `{}`); code != 200 { t.Fatalf("block = %d %s", code, body) } if code, body := do(t, app, http.MethodPost, "/api/v1/miler/duty/start", rider, `{"lat":0,"lon":0}`); code != 403 || !strings.Contains(body, "blocked") { t.Fatalf("blocked rider start duty = %d %s, want 403", code, body) } if code, _ := do(t, app, http.MethodPut, "/api/v1/miler/availability", rider, `{"status":"Available"}`); code != 403 { t.Fatalf("blocked rider set Available = %d, want 403", code) } gdb.Where("userid = ?", u.Userid).First(&p) if p.Availabilitystatus != constants.MilerBlocked { t.Fatalf("after the rider's attempts status = %q, want still Blocked", p.Availabilitystatus) } // Unblock: Offline, can sign in and start duty again. if code, body := do(t, app, http.MethodPut, fmt.Sprintf("/api/v1/admin/milers/%d/unblock", p.Milerprofileid), staff, `{}`); code != 200 { t.Fatalf("unblock = %d %s", code, body) } gdb.Where("userid = ?", u.Userid).First(&p) gdb.Where("userid = ?", u.Userid).First(&u) if p.Availabilitystatus != constants.MilerOffline || u.Status != "Active" { t.Fatalf("after unblock: profile %q, account %q", p.Availabilitystatus, u.Status) } if code, body := do(t, app, http.MethodPost, "/api/v1/miler/duty/start", rider, `{"lat":0,"lon":0}`); code != 200 { t.Fatalf("start duty after unblock = %d %s", code, body) } if code, _ := do(t, app, http.MethodPut, fmt.Sprintf("/api/v1/admin/milers/%d/unblock", p.Milerprofileid), staff, `{}`); code != 400 { t.Fatalf("unblocking a rider who is not blocked = %d, want 400", code) } // A rider cannot block themselves either. if code, _ := do(t, app, http.MethodPut, "/api/v1/miler/availability", rider, `{"status":"Blocked"}`); code != 400 { t.Fatalf("rider setting Blocked = %d, want 400", code) } } func TestEditingAMilerCanFixThePhoneAndKeepsTheHubInTheirCity(t *testing.T) { gdb := milerAccountDB(t) app := onboardingApp() staff := consoleToken(t, "ops@doormile.com", 1, 0) for i, phone := range []string{"9876500005", "9876500006"} { if code, body := do(t, app, http.MethodPost, "/api/v1/admin/milers", staff, riderBody(fmt.Sprintf("R%d", i), phone, fmt.Sprintf("r%d@r.test", i), "")); code != 201 { t.Fatalf("create = %d %s", code, body) } } var p models.MilerProfile gdb.Where("phone = ?", "9876500005").First(&p) url := fmt.Sprintf("/api/v1/admin/milers/%d", p.Milerprofileid) if code, _ := do(t, app, http.MethodPut, url, staff, `{"contactno":"9876500006"}`); code != 409 { t.Fatalf("editing to another rider's phone = %d, want 409", code) } if code, _ := do(t, app, http.MethodPut, url, staff, `{"hubid":21}`); code != 400 { t.Fatalf("editing to a hub in another city = %d, want 400", code) } // An older rider whose CURRENT hub is in another city can still be edited // (the form sends the unchanged hub back on every save). gdb.Model(&models.MilerProfile{}).Where("milerprofileid = ?", p.Milerprofileid).Update("hubid", 21) if code, body := do(t, app, http.MethodPut, url, staff, `{"displayname":"Renamed","hubid":21}`); code != 200 { t.Fatalf("editing a legacy rider with an unchanged out-of-city hub = %d %s, want 200", code, body) } if code, body := do(t, app, http.MethodPut, url, staff, `{"contactno":"+91 98765 00007","hubid":11}`); code != 200 { t.Fatalf("edit = %d %s", code, body) } var u models.AppUser gdb.Where("userid = ?", p.Userid).First(&u) gdb.Where("userid = ?", p.Userid).First(&p) if u.Contactno != "9876500007" || p.Phone != "9876500007" || u.Hubid == nil || *u.Hubid != 11 { t.Fatalf("after edit: account %q hub %v, profile %q", u.Contactno, u.Hubid, p.Phone) } // The rider signs in with the corrected number. if code, body := do(t, app, http.MethodPost, "/api/v1/miler/login", "", `{"phone":"9876500007"}`); code != 200 { t.Fatalf("login with the new phone = %d %s", code, body) } }