package console import ( "context" "errors" "net/http" "net/url" "strings" "testing" "time" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) func TestAccountReviewExcludesLegitimateSameNamedApp(t *testing.T) { fx := newConsoleFixture(t) const otherID = 4242 if err := fx.st.CreateAccount(otherID, "cdef", "Other", "paul@keymail.dev", time.Now()); err != nil { t.Fatal(err) } if err := fx.st.AddMember(otherID, "paul@keymail.dev", RoleOwner, time.Now()); err != nil { t.Fatal(err) } if _, err := fx.st.CreateAppIn(otherID, "kass", "paul@keymail.dev", time.Now()); err != nil { t.Fatal(err) } fx.addRoute(t, registry.Route{Host: "other.example", App: "kass", Account: "cdef", Kind: "instance"}) viewed, err := fx.st.AccountBySqid("bab") if err != nil || viewed == nil { t.Fatal(err) } review, all, err := fx.srv.appAccountReview("paul@keymail.dev", viewed, "kass") if err != nil { t.Fatal(err) } if review.Count != 0 || all { t.Fatalf("legitimate sibling became a repair candidate: %+v all=%v", review, all) } } func TestAccountReviewSiblingPreventsUnrelatedMove(t *testing.T) { fx := newConsoleFixture(t) now := time.Now() for _, tc := range []struct { id int64 sqid string name string app bool }{{4242, "cdef", "Sibling", true}, {4343, "cghi", "Candidate", false}} { if err := fx.st.CreateAccount(tc.id, tc.sqid, tc.name, "paul@keymail.dev", now); err != nil { t.Fatal(err) } if err := fx.st.AddMember(tc.id, "paul@keymail.dev", RoleOwner, now); err != nil { t.Fatal(err) } if tc.app { if _, err := fx.st.CreateAppIn(tc.id, "kass", "paul@keymail.dev", now); err != nil { t.Fatal(err) } } } fx.addRoute(t, registry.Route{Host: "sibling.example", App: "kass", Account: "cdef", Kind: "instance"}) fx.addRoute(t, registry.Route{Host: "candidate.example", App: "kass", Account: "cghi", Kind: "instance"}) viewed, _ := fx.st.AccountBySqid("bab") review, all, err := fx.srv.appAccountReview("paul@keymail.dev", viewed, "kass") if err != nil { t.Fatal(err) } if review.Count != 1 || all { t.Fatalf("review = %+v all=%v; sibling must prevent move authority", review, all) } if _, agreed, err := fx.srv.routeAccountFor("paul@keymail.dev", viewed, "kass"); !errors.Is(err, errMoveTargetUnavailable) || agreed { t.Fatalf("routeAccountFor agreed=%v err=%v; same-named sibling must hide the move target", agreed, err) } } func TestAccountReviewHidesInaccessibleSource(t *testing.T) { fx := newConsoleFixture(t) fx.addRoute(t, registry.Route{Host: "secret.example", App: "kass", Account: "unknown", Kind: "instance"}) viewed, _ := fx.st.AccountBySqid("bab") review, all, err := fx.srv.appAccountReview("paul@keymail.dev", viewed, "kass") if err != nil { t.Fatal(err) } if review.Count != 0 || all { t.Fatalf("inaccessible source disclosed: %+v all=%v", review, all) } if _, agreed, err := fx.srv.routeAccountFor("paul@keymail.dev", viewed, "kass"); !errors.Is(err, errMoveTargetUnavailable) || agreed { t.Fatalf("routeAccountFor agreed=%v err=%v; an inaccessible source must hide the move target", agreed, err) } } func TestAccountReviewTreatsTrashedSameNamedAppAsSibling(t *testing.T) { fx := newConsoleFixture(t) const otherID = 4242 if err := fx.st.CreateAccount(otherID, "cdef", "Other", "paul@keymail.dev", time.Now()); err != nil { t.Fatal(err) } if _, err := fx.st.CreateAppIn(otherID, "kass", "paul@keymail.dev", time.Now()); err != nil { t.Fatal(err) } if err := fx.st.TrashAppRow(otherID, "kass", time.Now()); err != nil { t.Fatal(err) } fx.addRoute(t, registry.Route{Host: "trashed-sibling.example", App: "kass", Account: "cdef", Kind: "instance"}) viewed, _ := fx.st.AccountBySqid("bab") review, all, err := fx.srv.appAccountReview("paul@keymail.dev", viewed, "kass") if err != nil { t.Fatal(err) } if review.Count != 0 || all { t.Fatalf("trashed sibling became a move candidate: %+v all=%v", review, all) } if _, agreed, err := fx.srv.routeAccountFor("paul@keymail.dev", viewed, "kass"); !errors.Is(err, errMoveTargetUnavailable) || agreed { t.Fatalf("routeAccountFor agreed=%v err=%v; trashed sibling must hide the move target", agreed, err) } } func TestAccountReviewPageShowsOwnedCandidatesAndHidesFromNonMembers(t *testing.T) { fx := newConsoleFixture(t) const otherID = 4242 if err := fx.st.CreateAccount(otherID, "cdef", "Other", "paul@keymail.dev", time.Now()); err != nil { t.Fatal(err) } if err := fx.st.AddMember(otherID, "paul@keymail.dev", RoleOwner, time.Now()); err != nil { t.Fatal(err) } fx.addRoute(t, registry.Route{Host: "candidate.example", App: "kass", Account: "cdef", Kind: "instance"}) c := signInPaul(t, fx) status, _, body := get(t, c, fx.u("/apps/kass/account-review")) if status != http.StatusOK || !strings.Contains(body, "candidate.example") || !strings.Contains(body, "Other") || !strings.Contains(body, approvedMismatchCopy.Partial) { t.Fatalf("account review = %d, missing candidate/account/partial copy:\n%s", status, body) } reg, err := registry.Open(fx.regPath) if err != nil { t.Fatal(err) } routes, err := reg.List() if err != nil { t.Fatal(err) } for _, route := range routes { if route.App == "kass" && !route.Platform { if err := reg.SetAccount(route.Host, "cdef"); err != nil { t.Fatal(err) } } } reg.Close() status, _, body = get(t, c, fx.u("/apps/kass/account-review")) if status != http.StatusOK || !strings.Contains(body, approvedMismatchCopy.Repairable) || strings.Contains(body, approvedMismatchCopy.Partial) { t.Fatalf("fully mismatched review = %d, missing repairable branch:\n%s", status, body) } fx.setUser("eve@keymail.dev") stranger := newBrowser(t) signUp(t, fx, stranger, "code-account-review-stranger") strangerCSRF := sessionToken(t, fx, stranger) strangerStatus, _, strangerBody := get(t, stranger, fx.u("/apps/kass/account-review")) ghostStatus, _, ghostBody := get(t, stranger, fx.ts.URL+"/definitely-not-a-page") if strangerStatus != http.StatusNotFound || strangerStatus != ghostStatus || strangerBody != ghostBody { t.Fatalf("non-member review = %d %q; unrouted = %d %q", strangerStatus, strangerBody, ghostStatus, ghostBody) } moveStatus, _, moveBody := postForm(t, stranger, fx.u("/apps/kass/move"), url.Values{"csrf": {strangerCSRF}}) if moveStatus != http.StatusNotFound || moveBody != ghostBody { t.Fatalf("non-member move = %d %q; unrouted = %d %q", moveStatus, moveBody, ghostStatus, ghostBody) } } func TestMoveRefusesHiddenOrSiblingSources(t *testing.T) { for _, tc := range []struct { name, account string sibling bool }{ {name: "hidden", account: "unknown"}, {name: "sibling", account: "cdef", sibling: true}, } { t.Run(tc.name, func(t *testing.T) { fx := newConsoleFixture(t) if tc.sibling { const otherID = 4242 if err := fx.st.CreateAccount(otherID, tc.account, "Other", "paul@keymail.dev", time.Now()); err != nil { t.Fatal(err) } if _, err := fx.st.CreateAppIn(otherID, "kass", "paul@keymail.dev", time.Now()); err != nil { t.Fatal(err) } } fx.addRoute(t, registry.Route{Host: tc.name + ".example", App: "kass", Account: tc.account, Kind: "instance"}) c := signInPaul(t, fx) csrf := sessionToken(t, fx, c) status, _, body := postForm(t, c, fx.u("/apps/kass/move"), url.Values{"csrf": {csrf}}) ghostStatus, _, ghostBody := get(t, c, fx.ts.URL+"/definitely-not-a-page") if status != http.StatusNotFound || status != ghostStatus || body != ghostBody { t.Fatalf("unavailable move = %d %q; unrouted = %d %q", status, body, ghostStatus, ghostBody) } accounts, err := fx.st.AppAccounts("kass") if err != nil || len(accounts) == 0 || accounts[0] != OpsAccountID { t.Fatalf("AppAccounts after refusal = %v, %v", accounts, err) } }) } } func TestPublishedAppWithoutOwnedInstancesWarnsFromOwnedStateOnly(t *testing.T) { fx := newConsoleFixture(t) if _, err := fx.st.CreateAppIn(OpsAccountID, "empty", "paul@keymail.dev", time.Now()); err != nil { t.Fatal(err) } c := signInPaul(t, fx) _, _, empty := get(t, c, fx.u("/apps/empty")) if strings.Contains(empty, `