Go · 5218 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package auth_test
4
5 import (
6 "io"
7 "net/http"
8 "net/url"
9 "strings"
10 "testing"
11 )
12
13 // loginAccountUser is the same login helper as profile_test.go but
14 // against a different account so tests stay isolated when run in
15 // parallel.
16 func loginAccountUser(t *testing.T, name string) *client {
17 t.Helper()
18 httpsrv, captor := newTestServer(t, false)
19 cli := newClient(t, httpsrv)
20 mustSignup(t, cli, name, name+"@example.com", "correct horse battery staple")
21 tok := extractTokenFromMessage(t, captor.all()[0], "/verify-email")
22 _ = cli.get(t, "/verify-email/"+tok).Body.Close()
23
24 csrf := cli.extractCSRF(t, "/login")
25 resp := cli.post(t, "/login", url.Values{
26 "csrf_token": {csrf},
27 "username": {name},
28 "password": {"correct horse battery staple"},
29 })
30 if resp.StatusCode != http.StatusSeeOther {
31 t.Fatalf("login: %d", resp.StatusCode)
32 }
33 _ = resp.Body.Close()
34 return cli
35 }
36
37 func TestAccount_RenameRoundtrip(t *testing.T) {
38 t.Parallel()
39 cli := loginAccountUser(t, "renamea")
40 csrf := cli.extractCSRF(t, "/settings/account")
41
42 resp := cli.post(t, "/settings/account/username", url.Values{
43 "csrf_token": {csrf},
44 "new_username": {"renameb"},
45 })
46 defer func() { _ = resp.Body.Close() }()
47 if resp.StatusCode != http.StatusOK {
48 body, _ := io.ReadAll(resp.Body)
49 t.Fatalf("status=%d body=%s", resp.StatusCode, body)
50 }
51 body, _ := io.ReadAll(resp.Body)
52 if !strings.Contains(string(body), "Username updated to renameb") {
53 t.Errorf("missing success message; got: %s", body)
54 }
55 if !strings.Contains(string(body), "USERNAME=renameb") {
56 t.Errorf("expected USERNAME=renameb in body; got: %s", body)
57 }
58 if !strings.Contains(string(body), "USED=1/3") {
59 t.Errorf("expected USED=1/3 after rename; got: %s", body)
60 }
61
62 // The old name should now redirect to the new profile (/oldname → /newname).
63 resp = cli.get(t, "/renamea")
64 defer func() { _ = resp.Body.Close() }()
65 // /renamea hits the catch-all in this minimal test rig — we don't have
66 // the profile handler mounted, so the only thing we can verify here
67 // is the DB state.
68 }
69
70 func TestAccount_RejectsReservedName(t *testing.T) {
71 t.Parallel()
72 cli := loginAccountUser(t, "reserveda")
73 csrf := cli.extractCSRF(t, "/settings/account")
74 resp := cli.post(t, "/settings/account/username", url.Values{
75 "csrf_token": {csrf},
76 "new_username": {"settings"},
77 })
78 defer func() { _ = resp.Body.Close() }()
79 body, _ := io.ReadAll(resp.Body)
80 if !strings.Contains(string(body), "reserved") {
81 t.Fatalf("expected reserved error, got: %s", body)
82 }
83 }
84
85 func TestAccount_RejectsInvalidShape(t *testing.T) {
86 t.Parallel()
87 cli := loginAccountUser(t, "shapea")
88 csrf := cli.extractCSRF(t, "/settings/account")
89 // UPPER is normalized to lowercase by the handler (user-friendly), so
90 // it isn't on this rejection list.
91 for _, bad := range []string{"-leading", "trailing-", "with spaces", ""} {
92 resp := cli.post(t, "/settings/account/username", url.Values{
93 "csrf_token": {csrf},
94 "new_username": {bad},
95 })
96 body, _ := io.ReadAll(resp.Body)
97 _ = resp.Body.Close()
98 if !strings.Contains(string(body), "Username must") && !strings.Contains(string(body), "1–39") {
99 t.Errorf("input %q: expected validation error, got: %s", bad, body)
100 }
101 }
102 }
103
104 func TestAccount_RejectsTaken(t *testing.T) {
105 t.Parallel()
106 // Two clients on the SAME server so they share the same DB.
107 httpsrv, captor := newTestServer(t, false)
108 cliA := newClient(t, httpsrv)
109 cliB := newClient(t, httpsrv)
110
111 mustSignup(t, cliA, "takena", "takena@example.com", "correct horse battery staple")
112 mustSignup(t, cliB, "takenb", "takenb@example.com", "correct horse battery staple")
113 for _, m := range captor.all() {
114 // Verify each.
115 if tok := extractTokenFromMessage(t, m, "/verify-email"); tok != "" {
116 _ = cliA.get(t, "/verify-email/"+tok).Body.Close()
117 }
118 }
119 csrf := cliA.extractCSRF(t, "/login")
120 _ = cliA.post(t, "/login", url.Values{
121 "csrf_token": {csrf}, "username": {"takena"}, "password": {"correct horse battery staple"},
122 }).Body.Close()
123
124 csrf = cliA.extractCSRF(t, "/settings/account")
125 resp := cliA.post(t, "/settings/account/username", url.Values{
126 "csrf_token": {csrf},
127 "new_username": {"takenb"}, // already used by cliB
128 })
129 defer func() { _ = resp.Body.Close() }()
130 body, _ := io.ReadAll(resp.Body)
131 if !strings.Contains(string(body), "taken") {
132 t.Fatalf("expected taken error, got: %s", body)
133 }
134 }
135
136 func TestAccount_RateLimitAtThree(t *testing.T) {
137 t.Parallel()
138 cli := loginAccountUser(t, "ratea")
139
140 // Burn three rename slots.
141 names := []string{"rateb", "ratec", "rated"}
142 for _, n := range names {
143 csrf := cli.extractCSRF(t, "/settings/account")
144 resp := cli.post(t, "/settings/account/username", url.Values{
145 "csrf_token": {csrf},
146 "new_username": {n},
147 })
148 _ = resp.Body.Close()
149 }
150
151 // Fourth attempt should be blocked.
152 csrf := cli.extractCSRF(t, "/settings/account")
153 resp := cli.post(t, "/settings/account/username", url.Values{
154 "csrf_token": {csrf},
155 "new_username": {"ratee"},
156 })
157 defer func() { _ = resp.Body.Close() }()
158 body, _ := io.ReadAll(resp.Body)
159 if !strings.Contains(string(body), "too many") {
160 t.Fatalf("expected rate-limit error, got: %s", body)
161 }
162 }
163