user_test.go
1package user
2
3import (
4 "os"
5 "testing"
6
7 "git.kilimanjaro.io/rtw/pkg/abac"
8 "git.kilimanjaro.io/rtw/pkg/id"
9 "git.kilimanjaro.io/rtw/pkg/user"
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/require"
12)
13
14func setupTestService(t *testing.T) (*Service, func()) {
15 tmpDir, err := os.MkdirTemp("", "user_test_*")
16 require.NoError(t, err)
17
18 us, err := NewService(WithBasePath(tmpDir))
19 require.NoError(t, err)
20
21 cleanup := func() {
22 us.db.Close()
23 os.RemoveAll(tmpDir)
24 }
25
26 return us, cleanup
27}
28
29func TestSignup_ValidEmailHandlePassword(t *testing.T) {
30 us, cleanup := setupTestService(t)
31 defer cleanup()
32
33 email := "[email protected]"
34 handle := "testuser"
35 password := "securePassword123"
36
37 userID, err := us.Signup(&email, &handle, password)
38 assert.NoError(t, err)
39 assert.NotEqual(t, 0, userID)
40}
41
42func TestGetUserByID_AfterSignup(t *testing.T) {
43 us, cleanup := setupTestService(t)
44 defer cleanup()
45
46 email := "[email protected]"
47 handle := "testuser"
48 password := "securePassword123"
49
50 userID, err := us.Signup(&email, &handle, password)
51 require.NoError(t, err)
52
53 user, err := us.GetUserByID(userID)
54 assert.NoError(t, err)
55 assert.Equal(t, userID, user.ID)
56 assert.Equal(t, email, user.Email)
57 assert.Equal(t, handle, user.Handle)
58 assert.True(t, user.Registered)
59}
60
61func TestSignup_MissingEmailAndHandle(t *testing.T) {
62 us, cleanup := setupTestService(t)
63 defer cleanup()
64
65 password := "securePassword123"
66
67 userID, err := us.Signup(nil, nil, password)
68 assert.ErrorIs(t, err, ErrMissingEmailOrHandle)
69 assert.Equal(t, id.NotExist, userID)
70}
71
72func TestSignup_MissingPassword(t *testing.T) {
73 us, cleanup := setupTestService(t)
74 defer cleanup()
75
76 email := "[email protected]"
77
78 userID, err := us.Signup(&email, nil, "")
79 assert.ErrorIs(t, err, ErrMissingPassword)
80 assert.Equal(t, id.NotExist, userID)
81}
82
83func TestSignup_InvalidHandle(t *testing.T) {
84 us, cleanup := setupTestService(t)
85 defer cleanup()
86
87 email := "[email protected]"
88 invalidHandle := ";;;"
89
90 userID, err := us.Signup(&email, &invalidHandle, "password123")
91 assert.ErrorIs(t, err, user.ErrInvalidHandle)
92 assert.Equal(t, id.NotExist, userID)
93}
94
95func TestSignup_OnlyHandleAndPassword(t *testing.T) {
96 us, cleanup := setupTestService(t)
97 defer cleanup()
98
99 handle := "handleonly"
100 password := "securePassword123"
101
102 userID, err := us.Signup(nil, &handle, password)
103 assert.NoError(t, err)
104 assert.NotEqual(t, 0, userID)
105
106 // Verify user is registered
107 user, err := us.GetUserByID(userID)
108 assert.NoError(t, err)
109 assert.True(t, user.Registered)
110 assert.Equal(t, handle, user.Handle)
111 assert.Empty(t, user.Email)
112}
113
114func TestCheckHandle_Exists(t *testing.T) {
115 us, cleanup := setupTestService(t)
116 defer cleanup()
117
118 handle := "existinguser"
119 password := "securePassword123"
120
121 _, err := us.Signup(nil, &handle, password)
122 require.NoError(t, err)
123
124 err = us.CheckHandle(handle)
125 assert.ErrorIs(t, err, user.ErrHandleExists)
126}
127
128func TestCheckHandle_ValidAvailable(t *testing.T) {
129 us, cleanup := setupTestService(t)
130 defer cleanup()
131
132 err := us.CheckHandle("abc")
133 assert.NoError(t, err)
134}
135
136func TestNewService_NonExistentPath(t *testing.T) {
137 us, err := NewService(WithBasePath("/nonexistent/path/that/does/not/exist"))
138 assert.ErrorIs(t, err, ErrPathNotExist)
139 assert.Nil(t, us)
140}
141
142func TestLogin_WithEmail(t *testing.T) {
143 us, cleanup := setupTestService(t)
144 defer cleanup()
145
146 email := "[email protected]"
147 handle := "testuser"
148 password := "securePassword123"
149
150 userID, err := us.Signup(&email, &handle, password)
151 require.NoError(t, err)
152
153 loginID, err := us.Login(email, password)
154 assert.NoError(t, err)
155 assert.Equal(t, userID, loginID)
156}
157
158func TestLogin_WithHandle(t *testing.T) {
159 us, cleanup := setupTestService(t)
160 defer cleanup()
161
162 email := "[email protected]"
163 handle := "testuser"
164 password := "securePassword123"
165
166 userID, err := us.Signup(&email, &handle, password)
167 require.NoError(t, err)
168
169 loginID, err := us.Login(handle, password)
170 assert.NoError(t, err)
171 assert.Equal(t, userID, loginID)
172}
173
174func TestShutdown(t *testing.T) {
175 us, _ := setupTestService(t)
176 assert.NoError(t, us.Shutdown())
177}
178
179func TestSignupWithExistingSession(t *testing.T) {
180 us, cleanup := setupTestService(t)
181 defer cleanup()
182
183 // Create an anonymous user
184 anonID := id.New()
185 assert.NoError(t, user.Set(us.DB(), anonID))
186
187 // Verify anonymous
188 isAnon, err := user.IsAnonymous(us.DB(), anonID)
189 assert.NoError(t, err)
190 assert.True(t, isAnon)
191
192 // Signup using the anonymous session
193 email := "[email protected]"
194 handle := "testhandle"
195 password := "secret123"
196 userID, err := us.SignupWithExistingSession(&email, &handle, password, anonID)
197 assert.NoError(t, err)
198 assert.Equal(t, anonID, userID)
199
200 // Verify the user is now registered (same ID)
201 u, err := us.GetUserByID(userID)
202 assert.NoError(t, err)
203 assert.Equal(t, anonID, u.ID)
204 assert.Equal(t, email, u.Email)
205 assert.Equal(t, handle, u.Handle)
206 assert.True(t, u.Registered)
207
208 // Verify no extra user rows were created
209 var count int
210 err = us.DB().QueryRow("SELECT COUNT(*) FROM users").Scan(&count)
211 assert.NoError(t, err)
212 assert.Equal(t, 1, count)
213}
214
215func TestSignupWithExistingSession_NotAnonymous(t *testing.T) {
216 us, cleanup := setupTestService(t)
217 defer cleanup()
218
219 // Create a registered user
220 regID := id.New()
221 email := "[email protected]"
222 handle := "existing"
223 password := "password123"
224 assert.NoError(t, user.Set(us.DB(), regID, user.Email(email), user.Handle(handle), user.Password(password)))
225
226 // Try to sign up with the registered user ID — should fail
227 newEmail := "[email protected]"
228 newHandle := "newhandle"
229 userID, err := us.SignupWithExistingSession(&newEmail, &newHandle, "newpass", regID)
230 assert.ErrorIs(t, err, ErrNotAnonymous)
231 assert.Equal(t, id.NotExist, userID)
232}
233
234func TestSignupWithExistingSession_UserNotFound(t *testing.T) {
235 us, cleanup := setupTestService(t)
236 defer cleanup()
237
238 // Try to sign up with a non-existent user ID
239 nonExistentID := id.New()
240 email := "[email protected]"
241 handle := "testhandle"
242 userID, err := us.SignupWithExistingSession(&email, &handle, "password", nonExistentID)
243 assert.ErrorIs(t, err, ErrUserNotFound)
244 assert.Equal(t, id.NotExist, userID)
245}
246
247func TestSignupWithExistingSession_PreservesPermissions(t *testing.T) {
248 us, cleanup := setupTestService(t)
249 defer cleanup()
250
251 // Create an anonymous user
252 anonID := id.New()
253 assert.NoError(t, user.Set(us.DB(), anonID))
254
255 // Grant them Admin permission on a document
256 docID := id.Key(42)
257 assert.NoError(t, us.SetDocumentPermission(docID, anonID, abac.Admin))
258
259 // Sign up using the anonymous session
260 email := "[email protected]"
261 handle := "testhandle"
262 password := "secret123"
263 userID, err := us.SignupWithExistingSession(&email, &handle, password, anonID)
264 assert.NoError(t, err)
265 assert.Equal(t, anonID, userID)
266
267 // Verify the permission still exists for the same user ID
268 perm, err := us.GetDocumentPermission(docID, anonID)
269 assert.NoError(t, err)
270 assert.True(t, abac.Has(perm, abac.Admin))
271}
272
273func TestLoginWithExistingSession(t *testing.T) {
274 us, cleanup := setupTestService(t)
275 defer cleanup()
276
277 // Create a registered user to log in as
278 regID := id.New()
279 email := "[email protected]"
280 handle := "existing"
281 password := "password123"
282 assert.NoError(t, user.Set(us.DB(), regID, user.Email(email), user.Handle(handle), user.Password(password)))
283
284 // Create an anonymous user
285 anonID := id.New()
286 assert.NoError(t, user.Set(us.DB(), anonID))
287
288 // Give the anonymous user admin permission on a document
289 docID := id.Key(42)
290 assert.NoError(t, us.SetDocumentPermission(docID, anonID, abac.Admin))
291
292 // Verify the permission belongs to the anonymous user
293 perm, err := us.GetDocumentPermission(docID, anonID)
294 assert.NoError(t, err)
295 assert.True(t, abac.Has(perm, abac.Admin))
296
297 // Log in with the anonymous session
298 loggedInID, err := us.LoginWithExistingSession(email, password, anonID)
299 assert.NoError(t, err)
300 assert.Equal(t, regID, loggedInID)
301
302 // Verify the permission migrated to the registered user
303 perm, err = us.GetDocumentPermission(docID, regID)
304 assert.NoError(t, err)
305 assert.True(t, abac.Has(perm, abac.Admin))
306
307 // Verify the anonymous user no longer has the permission
308 perm, err = us.GetDocumentPermission(docID, anonID)
309 assert.False(t, abac.Has(perm, abac.Admin))
310
311 // Verify the anonymous user is deleted
312 var count int
313 err = us.DB().QueryRow("SELECT COUNT(*) FROM users WHERE id = ?", int64(anonID)).Scan(&count)
314 assert.NoError(t, err)
315 assert.Equal(t, 0, count)
316}
317
318func TestLoginWithExistingSession_NotAnonymous(t *testing.T) {
319 us, cleanup := setupTestService(t)
320 defer cleanup()
321
322 // Create a registered user
323 regID := id.New()
324 email := "[email protected]"
325 handle := "existing"
326 password := "password123"
327 assert.NoError(t, user.Set(us.DB(), regID, user.Email(email), user.Handle(handle), user.Password(password)))
328
329 // Try to use the registered user as the anonymous parameter — should fail
330 loggedInID, err := us.LoginWithExistingSession(email, password, regID)
331 assert.ErrorIs(t, err, ErrNotAnonymous)
332 assert.Equal(t, id.NotExist, loggedInID)
333}
334
335func TestLoginWithExistingSession_BadCredentials(t *testing.T) {
336 us, cleanup := setupTestService(t)
337 defer cleanup()
338
339 // Create a registered user with password "right"
340 regID := id.New()
341 email := "[email protected]"
342 password := "right"
343 assert.NoError(t, user.Set(us.DB(), regID, user.Email(email), user.Password(password)))
344
345 // Create an anonymous user
346 anonID := id.New()
347 assert.NoError(t, user.Set(us.DB(), anonID))
348
349 // Try to log in with wrong password
350 loggedInID, err := us.LoginWithExistingSession(email, "wrong", anonID)
351 assert.ErrorIs(t, err, ErrLoginFailed)
352 assert.Equal(t, id.NotExist, loggedInID)
353}
354
355func TestLoginWithExistingSessionPermissionMerge(t *testing.T) {
356 us, cleanup := setupTestService(t)
357 defer cleanup()
358
359 // Create a registered user
360 regID := id.New()
361 email := "[email protected]"
362 password := "password123"
363 assert.NoError(t, user.Set(us.DB(), regID, user.Email(email), user.Password(password)))
364
365 // Create an anonymous user
366 anonID := id.New()
367 assert.NoError(t, user.Set(us.DB(), anonID))
368
369 // Give the registered user Read permission on a document
370 docID := id.Key(99)
371 assert.NoError(t, us.SetDocumentPermission(docID, regID, abac.Read))
372
373 // Give the anonymous user Admin permission on the same document
374 assert.NoError(t, us.SetDocumentPermission(docID, anonID, abac.Admin))
375
376 // Log in — should merge permissions via bitwise OR
377 loggedInID, err := us.LoginWithExistingSession(email, password, anonID)
378 assert.NoError(t, err)
379 assert.Equal(t, regID, loggedInID)
380
381 // Verify the registered user now has Admin (merged)
382 perm, err := us.GetDocumentPermission(docID, regID)
383 assert.NoError(t, err)
384 assert.True(t, abac.Has(perm, abac.Admin))
385 assert.True(t, abac.Has(perm, abac.Read))
386
387 // Verify only one permission row remains for this resource+user
388 var count int
389 err = us.DB().QueryRow(
390 "SELECT COUNT(*) FROM permissions WHERE resource_type = ? AND resource_id = ? AND user_id = ?",
391 "document", int64(docID), int64(regID),
392 ).Scan(&count)
393 assert.NoError(t, err)
394 assert.Equal(t, 1, count)
395
396 // Verify anonymous permission row is gone
397 err = us.DB().QueryRow(
398 "SELECT COUNT(*) FROM permissions WHERE resource_type = ? AND resource_id = ? AND user_id = ?",
399 "document", int64(docID), int64(anonID),
400 ).Scan(&count)
401 assert.NoError(t, err)
402 assert.Equal(t, 0, count)
403}