db_test.go
1package user
2
3import (
4 "database/sql"
5 "testing"
6 "time"
7
8 _ "github.com/mattn/go-sqlite3"
9 "github.com/stretchr/testify/assert"
10)
11
12func getDB() (*sql.DB, func()) {
13 db, err := sql.Open("sqlite3", "file:test.db?cache=shared&mode=memory&_fk=false")
14 if err != nil {
15 panic(err)
16 }
17 if err := Migrate(db); err != nil {
18 panic(err)
19 }
20 return db, func() { db.Close() }
21}
22
23func TestEmail(t *testing.T) {
24 db, close := getDB()
25 defer close()
26
27 e1 := Email("[email protected]")
28 e2 := Email("[email protected]")
29
30 // insert email with mixed case
31 assert.NoError(t, Set(db, int64(0), e1))
32 // violate unique lowercase index
33 assert.Error(t, Set(db, int64(1), e2))
34 // upsert different email value (lowercase) when conflict on id
35 assert.NoError(t, Set(db, int64(0), e2))
36
37 er, _, err := GetEmailAndHandle(db, int64(0))
38 assert.NoError(t, err)
39 assert.Equal(t, "[email protected]", er)
40}
41
42func TestHandle(t *testing.T) {
43 db, close := getDB()
44 defer close()
45
46 h1 := Handle("MFer")
47 h2 := Handle("mfer")
48 h3 := Handle("scooter")
49
50 assert.NoError(t, Set(db, int64(0), h1))
51 // violate unique constraint
52 assert.Error(t, Set(db, int64(1), h2))
53 // blocklist
54 assert.Error(t, Set(db, int64(3), h3))
55
56 assert.ErrorIs(t, CheckHandle(db, "MFer"), ErrHandleExists)
57 assert.ErrorIs(t, CheckHandle(db, "mfer"), ErrHandleExists)
58 assert.NoError(t, CheckHandle(db, "test"))
59}
60
61func TestHandleValidity(t *testing.T) {
62 tc := map[string]bool{
63 "scooter": false,
64 "rtw.run": false,
65 "test": true,
66 "a": false,
67 "AAA": true,
68 "test.com": true,
69 "something.net/path": true,
70 "me...": true,
71 "also-me": true,
72 "something;": false,
73 ".": false,
74 "..": true,
75 "[deleted]": false,
76 "[DELETED]": false,
77 "MODERATOR": false,
78 "@twitter": true,
79 "test![yo]": true,
80 }
81 for handle, isOK := range tc {
82 t.Run(handle, func(t *testing.T) {
83 ok := isHandleValid(handle)
84 assert.Equal(t, isOK, ok)
85 })
86 }
87}
88
89func TestMetadata(t *testing.T) {
90 db, close := getDB()
91 defer close()
92
93 // Covers to the two most common cases for additional metadata
94
95 // Case 1: map[string]T
96 in1 := map[string]string{"test": "test1", "test2": "test"}
97 out1 := map[string]string{}
98 assert.NoError(t, Set(db, int64(0), Metadata(in1)))
99 assert.NoError(t, GetMetadata(db, int64(0), &out1))
100 assert.Equal(t, in1, out1)
101
102 // Case 2: struct marshaled to JSON
103 type testStruct struct {
104 Name string
105 Value int
106 }
107 in2 := testStruct{"test", 1}
108 out2 := testStruct{}
109 assert.NoError(t, Set(db, int64(1), Metadata(in2)))
110 assert.NoError(t, GetMetadata(db, int64(1), &out2))
111 assert.Equal(t, in2, out2)
112}
113
114func TestCheckEmail(t *testing.T) {
115 db, close := getDB()
116 defer close()
117
118 h1 := Email("[email protected]")
119 h2 := Email("[email protected]")
120
121 assert.NoError(t, Set(db, int64(0), h1))
122 // violate unique constraint
123 assert.Error(t, Set(db, int64(1), h2))
124
125 assert.ErrorIs(t, CheckEmail(db, "[email protected]"), ErrAccountExists)
126 assert.ErrorIs(t, CheckEmail(db, "[email protected]"), ErrAccountExists)
127 assert.ErrorIs(t, CheckEmail(db, "notanemail"), ErrInvalidEmail)
128 assert.NoError(t, CheckEmail(db, "[email protected]"))
129}
130
131func TestLogin(t *testing.T) {
132 db, close := getDB()
133 defer close()
134
135 assert.NoError(t, Set(db, int64(0), Email("[email protected]"), Password("pa55w0rd"), Handle("test")))
136 id, err := Login[int64](db, "[email protected]", "pa55w0rd")
137 assert.NoError(t, err)
138 assert.Equal(t, int64(0), id)
139
140 id, err = Login[int64](db, "test", "pa55w0rd")
141 assert.NoError(t, err)
142 assert.Equal(t, int64(0), id)
143
144 id, err = Login[int64](db, "test", "garbage")
145 assert.Equal(t, id, int64(-1))
146 assert.ErrorIs(t, err, ErrPasswordMismatch)
147}
148
149func TestCleanup(t *testing.T) {
150 db, close := getDB()
151 defer close()
152
153 // create anon account, no handle, no email
154 assert.NoError(t, Set(db, int64(0)))
155 // manually backdate creation time more than a month
156 _, err := db.Exec("UPDATE users SET ctime=? WHERE id=0", time.Now().UTC().Add(-31*24*time.Hour))
157 assert.NoError(t, err)
158 // insert again to trigger
159 assert.NoError(t, Set(db, int64(1)))
160
161 var numRows int
162 err = db.QueryRow("SELECT COUNT(*) FROM users WHERE id=?", int64(0)).Scan(&numRows)
163 // row id 0 deleted
164 assert.NoError(t, err)
165 assert.Equal(t, 0, numRows)
166
167 // row id 1 remains
168 err = db.QueryRow("SELECT COUNT(*) FROM users").Scan(&numRows)
169 assert.NoError(t, err)
170 assert.Equal(t, 1, numRows)
171
172}