package routes_test import ( "encoding/json" "fmt" "net/http" "os" "strings" "testing" "doormile/db" "doormile/internal/testpg" "doormile/models" "doormile/utils" "gorm.io/gorm" ) // Client onboarding with an address, end to end through the real router and // handlers against a real Postgres. Skipped unless REGISTRY_TEST_DSN is set; // the DSN must be a THROWAWAY database — these tables are dropped and // recreated in their own schema. func onboardingDB(t *testing.T) *gorm.DB { t.Helper() dsn := os.Getenv("REGISTRY_TEST_DSN") if dsn == "" { t.Skip("REGISTRY_TEST_DSN not set; skipping Postgres onboarding test") } gdb := testpg.Open(t, dsn, "client_onboarding_routes_test") all := []any{&models.Tenant{}, &models.DoormileAuth{}, &models.AppUser{}, &models.TenantLocation{}, &models.AppLocation{}} if err := gdb.Migrator().DropTable(all...); err != nil { t.Fatal(err) } if err := gdb.AutoMigrate(all...); err != nil { t.Fatal(err) } // Put the previous handle back, nil included: other tests in this package // rely on there being no database (the gate tests expect the handler to // fail without one, not to query a closed test schema). prev := db.DB db.DB = gdb t.Cleanup(func() { db.DB = prev }) // The owner must exist as Doormile staff (onboardingOwnerStillValid). hash, _ := utils.HashPassword("owner-pass-123") if err := gdb.Create(&models.DoormileAuth{Email: onboardingOwner, PasswordHash: hash, Role: "admin"}).Error; err != nil { t.Fatal(err) } if err := gdb.Create(&models.AppLocation{Applocationid: 1, Applocationname: "Coimbatore", Status: "Active"}).Error; err != nil { t.Fatal(err) } return gdb } func onboardBody(extra string) string { return `{"companyname":"Peelamedu Provisions","contactname":"Priya Raman","email":"ops@peelamedu.test", "phone":"9876543210","password":"Strong-pass-1","applocationid":1` + extra + `}` } const goodAddress = `,"address":"14 DB Road, RS Puram, Coimbatore","city":"Coimbatore","state":"Tamil Nadu", "pincode":"641002","latitude":11.0090,"longitude":76.9500` func TestOnboardingRequiresAnAddressWithAMapLocation(t *testing.T) { gdb := onboardingDB(t) app := onboardingApp() tok := consoleToken(t, onboardingOwner, 1, 0) cases := []struct{ name, extra, want string }{ {"no address", "", "enter the client's address"}, {"bad pincode", `,"address":"14 DB Road, RS Puram","pincode":"64100","latitude":11.0,"longitude":76.9`, "6-digit pincode"}, {"typed, not picked", `,"address":"14 DB Road, RS Puram","pincode":"641002"`, "pick the address from the suggestions"}, } for _, c := range cases { code, body := do(t, app, http.MethodPost, "/api/v1/admin/clients/onboard", tok, onboardBody(c.extra)) if code != 400 || !strings.Contains(body, c.want) { t.Errorf("%s: %d %s, want 400 containing %q", c.name, code, body, c.want) } } var n int64 gdb.Model(&models.Tenant{}).Count(&n) if n != 0 { t.Fatal("a refused onboarding must create nothing") } } func TestOnboardingSavesTheAddressAsThePrimaryLocation(t *testing.T) { gdb := onboardingDB(t) app := onboardingApp() tok := consoleToken(t, onboardingOwner, 1, 0) code, body := do(t, app, http.MethodPost, "/api/v1/admin/clients/onboard", tok, onboardBody(goodAddress)) if code != 201 { t.Fatalf("onboard = %d %s", code, body) } var loc models.TenantLocation if err := gdb.First(&loc).Error; err != nil { t.Fatalf("no location created: %v", err) } if !loc.Isprimary || loc.Address != "14 DB Road, RS Puram, Coimbatore" || loc.Pincode != "641002" || loc.City != "Coimbatore" || loc.Latitude != 11.009 || loc.Locationname != "Peelamedu Provisions" { t.Fatalf("location saved as %+v", loc) } // The list shows it. code, body = do(t, app, http.MethodGet, "/api/v1/admin/clients/onboarded", tok, "") if code != 200 || !strings.Contains(body, `"pincode":"641002"`) || !strings.Contains(body, `"address":"14 DB Road, RS Puram, Coimbatore"`) { t.Fatalf("list = %d %s", code, body) } // The client signs in; the login now carries their city for the zone list. code, body = do(t, app, http.MethodPost, "/api/v1/admin/login", "", `{"email":"ops@peelamedu.test","password":"Strong-pass-1"}`) if code != 200 { t.Fatalf("client login = %d %s", code, body) } var login struct { User struct { Applocationid int `json:"applocationid"` Tenantid *int `json:"tenantid"` } `json:"user"` } if err := json.Unmarshal([]byte(body), &login); err != nil || login.User.Applocationid != 1 || login.User.Tenantid == nil { t.Fatalf("login user = %+v (%v)", login.User, err) } } func TestEditingTheAddressUpdatesOrCreatesTheMainLocation(t *testing.T) { gdb := onboardingDB(t) app := onboardingApp() tok := consoleToken(t, onboardingOwner, 1, 0) if code, body := do(t, app, http.MethodPost, "/api/v1/admin/clients/onboard", tok, onboardBody(goodAddress)); code != 201 { t.Fatalf("onboard = %d %s", code, body) } var auth models.DoormileAuth if err := gdb.Where("email = ?", "ops@peelamedu.test").First(&auth).Error; err != nil { t.Fatal(err) } move := `{"location":{"address":"5 Avinashi Road, Peelamedu","city":"Coimbatore","state":"Tamil Nadu", "pincode":"641004","latitude":11.0300,"longitude":76.9900}}` if code, body := do(t, app, http.MethodPut, fmt.Sprintf("/api/v1/admin/clients/%d", auth.ID), tok, move); code != 200 { t.Fatalf("edit = %d %s", code, body) } var locs []models.TenantLocation gdb.Find(&locs) if len(locs) != 1 || locs[0].Address != "5 Avinashi Road, Peelamedu" || locs[0].Pincode != "641004" || !locs[0].Isprimary { t.Fatalf("after edit: %+v", locs) } // An edit with a bad address is refused and changes nothing. bad := `{"location":{"address":"x","pincode":"641004","latitude":11.03,"longitude":76.99}}` if code, _ := do(t, app, http.MethodPut, fmt.Sprintf("/api/v1/admin/clients/%d", auth.ID), tok, bad); code != 400 { t.Fatalf("bad address edit = %d, want 400", code) } // A client onboarded before addresses existed: the edit creates the location. hash, _ := utils.HashPassword("Old-pass-123") old := models.Tenant{Tenantname: "Old Client", Primaryemail: "old@client.test", Primarycontact: "9876500000", Status: "Active"} gdb.Create(&old) tid := old.Tenantid oldAuth := models.DoormileAuth{Email: "old@client.test", PasswordHash: hash, Role: "manager", Tenantid: &tid} gdb.Create(&oldAuth) if code, body := do(t, app, http.MethodPut, fmt.Sprintf("/api/v1/admin/clients/%d", oldAuth.ID), tok, move); code != 200 { t.Fatalf("edit old client = %d %s", code, body) } var created models.TenantLocation if err := gdb.Where("tenantid = ?", tid).First(&created).Error; err != nil || !created.Isprimary || created.Locationname != "Old Client" { t.Fatalf("old client location = %+v (%v)", created, err) } }