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}