rin
Публичный Open-source lightweight git hosting.
account_admin_test.go · 248 строк · 9.3 KB
Raw
1 package handlers
2
3 import (
4 "context"
5 "errors"
6 "io"
7 "net/http"
8 "net/http/httptest"
9 "net/url"
10 "os"
11 "path/filepath"
12 "strconv"
13 "strings"
14 "testing"
15
16 "polar/internal/auth"
17 "polar/internal/db"
18 "polar/internal/repo"
19 "polar/internal/user"
20 "polar/internal/web/middleware"
21 )
22
23 func authenticatedFormRequest(t *testing.T, method, path string, form url.Values, sess *auth.Session) *http.Request {
24 t.Helper()
25 req := httptest.NewRequest(method, path, strings.NewReader(form.Encode()))
26 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
27 req.AddCookie(&http.Cookie{Name: middleware.SessionCookieName, Value: sess.ID})
28 return req
29 }
30
31 func TestRegistrationModesMediaPersistence(t *testing.T) {
32 app, _, userSvc, repoSvc := setupTestApp(t)
33 ctx := context.Background()
34
35 admin, err := userSvc.RegisterWithMode(ctx, "admin1", "admin1@example.com", "password123", db.RegistrationModePublic)
36 if err != nil {
37 t.Fatalf("register first administrator: %v", err)
38 }
39 if !admin.IsAdmin || !admin.IsApproved {
40 t.Fatalf("first account must be an approved administrator: %+v", admin)
41 }
42
43 pending, err := userSvc.RegisterWithMode(ctx, "pending1", "pending1@example.com", "password123", db.RegistrationModeConfirm)
44 if err != nil {
45 t.Fatalf("register pending account: %v", err)
46 }
47 if pending.IsApproved {
48 t.Fatal("confirm-only registration must create a pending account")
49 }
50 if _, err := userSvc.Authenticate(ctx, pending.Username, "password123"); !errors.Is(err, user.ErrPendingApproval) {
51 t.Fatalf("pending account login error = %v, want ErrPendingApproval", err)
52 }
53 if err := userSvc.SetApproved(ctx, pending.ID, true); err != nil {
54 t.Fatalf("approve pending account: %v", err)
55 }
56 if _, err := userSvc.Authenticate(ctx, pending.Username, "password123"); err != nil {
57 t.Fatalf("approved account login failed: %v", err)
58 }
59
60 member, err := userSvc.RegisterWithMode(ctx, "member1", "member1@example.com", "password123", db.RegistrationModePublic)
61 if err != nil {
62 t.Fatalf("register public account: %v", err)
63 }
64 if !member.IsApproved {
65 t.Fatal("public registration must create an approved account")
66 }
67 if _, err := userSvc.RegisterWithMode(ctx, "closed1", "closed1@example.com", "password123", db.RegistrationModeClosed); !errors.Is(err, user.ErrRegistrationDisabled) {
68 t.Fatalf("closed registration error = %v, want ErrRegistrationDisabled", err)
69 }
70
71 background := "data:image/webp;base64,AAAA"
72 avatar := "data:image/webp;base64,BBBB"
73 if err := userSvc.UpdateBackground(ctx, member.ID, background); err != nil {
74 t.Fatalf("update profile background: %v", err)
75 }
76 if err := userSvc.UpdateAvatar(ctx, member.ID, avatar); err != nil {
77 t.Fatalf("update profile avatar: %v", err)
78 }
79 storedMember, err := userSvc.GetByUsername(ctx, member.Username)
80 if err != nil {
81 t.Fatalf("load member: %v", err)
82 }
83 if storedMember.BackgroundURL != background || storedMember.AvatarURL != avatar {
84 t.Fatalf("profile media not persisted: background=%q avatar=%q", storedMember.BackgroundURL, storedMember.AvatarURL)
85 }
86
87 repository, err := repoSvc.Create(ctx, member, "media-repo", "Media persistence", false)
88 if err != nil {
89 t.Fatalf("create repository: %v", err)
90 }
91 repoAvatar := "data:image/webp;base64,CCCC"
92 if err := repoSvc.UpdateAvatar(ctx, repository.ID, repoAvatar); err != nil {
93 t.Fatalf("update repository avatar: %v", err)
94 }
95 storedRepo, err := repoSvc.GetByID(ctx, repository.ID)
96 if err != nil {
97 t.Fatalf("load repository: %v", err)
98 }
99 if storedRepo.AvatarURL != repoAvatar {
100 t.Fatalf("repository avatar = %q, want %q", storedRepo.AvatarURL, repoAvatar)
101 }
102
103 profileResp, err := app.Test(httptest.NewRequest(http.MethodGet, "/member1", nil), 5000)
104 if err != nil || profileResp.StatusCode != http.StatusOK {
105 t.Fatalf("profile response: status=%v err=%v", profileResp.StatusCode, err)
106 }
107 profileBody, _ := io.ReadAll(profileResp.Body)
108 if !strings.Contains(string(profileBody), background) || !strings.Contains(string(profileBody), "profile-hero-bg") {
109 t.Fatal("profile page must render the saved vignette background")
110 }
111
112 repoResp, err := app.Test(httptest.NewRequest(http.MethodGet, "/member1/media-repo", nil), 5000)
113 if err != nil || repoResp.StatusCode != http.StatusOK {
114 t.Fatalf("repository response: status=%v err=%v", repoResp.StatusCode, err)
115 }
116 repoBody, _ := io.ReadAll(repoResp.Body)
117 if !strings.Contains(string(repoBody), repoAvatar) {
118 t.Fatal("repository page must render the custom repository avatar")
119 }
120 }
121
122 func TestAdminCanDeleteRepositoriesAndAccounts(t *testing.T) {
123 app, authSvc, userSvc, repoSvc := setupTestApp(t)
124 ctx := context.Background()
125
126 admin, err := userSvc.RegisterWithMode(ctx, "admin2", "admin2@example.com", "password123", db.RegistrationModePublic)
127 if err != nil {
128 t.Fatalf("register administrator: %v", err)
129 }
130 member, err := userSvc.RegisterWithMode(ctx, "member2", "member2@example.com", "password123", db.RegistrationModePublic)
131 if err != nil {
132 t.Fatalf("register member: %v", err)
133 }
134 repository, err := repoSvc.Create(ctx, member, "delete-me", "Temporary repository", false)
135 if err != nil {
136 t.Fatalf("create repository: %v", err)
137 }
138
139 sess, err := authSvc.CreateSession(ctx, admin.ID)
140 if err != nil {
141 t.Fatalf("create administrator session: %v", err)
142 }
143
144 repoDeletePath := "/admin/repos/" + strconv.FormatInt(repository.ID, 10) + "/delete"
145 req := authenticatedFormRequest(t, http.MethodPost, repoDeletePath, url.Values{
146 "_csrf": {sess.CSRFToken},
147 "confirm_name": {repository.Name},
148 }, sess)
149 resp, err := app.Test(req, 5000)
150 if err != nil {
151 t.Fatalf("admin repository delete request: %v", err)
152 }
153 if resp.StatusCode != http.StatusSeeOther {
154 t.Fatalf("admin repository delete status = %d, want 303", resp.StatusCode)
155 }
156 if _, err := repoSvc.GetByID(ctx, repository.ID); !errors.Is(err, repo.ErrRepoNotFound) {
157 t.Fatalf("deleted repository lookup error = %v, want ErrRepoNotFound", err)
158 }
159
160 remaining, err := repoSvc.Create(ctx, member, "delete-with-account", "Temporary repository", false)
161 if err != nil {
162 t.Fatalf("create remaining repository: %v", err)
163 }
164 diskPath, err := repoSvc.DiskPath(member.Username, remaining.Name)
165 if err != nil {
166 t.Fatalf("resolve repository path: %v", err)
167 }
168 ownerDir := filepath.Dir(diskPath)
169
170 userDeletePath := "/admin/users/" + strconv.FormatInt(member.ID, 10) + "/delete"
171 req = authenticatedFormRequest(t, http.MethodPost, userDeletePath, url.Values{
172 "_csrf": {sess.CSRFToken},
173 "confirm_username": {member.Username},
174 }, sess)
175 resp, err = app.Test(req, 5000)
176 if err != nil {
177 t.Fatalf("admin account delete request: %v", err)
178 }
179 if resp.StatusCode != http.StatusSeeOther {
180 t.Fatalf("admin account delete status = %d, want 303", resp.StatusCode)
181 }
182 if _, err := userSvc.GetByUsername(ctx, member.Username); !errors.Is(err, user.ErrUserNotFound) {
183 t.Fatalf("deleted account lookup error = %v, want ErrUserNotFound", err)
184 }
185 if _, err := repoSvc.GetByID(ctx, remaining.ID); !errors.Is(err, repo.ErrRepoNotFound) {
186 t.Fatalf("cascaded repository lookup error = %v, want ErrRepoNotFound", err)
187 }
188 if _, err := os.Stat(ownerDir); !os.IsNotExist(err) {
189 t.Fatalf("owner repository directory still exists: %v", err)
190 }
191 }
192
193 func TestAccountSelfDeleteAndLastAdminProtection(t *testing.T) {
194 app, authSvc, userSvc, repoSvc := setupTestApp(t)
195 ctx := context.Background()
196
197 admin, err := userSvc.RegisterWithMode(ctx, "admin3", "admin3@example.com", "password123", db.RegistrationModePublic)
198 if err != nil {
199 t.Fatalf("register administrator: %v", err)
200 }
201 member, err := userSvc.RegisterWithMode(ctx, "member3", "member3@example.com", "password123", db.RegistrationModePublic)
202 if err != nil {
203 t.Fatalf("register member: %v", err)
204 }
205 repository, err := repoSvc.Create(ctx, member, "self-delete", "Temporary repository", false)
206 if err != nil {
207 t.Fatalf("create repository: %v", err)
208 }
209
210 sess, err := authSvc.CreateSession(ctx, member.ID)
211 if err != nil {
212 t.Fatalf("create member session: %v", err)
213 }
214 wrongPassword := authenticatedFormRequest(t, http.MethodPost, "/settings/delete", url.Values{
215 "_csrf": {sess.CSRFToken},
216 "confirm_username": {member.Username},
217 "password": {"wrong-password"},
218 }, sess)
219 resp, err := app.Test(wrongPassword, 5000)
220 if err != nil || resp.StatusCode != http.StatusSeeOther {
221 t.Fatalf("wrong-password delete response: status=%v err=%v", resp.StatusCode, err)
222 }
223 if _, err := userSvc.GetByUsername(ctx, member.Username); err != nil {
224 t.Fatalf("account was deleted with a wrong password: %v", err)
225 }
226
227 deleteReq := authenticatedFormRequest(t, http.MethodPost, "/settings/delete", url.Values{
228 "_csrf": {sess.CSRFToken},
229 "confirm_username": {member.Username},
230 "password": {"password123"},
231 }, sess)
232 resp, err = app.Test(deleteReq, 5000)
233 if err != nil {
234 t.Fatalf("self delete request: %v", err)
235 }
236 if resp.StatusCode != http.StatusSeeOther {
237 t.Fatalf("self delete status = %d, want 303", resp.StatusCode)
238 }
239 if _, err := userSvc.GetByUsername(ctx, member.Username); !errors.Is(err, user.ErrUserNotFound) {
240 t.Fatalf("self-deleted account lookup error = %v, want ErrUserNotFound", err)
241 }
242 if _, err := repoSvc.GetByID(ctx, repository.ID); !errors.Is(err, repo.ErrRepoNotFound) {
243 t.Fatalf("self-deleted repository lookup error = %v, want ErrRepoNotFound", err)
244 }
245 if err := userSvc.Delete(ctx, admin.ID); !errors.Is(err, user.ErrLastAdmin) {
246 t.Fatalf("last administrator deletion error = %v, want ErrLastAdmin", err)
247 }
248 }
249