internal/control/accountdelete_test.go

v1.43.1
gitbay/internal/control/accountdelete_test.go history · blame · raw

203 lines · 7544 bytes

  1package control
  2
  3import (
  4	"bytes"
  5	"os"
  6	"path/filepath"
  7	"strings"
  8	"testing"
  9	"time"
 10
 11	"gitbay.org/gitbay/internal/config"
 12	"gitbay.org/gitbay/internal/protocol"
 13	"gitbay.org/gitbay/internal/store"
 14)
 15
 16// runAs dispatches argv as u and returns the exit code and stderr.
 17func runAs(st *store.Store, u store.User, root string, argv ...string) (int, string) {
 18	out, errOut := &bytes.Buffer{}, &bytes.Buffer{}
 19	c := &Ctx{User: u, Scope: "full", Store: st, Stdout: out, Stderr: errOut, Stdin: strings.NewReader("")}
 20	c.Cfg.Server.Root = root
 21	c.Cfg.Server.SiteURL = "https://forge.test/"
 22	c.Cfg.Limits.WriteRate = -1
 23	return Dispatch(c, argv), errOut.String()
 24}
 25
 26func deleteFixture(t *testing.T) (*store.Store, string, store.User, store.User) {
 27	t.Helper()
 28	st, err := store.Open(filepath.Join(t.TempDir(), "gitbay.db"))
 29	if err != nil {
 30		t.Fatal(err)
 31	}
 32	t.Cleanup(func() { st.Close() })
 33	if err := st.MigrateUp(); err != nil {
 34		t.Fatal(err)
 35	}
 36	root := t.TempDir()
 37	user := func(name string) store.User {
 38		id, err := st.CreateUser(name, false)
 39		if err != nil {
 40			t.Fatal(err)
 41		}
 42		u, _ := st.UserByID(id)
 43		return u
 44	}
 45	return st, root, user("alice"), user("bob")
 46}
 47
 48// account delete refuses a mistyped name, an account with no verified
 49// address, and the only admin of an org; otherwise it mails a link and
 50// changes nothing else.
 51func TestAccountDeleteRequest(t *testing.T) {
 52	st, root, _, bob := deleteFixture(t)
 53	if code, errOut := runAs(st, bob, root, "account", "delete", "--confirm", "bob"); code != protocol.ExitDenied || !strings.Contains(errOut, "verify an address") {
 54		t.Fatalf("no address: exit %d %q", code, errOut)
 55	}
 56	if err := st.AddEmail(bob.ID, "bob@example.test", "smtp", true); err != nil {
 57		t.Fatal(err)
 58	}
 59	if code, _ := runAs(st, bob, root, "account", "delete", "--confirm", "bobb"); code != protocol.ExitUsage {
 60		t.Fatalf("mistyped name: exit %d", code)
 61	}
 62	// The only instance admin is refused.
 63	soleAdmin := bob
 64	soleAdmin.IsAdmin = true
 65	st.DB.Exec("UPDATE users SET is_admin = 1 WHERE id = ?", bob.ID)
 66	if code, errOut := runAs(st, soleAdmin, root, "account", "delete", "--confirm", "bob"); code != protocol.ExitDenied || !strings.Contains(errOut, "only admin") {
 67		t.Fatalf("sole instance admin: exit %d %q", code, errOut)
 68	}
 69	st.DB.Exec("UPDATE users SET is_admin = 0 WHERE id = ?", bob.ID)
 70	if code, _ := runAs(st, bob, root, "org", "create", "acme"); code != 0 {
 71		t.Fatal("org create")
 72	}
 73	if code, errOut := runAs(st, bob, root, "account", "delete", "--confirm", "bob"); code != protocol.ExitDenied || !strings.Contains(errOut, "only admin of acme") {
 74		t.Fatalf("sole org admin: exit %d %q", code, errOut)
 75	}
 76	if _, err := st.DB.Exec("DELETE FROM orgs WHERE name = 'acme'"); err != nil {
 77		t.Fatal(err)
 78	}
 79	if code, errOut := runAs(st, bob, root, "account", "delete", "--confirm", "bob"); code != 0 {
 80		t.Fatalf("request: exit %d %q", code, errOut)
 81	}
 82	var body string
 83	if err := st.DB.QueryRow("SELECT body FROM notifications WHERE recipient = 'bob@example.test'").Scan(&body); err != nil ||
 84		!strings.Contains(body, "https://forge.test/settings/delete?token=") {
 85		t.Fatalf("mail: %v %q", err, body)
 86	}
 87	if u, _ := st.UserByID(bob.ID); u.Disabled || u.DeleteAfter != "" {
 88		t.Fatal("a request alone changed the account")
 89	}
 90	if code, _ := runAs(st, bob, root, "account", "delete", "--cancel"); code != 0 {
 91		t.Fatal("cancel")
 92	}
 93	if code, _ := runAs(st, bob, root, "account", "delete", "--cancel"); code != protocol.ExitNotFound {
 94		t.Fatalf("second cancel: exit %d", code)
 95	}
 96}
 97
 98// A confirmed deletion disables the account; the purge removes its
 99// repositories and the account, and moves what it wrote elsewhere to the
