package onboarding import ( "net/http" "net/http/httptest" "strings" "testing" "github.com/google/uuid" ) func newOperatorLoginFixture(t *testing.T) *loginFixture { f := newLoginFixture(t) f.s = f.s.operatorView() f.newLoginSession() f.mux.Handle("POST /operator-write", f.s.CustomerAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(204) }))) return f } func operatorPasswordSession(t *testing.T, f *loginFixture, password string) (*http.Cookie, string) { t.Helper() if f.trusted == nil || f.trusted.Name != f.s.trustedLoginCookieName() { f.trustDeviceForTest() } in := PasswordLoginInput{Phone: f.phone, Password: password, CaptchaID: f.knownCaptcha(), CaptchaCode: "654321"} w, data := f.request("POST", "/api/v1/web/login/password", in, f.cookie, f.csrf, "") if w.Code != 200 { t.Fatal(w.Body.String()) } for _, c := range w.Result().Cookies() { if c.Name == f.s.cookieName(true) { return c, data["csrf_token"].(string) } } t.Fatal("missing operator cookie") return nil, "" } func TestOperatorPhoneLoginKeepsRolesAndIsolatesCustomerSession(t *testing.T) { f := newOperatorLoginFixture(t) _, err := f.s.ProvisionOperatorPhone(f.ctx, f.operatorID, f.p.Issuer, f.p.Subject, f.phone, "OperatorOnly123!", uuid.NewString()) must(t, err) account, csrf := operatorPasswordSession(t, f, "OperatorOnly123!") if account.Name != "__Host-platform-operator" || !account.Secure || !account.HttpOnly || account.SameSite != http.SameSiteLaxMode { t.Fatal("operator cookie protections") } p, err := f.s.AccountPrincipal(f.ctx, account.Value, csrf, true) must(t, err) if p.Subject != f.p.Subject { t.Fatal("wrong operator identity") } w, _ := f.request("POST", "/operator-write", struct{}{}, account, "wrong", "") if w.Code != 403 { t.Fatal("CSRF bypass") } w, _ = f.request("POST", "/operator-write", struct{}{}, account, csrf, "") if w.Code != 204 { t.Fatal(w.Body.String()) } r := httptest.NewRequest("POST", "https://platform.example/operator-write", strings.NewReader("{}")) r.Header.Set("Origin", "https://other.example") r.Header.Set("X-CSRF-Token", csrf) r.AddCookie(account) w2 := httptest.NewRecorder() f.mux.ServeHTTP(w2, r) if w2.Code != 403 { t.Fatal("cross-origin operator write") } _, state := f.request("GET", "/api/v1/web/account/password", nil, account, "", "") if state["password_configured"] != true || state["sms_verified_recently"] != false { t.Fatal("password login granted SMS proof") } // A phone can be registered in both portals without linking their identities. customer := *f.s customer.operatorLogin = false _, err = customer.ProvisionCustomerPhone(f.ctx, f.web.UserID, f.user.Issuer, f.user.Subject, f.phone, "CustomerOnly123!", uuid.NewString()) must(t, err) if _, err = customer.AccountPrincipal(f.ctx, account.Value, csrf, true); err == nil { t.Fatal("operator token crossed into customer table") } cf := *f cf.fixture = &fixture{} *cf.fixture = *f.fixture cf.s = &customer cf.newLoginSession() cc, ccsrf := operatorPasswordSession(t, &cf, "CustomerOnly123!") if _, err = f.s.AccountPrincipal(f.ctx, cc.Value, ccsrf, true); err == nil { t.Fatal("customer token granted operator access") } f.newLoginSession() second, _ := operatorPasswordSession(t, f, "OperatorOnly123!") w, _ = f.request("POST", "/api/v1/web/account/password", SetPasswordInput{Password: "ChangedOperator123!", CurrentPassword: "OperatorOnly123!"}, account, csrf, "") if w.Code != 200 { t.Fatal(w.Body.String()) } if _, err = f.s.AccountPrincipal(f.ctx, second.Value, "", false); err == nil { t.Fatal("password change kept another operator session") } if _, err = customer.AccountPrincipal(f.ctx, cc.Value, "", false); err != nil { t.Fatal("operator password change revoked customer", err) } w, _ = f.request("POST", "/api/v1/web/auth/logout", struct{}{}, account, csrf, "") if w.Code != 200 { t.Fatal(w.Body.String()) } if _, err = f.s.AccountPrincipal(f.ctx, account.Value, "", false); err == nil { t.Fatal("operator logout not revoked") } if _, err = customer.AccountPrincipal(f.ctx, cc.Value, "", false); err != nil { t.Fatal("operator logout revoked customer", err) } } func TestOperatorSMSOnlyAllowsProvisionedActiveOperators(t *testing.T) { f := newOperatorLoginFixture(t) if _, err := f.s.ProvisionOperatorPhone(f.ctx, f.web.UserID, f.user.Issuer, f.user.Subject, f.phone, "OperatorOnly123!", uuid.NewString()); err == nil { t.Fatal("customer promoted by provisioning") } id := f.send() w, _ := f.request("POST", "/api/v1/web/login/sms/verify", SMSVerifyInput{ChallengeID: id, Code: f.sender.code}, f.cookie, f.csrf, "") if w.Code != 403 { t.Fatal("unregistered operator accepted", w.Body.String()) } var count int must(t, f.s.pool.QueryRow(f.ctx, `SELECT count(*) FROM operator_phone_credential WHERE phone_digest=$1`, domainMAC(f.s.crypt.hashKey, "PHONE1\n"+f.phone)).Scan(&count)) if count != 0 { t.Fatal("SMS registered operator") } _, err := f.s.ProvisionOperatorPhone(f.ctx, f.operatorID, f.p.Issuer, f.p.Subject, f.phone, "OperatorOnly123!", uuid.NewString()) must(t, err) w, data := f.request("POST", "/api/v1/web/login/sms/verify", SMSVerifyInput{ChallengeID: id, Code: f.sender.code}, f.cookie, f.csrf, "") if w.Code != 200 { t.Fatal(w.Body.String()) } var account *http.Cookie for _, c := range w.Result().Cookies() { if c.Name == f.s.cookieName(true) { account = c } } if account == nil { t.Fatal("missing operator SMS session") } w, _ = f.request("POST", "/api/v1/web/account/password", SetPasswordInput{Password: "ResetOperator123!"}, account, data["csrf_token"].(string), "") if w.Code != 200 { t.Fatal("SMS reset failed", w.Body.String()) } _, err = f.s.pool.Exec(f.ctx, `UPDATE user_account SET status='disabled' WHERE id=$1::uuid`, f.operatorID) must(t, err) t.Cleanup(func() { _, _ = f.s.pool.Exec(f.ctx, `UPDATE user_account SET status='active' WHERE id=$1::uuid`, f.operatorID) }) if _, err = f.s.AccountPrincipal(f.ctx, account.Value, "", false); err == nil { t.Fatal("disabled operator session accepted") } }