internal/control/accountdelete_test.go
203 lines · 7544 bytes
4 symbols in this file
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}