100// ghost. Cancelling restores the account, and an admin decision clears
101// the schedule.
102func TestPurgeDueAccounts(t *testing.T) {
103	st, root, alice, bob := deleteFixture(t)
104	runAs(st, alice, root, "repo", "create", "alice/app")
105	if code, errOut := runAs(st, bob, root, "repo", "create", "bob/own"); code != 0 {
106		t.Fatalf("bob repo: %s", errOut)
107	}
108	app, _ := st.RepoByPath("alice/app")
109	issue, err := st.CreateIssue(app.ID, bob.ID, "from bob", "", "md")
110	if err != nil {
111		t.Fatal(err)
112	}
113
114	schedule := func(u store.User, after time.Time) store.User {
115		t.Helper()
116		_, hash, _ := store.NewToken()
117		if err := st.RequestAccountDeletion(u.ID, hash, time.Hour); err != nil {
118			t.Fatal(err)
119		}
120		s, err := st.ConfirmAccountDeletion(hash, after)
121		if err != nil {
122			t.Fatal(err)
123		}
124		return s
125	}
126
127	// Cancelling (what signing in does) restores the account.
128	s := schedule(bob, time.Now().Add(time.Hour))
129	if code, errOut := runAs(st, s, root, "whoami"); code != protocol.ExitDenied || !strings.Contains(errOut, "scheduled for deletion") {
130		t.Fatalf("scheduled account: exit %d %q", code, errOut)
131	}
132	if !CancelScheduledDeletion(st, &s, "test") || s.Disabled {
133		t.Fatal("cancel did not restore the account")
134	}
135	// A link opened after an admin suspended the account schedules
136	// nothing.
137	_, hash, _ := store.NewToken()
138	st.RequestAccountDeletion(bob.ID, hash, time.Hour)
139	st.DB.Exec("UPDATE users SET disabled = 1 WHERE id = ?", bob.ID)
140	if _, err := st.ConfirmAccountDeletion(hash, time.Now()); err == nil {
141		t.Fatal("a suspended account was scheduled")
142	}
143	st.DB.Exec("UPDATE users SET disabled = 0 WHERE id = ?", bob.ID)
144	// Once the purge has claimed an account, signing in cannot cancel.
145	s = schedule(bob, time.Now().Add(-time.Minute))
146	if ok, err := st.ClaimDeletion(bob.ID, time.Now()); !ok || err != nil {
147		t.Fatalf("claim: %v %v", ok, err)
148	}
149	s, _ = st.UserByID(bob.ID)
150	if CancelScheduledDeletion(st, &s, "test") {
151		t.Fatal("cancelled a purge in progress")
152	}
153	st.DB.Exec("UPDATE users SET disabled = 0, delete_after = NULL WHERE id = ?", bob.ID)
154	// An admin disabling a scheduled account keeps it, disabled.
155	schedule(bob, time.Now().Add(-time.Minute))
156	if err := st.SetUserDisabled(bob.ID, true); err != nil {
157		t.Fatal(err)
158	}
159	if due, _ := st.DueDeletions(time.Now()); len(due) != 0 {
160		t.Fatal("an admin's disable left the purge scheduled")
161	}
162	st.SetUserDisabled(bob.ID, false)
163
164	schedule(bob, time.Now().Add(-time.Minute))
165	cfg := config.Default()
166	cfg.Server.Root = root
167	purged, err := PurgeDueAccounts(cfg, st, time.Now())
168	if err != nil || len(purged) != 1 || purged[0] != "bob" {
169		t.Fatalf("purge: %v %v", purged, err)
170	}
171	if _, err := st.UserByID(bob.ID); err == nil {
172		t.Fatal("bob still exists")
173	}
174	if _, err := os.Stat(RepoDir(root, "bob", "own")); !os.IsNotExist(err) {
175		t.Fatalf("bob/own directory: %v", err)
176	}
177	ghost, err := st.UserByUsername("ghost")
178	if err != nil || !ghost.Ghost || !ghost.Disabled {
179		t.Fatalf("ghost: %+v %v", ghost, err)
180	}
181	var author int64
182	st.DB.QueryRow("SELECT author_id FROM issues WHERE id = ?", issue).Scan(&author)
183	if author != ghost.ID {
184		t.Fatalf("issue author %d, want ghost %d", author, ghost.ID)
185	}
186	admin := alice
187	admin.IsAdmin = true
188	if code, _ := runAs(st, admin, root, "admin", "user", "enable", "ghost"); code != protocol.ExitDenied {
189		t.Fatalf("admin enable ghost: exit %d", code)
190	}
191
192	// The only admin of an org is skipped and stays scheduled.
193	carolID, _ := st.CreateUser("carol", false)
194	carol, _ := st.UserByID(carolID)
195	runAs(st, carol, root, "org", "create", "solo")
196	schedule(carol, time.Now().Add(-time.Minute))
197	if purged, err := PurgeDueAccounts(cfg, st, time.Now()); err != nil || len(purged) != 0 {
198		t.Fatalf("sole admin purged: %v %v", purged, err)
199	}
200	if u, err := st.UserByID(carolID); err != nil || u.DeleteAfter == "" {
201		t.Fatalf("carol: %+v %v", u, err)
202	}
203}