rin
Публичный Open-source lightweight git hosting.
account_admin_test.go · 274 строки · 10.5 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, authSvc, 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 repoBackground := "data:image/webp;base64,DDDD"
96 sess, err := authSvc.CreateSession(ctx, member.ID)
97 if err != nil {
98 t.Fatalf("create member session: %v", err)
99 }
100 settingsReq := authenticatedFormRequest(t, http.MethodPost, "/member1/media-repo/settings", url.Values{
101 "_csrf": {sess.CSRFToken},
102 "description": {"Media persistence"},
103 "default_branch": {"main"},
104 "visibility": {"public"},
105 "enable_issues": {"true"},
106 "enable_pulls": {"true"},
107 "enable_releases": {"true"},
108 "enable_activity": {"true"},
109 "background_data": {repoBackground},
110 }, sess)
111 settingsResp, err := app.Test(settingsReq, 5000)
112 if err != nil || settingsResp.StatusCode != http.StatusSeeOther {
113 t.Fatalf("repository settings response: status=%v err=%v", settingsResp.StatusCode, err)
114 }
115 storedRepo, err := repoSvc.GetByID(ctx, repository.ID)
116 if err != nil {
117 t.Fatalf("load repository: %v", err)
118 }
119 if storedRepo.AvatarURL != repoAvatar || storedRepo.BackgroundURL != repoBackground {
120 t.Fatalf("repository media = avatar %q background %q", storedRepo.AvatarURL, storedRepo.BackgroundURL)
121 }
122
123 profileResp, err := app.Test(httptest.NewRequest(http.MethodGet, "/member1", nil), 5000)
124 if err != nil || profileResp.StatusCode != http.StatusOK {
125 t.Fatalf("profile response: status=%v err=%v", profileResp.StatusCode, err)
126 }
127 profileBody, _ := io.ReadAll(profileResp.Body)
128 if !strings.Contains(string(profileBody), background) || !strings.Contains(string(profileBody), "page-bg") {
129 t.Fatal("profile page must render the saved page background")
130 }
131 if strings.Contains(string(profileBody), "profile-hero-bg") || strings.Contains(string(profileBody), "profile-hero has-bg") {
132 t.Fatal("profile background must not be rendered inside the profile header")
133 }
134
135 repoResp, err := app.Test(httptest.NewRequest(http.MethodGet, "/member1/media-repo", nil), 5000)
136 if err != nil || repoResp.StatusCode != http.StatusOK {
137 t.Fatalf("repository response: status=%v err=%v", repoResp.StatusCode, err)
138 }
139 repoBody, _ := io.ReadAll(repoResp.Body)
140 if !strings.Contains(string(repoBody), repoAvatar) {
141 t.Fatal("repository page must render the custom repository avatar")
142 }
143 if !strings.Contains(string(repoBody), repoBackground) || !strings.Contains(string(repoBody), "page-bg") {
144 t.Fatal("repository page must render the saved page background")
145 }
146 }
147
148 func TestAdminCanDeleteRepositoriesAndAccounts(t *testing.T) {
149 app, authSvc, userSvc, repoSvc := setupTestApp(t)
150 ctx := context.Background()
151
152 admin, err := userSvc.RegisterWithMode(ctx, "admin2", "admin2@example.com", "password123", db.RegistrationModePublic)
153 if err != nil {
154 t.Fatalf("register administrator: %v", err)
155 }
156 member, err := userSvc.RegisterWithMode(ctx, "member2", "member2@example.com", "password123", db.RegistrationModePublic)
157 if err != nil {
158 t.Fatalf("register member: %v", err)
159 }
160 repository, err := repoSvc.Create(ctx, member, "delete-me", "Temporary repository", false)
161 if err != nil {
162 t.Fatalf("create repository: %v", err)
163 }
164
165 sess, err := authSvc.CreateSession(ctx, admin.ID)
166 if err != nil {
167 t.Fatalf("create administrator session: %v", err)
168 }
169
170 repoDeletePath := "/admin/repos/" + strconv.FormatInt(repository.ID, 10) + "/delete"
171 req := authenticatedFormRequest(t, http.MethodPost, repoDeletePath, url.Values{
172 "_csrf": {sess.CSRFToken},
173 "confirm_name": {repository.Name},
174 }, sess)
175 resp, err := app.Test(req, 5000)
176 if err != nil {
177 t.Fatalf("admin repository delete request: %v", err)
178 }
179 if resp.StatusCode != http.StatusSeeOther {
180 t.Fatalf("admin repository delete status = %d, want 303", resp.StatusCode)
181 }
182 if _, err := repoSvc.GetByID(ctx, repository.ID); !errors.Is(err, repo.ErrRepoNotFound) {
183 t.Fatalf("deleted repository lookup error = %v, want ErrRepoNotFound", err)
184 }
185
186 remaining, err := repoSvc.Create(ctx, member, "delete-with-account", "Temporary repository", false)
187 if err != nil {
188 t.Fatalf("create remaining repository: %v", err)
189 }
190 diskPath, err := repoSvc.DiskPath(member.Username, remaining.Name)
191 if err != nil {
192 t.Fatalf("resolve repository path: %v", err)
193 }
194 ownerDir := filepath.Dir(diskPath)
195
196 userDeletePath := "/admin/users/" + strconv.FormatInt(member.ID, 10) + "/delete"
197 req = authenticatedFormRequest(t, http.MethodPost, userDeletePath, url.Values{
198 "_csrf": {sess.CSRFToken},
199 "confirm_username": {member.Username},
200 }, sess)
201 resp, err = app.Test(req, 5000)
202 if err != nil {
203 t.Fatalf("admin account delete request: %v", err)
204 }
205 if resp.StatusCode != http.StatusSeeOther {
206 t.Fatalf("admin account delete status = %d, want 303", resp.StatusCode)
207 }
208 if _, err := userSvc.GetByUsername(ctx, member.Username); !errors.Is(err, user.ErrUserNotFound) {
209 t.Fatalf("deleted account lookup error = %v, want ErrUserNotFound", err)
210 }
211 if _, err := repoSvc.GetByID(ctx, remaining.ID); !errors.Is(err, repo.ErrRepoNotFound) {
212 t.Fatalf("cascaded repository lookup error = %v, want ErrRepoNotFound", err)
213 }
214 if _, err := os.Stat(ownerDir); !os.IsNotExist(err) {
215 t.Fatalf("owner repository directory still exists: %v", err)
216 }
217 }
218
219 func TestAccountSelfDeleteAndLastAdminProtection(t *testing.T) {
220 app, authSvc, userSvc, repoSvc := setupTestApp(t)
221 ctx := context.Background()
222
223 admin, err := userSvc.RegisterWithMode(ctx, "admin3", "admin3@example.com", "password123", db.RegistrationModePublic)
224 if err != nil {
225 t.Fatalf("register administrator: %v", err)
226 }
227 member, err := userSvc.RegisterWithMode(ctx, "member3", "member3@example.com", "password123", db.RegistrationModePublic)
228 if err != nil {
229 t.Fatalf("register member: %v", err)
230 }
231 repository, err := repoSvc.Create(ctx, member, "self-delete", "Temporary repository", false)
232 if err != nil {
233 t.Fatalf("create repository: %v", err)
234 }
235
236 sess, err := authSvc.CreateSession(ctx, member.ID)
237 if err != nil {
238 t.Fatalf("create member session: %v", err)
239 }
240 wrongPassword := authenticatedFormRequest(t, http.MethodPost, "/settings/delete", url.Values{
241 "_csrf": {sess.CSRFToken},
242 "confirm_username": {member.Username},
243 "password": {"wrong-password"},
244 }, sess)
245 resp, err := app.Test(wrongPassword, 5000)
246 if err != nil || resp.StatusCode != http.StatusSeeOther {
247 t.Fatalf("wrong-password delete response: status=%v err=%v", resp.StatusCode, err)
248 }
249 if _, err := userSvc.GetByUsername(ctx, member.Username); err != nil {
250 t.Fatalf("account was deleted with a wrong password: %v", err)
251 }
252
253 deleteReq := authenticatedFormRequest(t, http.MethodPost, "/settings/delete", url.Values{
254 "_csrf": {sess.CSRFToken},
255 "confirm_username": {member.Username},
256 "password": {"password123"},
257 }, sess)
258 resp, err = app.Test(deleteReq, 5000)
259 if err != nil {
260 t.Fatalf("self delete request: %v", err)
261 }
262 if resp.StatusCode != http.StatusSeeOther {
263 t.Fatalf("self delete status = %d, want 303", resp.StatusCode)
264 }
265 if _, err := userSvc.GetByUsername(ctx, member.Username); !errors.Is(err, user.ErrUserNotFound) {
266 t.Fatalf("self-deleted account lookup error = %v, want ErrUserNotFound", err)
267 }
268 if _, err := repoSvc.GetByID(ctx, repository.ID); !errors.Is(err, repo.ErrRepoNotFound) {
269 t.Fatalf("self-deleted repository lookup error = %v, want ErrRepoNotFound", err)
270 }
271 if err := userSvc.Delete(ctx, admin.ID); !errors.Is(err, user.ErrLastAdmin) {
272 t.Fatalf("last administrator deletion error = %v, want ErrLastAdmin", err)
273 }
274 }
275