Go · 5000 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package auth_test
4
5 import (
6 "context"
7 "io"
8 "net/http"
9 "net/url"
10 "strings"
11 "testing"
12 "time"
13
14 "github.com/jackc/pgx/v5/pgxpool"
15 )
16
17 func loginDangerUser(t *testing.T, name string) (cli *client, pool *pgxpool.Pool, captor *captureSender) {
18 t.Helper()
19 httpsrv, pool, captor := newTestServerWithPool(t, false)
20 cli = newClient(t, httpsrv)
21 mustSignup(t, cli, name, name+"@example.com", "correct horse battery staple")
22 tok := extractTokenFromMessage(t, captor.all()[0], "/verify-email")
23 _ = cli.get(t, "/verify-email/"+tok).Body.Close()
24
25 csrf := cli.extractCSRF(t, "/login")
26 resp := cli.post(t, "/login", url.Values{
27 "csrf_token": {csrf},
28 "username": {name},
29 "password": {"correct horse battery staple"},
30 })
31 if resp.StatusCode != http.StatusSeeOther {
32 t.Fatalf("login: %d", resp.StatusCode)
33 }
34 _ = resp.Body.Close()
35 return cli, pool, captor
36 }
37
38 func TestDanger_DeleteRoundtripAndRestore(t *testing.T) {
39 t.Parallel()
40 cli, _, _ := loginDangerUser(t, "danga")
41
42 // Wrong username should be rejected.
43 csrf := cli.extractCSRF(t, "/settings/danger")
44 resp := cli.post(t, "/settings/danger", url.Values{
45 "csrf_token": {csrf},
46 "confirm_username": {"not-me"},
47 "password": {"correct horse battery staple"},
48 })
49 body, _ := io.ReadAll(resp.Body)
50 _ = resp.Body.Close()
51 if !strings.Contains(string(body), "Type your username") {
52 t.Fatalf("expected confirm-username error, got: %s", body)
53 }
54
55 // Wrong password should be rejected.
56 csrf = cli.extractCSRF(t, "/settings/danger")
57 resp = cli.post(t, "/settings/danger", url.Values{
58 "csrf_token": {csrf},
59 "confirm_username": {"danga"},
60 "password": {"definitely-not-the-password"},
61 })
62 body, _ = io.ReadAll(resp.Body)
63 _ = resp.Body.Close()
64 if !strings.Contains(string(body), "Password is incorrect") {
65 t.Fatalf("expected password error, got: %s", body)
66 }
67
68 // Correct credentials -> 303 to "/?notice=account-deleted".
69 csrf = cli.extractCSRF(t, "/settings/danger")
70 resp = cli.post(t, "/settings/danger", url.Values{
71 "csrf_token": {csrf},
72 "confirm_username": {"danga"},
73 "password": {"correct horse battery staple"},
74 })
75 if resp.StatusCode != http.StatusSeeOther {
76 body, _ := io.ReadAll(resp.Body)
77 t.Fatalf("delete: status=%d body=%s", resp.StatusCode, body)
78 }
79 if loc := resp.Header.Get("Location"); !strings.Contains(loc, "account-deleted") {
80 t.Fatalf("Location=%q", loc)
81 }
82 _ = resp.Body.Close()
83
84 // /settings/profile is now unreachable for this client (epoch stale +
85 // cookie cleared). RequireUser bounces to /login.
86 resp = cli.get(t, "/settings/profile")
87 defer func() { _ = resp.Body.Close() }()
88 if resp.StatusCode != http.StatusSeeOther && resp.StatusCode != http.StatusFound {
89 t.Fatalf("post-delete /settings/profile expected redirect, got %d", resp.StatusCode)
90 }
91
92 // Restore-on-login: signing in again with the SAME credentials
93 // should clear deleted_at. The login attempt itself returns the
94 // usual 303 to /.
95 cli2 := newClient(t, cli.srv)
96 csrf = cli2.extractCSRF(t, "/login")
97 resp = cli2.post(t, "/login", url.Values{
98 "csrf_token": {csrf},
99 "username": {"danga"},
100 "password": {"correct horse battery staple"},
101 })
102 if resp.StatusCode != http.StatusSeeOther {
103 body, _ := io.ReadAll(resp.Body)
104 t.Fatalf("restore-login: status=%d body=%s", resp.StatusCode, body)
105 }
106 _ = resp.Body.Close()
107
108 // Now /settings/profile is back.
109 resp = cli2.get(t, "/settings/profile")
110 defer func() { _ = resp.Body.Close() }()
111 if resp.StatusCode != http.StatusOK {
112 t.Fatalf("post-restore /settings/profile expected 200, got %d", resp.StatusCode)
113 }
114 }
115
116 func TestDanger_PostGracePermanent(t *testing.T) {
117 t.Parallel()
118 cli, pool, _ := loginDangerUser(t, "dangb")
119
120 // Delete normally.
121 csrf := cli.extractCSRF(t, "/settings/danger")
122 resp := cli.post(t, "/settings/danger", url.Values{
123 "csrf_token": {csrf},
124 "confirm_username": {"dangb"},
125 "password": {"correct horse battery staple"},
126 })
127 if resp.StatusCode != http.StatusSeeOther {
128 t.Fatalf("delete: %d", resp.StatusCode)
129 }
130 _ = resp.Body.Close()
131
132 // Backdate deleted_at past the grace window so the next login should
133 // be treated as nonexistent. Uses the SAME pool the test server is
134 // reading from — a fresh dbtest.NewTestDB call would clone a brand
135 // new database.
136 if _, err := pool.Exec(context.Background(),
137 "UPDATE users SET deleted_at = $1 WHERE username = 'dangb'",
138 time.Now().Add(-30*24*time.Hour),
139 ); err != nil {
140 t.Fatalf("backdate: %v", err)
141 }
142
143 cli2 := newClient(t, cli.srv)
144 csrf = cli2.extractCSRF(t, "/login")
145 resp = cli2.post(t, "/login", url.Values{
146 "csrf_token": {csrf},
147 "username": {"dangb"},
148 "password": {"correct horse battery staple"},
149 })
150 defer func() { _ = resp.Body.Close() }()
151 body, _ := io.ReadAll(resp.Body)
152 if !strings.Contains(string(body), "Incorrect username or password") {
153 t.Fatalf("expected post-grace login to be treated as wrong, got: %s", body)
154 }
155 }
156