package handlers import ( "context" "errors" "io" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "strconv" "strings" "testing" "polar/internal/auth" "polar/internal/db" "polar/internal/repo" "polar/internal/user" "polar/internal/web/middleware" ) func authenticatedFormRequest(t *testing.T, method, path string, form url.Values, sess *auth.Session) *http.Request { t.Helper() req := httptest.NewRequest(method, path, strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.AddCookie(&http.Cookie{Name: middleware.SessionCookieName, Value: sess.ID}) return req } func TestRegistrationModesMediaPersistence(t *testing.T) { app, authSvc, userSvc, repoSvc := setupTestApp(t) ctx := context.Background() admin, err := userSvc.RegisterWithMode(ctx, "admin1", "admin1@example.com", "password123", db.RegistrationModePublic) if err != nil { t.Fatalf("register first administrator: %v", err) } if !admin.IsAdmin || !admin.IsApproved { t.Fatalf("first account must be an approved administrator: %+v", admin) } pending, err := userSvc.RegisterWithMode(ctx, "pending1", "pending1@example.com", "password123", db.RegistrationModeConfirm) if err != nil { t.Fatalf("register pending account: %v", err) } if pending.IsApproved { t.Fatal("confirm-only registration must create a pending account") } if _, err := userSvc.Authenticate(ctx, pending.Username, "password123"); !errors.Is(err, user.ErrPendingApproval) { t.Fatalf("pending account login error = %v, want ErrPendingApproval", err) } if err := userSvc.SetApproved(ctx, pending.ID, true); err != nil { t.Fatalf("approve pending account: %v", err) } if _, err := userSvc.Authenticate(ctx, pending.Username, "password123"); err != nil { t.Fatalf("approved account login failed: %v", err) } member, err := userSvc.RegisterWithMode(ctx, "member1", "member1@example.com", "password123", db.RegistrationModePublic) if err != nil { t.Fatalf("register public account: %v", err) } if !member.IsApproved { t.Fatal("public registration must create an approved account") } if _, err := userSvc.RegisterWithMode(ctx, "closed1", "closed1@example.com", "password123", db.RegistrationModeClosed); !errors.Is(err, user.ErrRegistrationDisabled) { t.Fatalf("closed registration error = %v, want ErrRegistrationDisabled", err) } background := "data:image/webp;base64,AAAA" avatar := "data:image/webp;base64,BBBB" if err := userSvc.UpdateBackground(ctx, member.ID, background); err != nil { t.Fatalf("update profile background: %v", err) } if err := userSvc.UpdateAvatar(ctx, member.ID, avatar); err != nil { t.Fatalf("update profile avatar: %v", err) } storedMember, err := userSvc.GetByUsername(ctx, member.Username) if err != nil { t.Fatalf("load member: %v", err) } if storedMember.BackgroundURL != background || storedMember.AvatarURL != avatar { t.Fatalf("profile media not persisted: background=%q avatar=%q", storedMember.BackgroundURL, storedMember.AvatarURL) } repository, err := repoSvc.Create(ctx, member, "media-repo", "Media persistence", false) if err != nil { t.Fatalf("create repository: %v", err) } repoAvatar := "data:image/webp;base64,CCCC" if err := repoSvc.UpdateAvatar(ctx, repository.ID, repoAvatar); err != nil { t.Fatalf("update repository avatar: %v", err) } repoBackground := "data:image/webp;base64,DDDD" sess, err := authSvc.CreateSession(ctx, member.ID) if err != nil { t.Fatalf("create member session: %v", err) } settingsReq := authenticatedFormRequest(t, http.MethodPost, "/member1/media-repo/settings", url.Values{ "_csrf": {sess.CSRFToken}, "description": {"Media persistence"}, "default_branch": {"main"}, "visibility": {"public"}, "enable_issues": {"true"}, "enable_pulls": {"true"}, "enable_releases": {"true"}, "enable_activity": {"true"}, "background_data": {repoBackground}, }, sess) settingsResp, err := app.Test(settingsReq, 5000) if err != nil || settingsResp.StatusCode != http.StatusSeeOther { t.Fatalf("repository settings response: status=%v err=%v", settingsResp.StatusCode, err) } storedRepo, err := repoSvc.GetByID(ctx, repository.ID) if err != nil { t.Fatalf("load repository: %v", err) } if storedRepo.AvatarURL != repoAvatar || storedRepo.BackgroundURL != repoBackground { t.Fatalf("repository media = avatar %q background %q", storedRepo.AvatarURL, storedRepo.BackgroundURL) } profileResp, err := app.Test(httptest.NewRequest(http.MethodGet, "/member1", nil), 5000) if err != nil || profileResp.StatusCode != http.StatusOK { t.Fatalf("profile response: status=%v err=%v", profileResp.StatusCode, err) } profileBody, _ := io.ReadAll(profileResp.Body) if !strings.Contains(string(profileBody), background) || !strings.Contains(string(profileBody), "page-bg") { t.Fatal("profile page must render the saved page background") } if strings.Contains(string(profileBody), "profile-hero-bg") || strings.Contains(string(profileBody), "profile-hero has-bg") { t.Fatal("profile background must not be rendered inside the profile header") } repoResp, err := app.Test(httptest.NewRequest(http.MethodGet, "/member1/media-repo", nil), 5000) if err != nil || repoResp.StatusCode != http.StatusOK { t.Fatalf("repository response: status=%v err=%v", repoResp.StatusCode, err) } repoBody, _ := io.ReadAll(repoResp.Body) if !strings.Contains(string(repoBody), repoAvatar) { t.Fatal("repository page must render the custom repository avatar") } if !strings.Contains(string(repoBody), repoBackground) || !strings.Contains(string(repoBody), "page-bg") { t.Fatal("repository page must render the saved page background") } } func TestAdminCanDeleteRepositoriesAndAccounts(t *testing.T) { app, authSvc, userSvc, repoSvc := setupTestApp(t) ctx := context.Background() admin, err := userSvc.RegisterWithMode(ctx, "admin2", "admin2@example.com", "password123", db.RegistrationModePublic) if err != nil { t.Fatalf("register administrator: %v", err) } member, err := userSvc.RegisterWithMode(ctx, "member2", "member2@example.com", "password123", db.RegistrationModePublic) if err != nil { t.Fatalf("register member: %v", err) } repository, err := repoSvc.Create(ctx, member, "delete-me", "Temporary repository", false) if err != nil { t.Fatalf("create repository: %v", err) } sess, err := authSvc.CreateSession(ctx, admin.ID) if err != nil { t.Fatalf("create administrator session: %v", err) } repoDeletePath := "/admin/repos/" + strconv.FormatInt(repository.ID, 10) + "/delete" req := authenticatedFormRequest(t, http.MethodPost, repoDeletePath, url.Values{ "_csrf": {sess.CSRFToken}, "confirm_name": {repository.Name}, }, sess) resp, err := app.Test(req, 5000) if err != nil { t.Fatalf("admin repository delete request: %v", err) } if resp.StatusCode != http.StatusSeeOther { t.Fatalf("admin repository delete status = %d, want 303", resp.StatusCode) } if _, err := repoSvc.GetByID(ctx, repository.ID); !errors.Is(err, repo.ErrRepoNotFound) { t.Fatalf("deleted repository lookup error = %v, want ErrRepoNotFound", err) } remaining, err := repoSvc.Create(ctx, member, "delete-with-account", "Temporary repository", false) if err != nil { t.Fatalf("create remaining repository: %v", err) } diskPath, err := repoSvc.DiskPath(member.Username, remaining.Name) if err != nil { t.Fatalf("resolve repository path: %v", err) } ownerDir := filepath.Dir(diskPath) userDeletePath := "/admin/users/" + strconv.FormatInt(member.ID, 10) + "/delete" req = authenticatedFormRequest(t, http.MethodPost, userDeletePath, url.Values{ "_csrf": {sess.CSRFToken}, "confirm_username": {member.Username}, }, sess) resp, err = app.Test(req, 5000) if err != nil { t.Fatalf("admin account delete request: %v", err) } if resp.StatusCode != http.StatusSeeOther { t.Fatalf("admin account delete status = %d, want 303", resp.StatusCode) } if _, err := userSvc.GetByUsername(ctx, member.Username); !errors.Is(err, user.ErrUserNotFound) { t.Fatalf("deleted account lookup error = %v, want ErrUserNotFound", err) } if _, err := repoSvc.GetByID(ctx, remaining.ID); !errors.Is(err, repo.ErrRepoNotFound) { t.Fatalf("cascaded repository lookup error = %v, want ErrRepoNotFound", err) } if _, err := os.Stat(ownerDir); !os.IsNotExist(err) { t.Fatalf("owner repository directory still exists: %v", err) } } func TestAccountSelfDeleteAndLastAdminProtection(t *testing.T) { app, authSvc, userSvc, repoSvc := setupTestApp(t) ctx := context.Background() admin, err := userSvc.RegisterWithMode(ctx, "admin3", "admin3@example.com", "password123", db.RegistrationModePublic) if err != nil { t.Fatalf("register administrator: %v", err) } member, err := userSvc.RegisterWithMode(ctx, "member3", "member3@example.com", "password123", db.RegistrationModePublic) if err != nil { t.Fatalf("register member: %v", err) } repository, err := repoSvc.Create(ctx, member, "self-delete", "Temporary repository", false) if err != nil { t.Fatalf("create repository: %v", err) } sess, err := authSvc.CreateSession(ctx, member.ID) if err != nil { t.Fatalf("create member session: %v", err) } wrongPassword := authenticatedFormRequest(t, http.MethodPost, "/settings/delete", url.Values{ "_csrf": {sess.CSRFToken}, "confirm_username": {member.Username}, "password": {"wrong-password"}, }, sess) resp, err := app.Test(wrongPassword, 5000) if err != nil || resp.StatusCode != http.StatusSeeOther { t.Fatalf("wrong-password delete response: status=%v err=%v", resp.StatusCode, err) } if _, err := userSvc.GetByUsername(ctx, member.Username); err != nil { t.Fatalf("account was deleted with a wrong password: %v", err) } deleteReq := authenticatedFormRequest(t, http.MethodPost, "/settings/delete", url.Values{ "_csrf": {sess.CSRFToken}, "confirm_username": {member.Username}, "password": {"password123"}, }, sess) resp, err = app.Test(deleteReq, 5000) if err != nil { t.Fatalf("self delete request: %v", err) } if resp.StatusCode != http.StatusSeeOther { t.Fatalf("self delete status = %d, want 303", resp.StatusCode) } if _, err := userSvc.GetByUsername(ctx, member.Username); !errors.Is(err, user.ErrUserNotFound) { t.Fatalf("self-deleted account lookup error = %v, want ErrUserNotFound", err) } if _, err := repoSvc.GetByID(ctx, repository.ID); !errors.Is(err, repo.ErrRepoNotFound) { t.Fatalf("self-deleted repository lookup error = %v, want ErrRepoNotFound", err) } if err := userSvc.Delete(ctx, admin.ID); !errors.Is(err, user.ErrLastAdmin) { t.Fatalf("last administrator deletion error = %v, want ErrLastAdmin", err) } }