Skip to content

Commit 15a4c8c

Browse files
committed
add tests
1 parent e4cf9be commit 15a4c8c

2 files changed

Lines changed: 344 additions & 2 deletions

File tree

internal/controller/user_controller_test.go

Lines changed: 150 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@ import (
1111

1212
"github.com/gin-gonic/gin"
1313
"github.com/pquerna/otp/totp"
14+
"github.com/stretchr/testify/assert"
15+
"github.com/stretchr/testify/require"
16+
1417
"github.com/steveiliop56/tinyauth/internal/bootstrap"
1518
"github.com/steveiliop56/tinyauth/internal/config"
1619
"github.com/steveiliop56/tinyauth/internal/controller"
1720
"github.com/steveiliop56/tinyauth/internal/repository"
1821
"github.com/steveiliop56/tinyauth/internal/service"
1922
"github.com/steveiliop56/tinyauth/internal/utils/tlog"
20-
"github.com/stretchr/testify/assert"
21-
"github.com/stretchr/testify/require"
2223
)
2324

2425
func TestUserController(t *testing.T) {
@@ -353,3 +354,150 @@ func TestUserController(t *testing.T) {
353354
require.NoError(t, err)
354355
})
355356
}
357+
358+
func TestUserControllerAttributes(t *testing.T) {
359+
tlog.NewTestLogger().Init()
360+
tempDir := t.TempDir()
361+
362+
authServiceCfg := service.AuthServiceConfig{
363+
Users: []config.User{
364+
{
365+
Username: "attruser",
366+
Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
367+
Attributes: config.UserAttributes{
368+
Name: "Alice Smith",
369+
Email: "alice@example.com",
370+
},
371+
},
372+
{
373+
Username: "attrtotpuser",
374+
Password: "$2a$10$ZwVYQH07JX2zq7Fjkt3gU.BjwvvwPeli4OqOno04RQIv0P7usBrXa", // password
375+
TotpSecret: "JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK",
376+
Attributes: config.UserAttributes{
377+
Name: "Bob Jones",
378+
Email: "bob@example.com",
379+
},
380+
},
381+
},
382+
SessionExpiry: 10,
383+
CookieDomain: "example.com",
384+
LoginTimeout: 10,
385+
LoginMaxRetries: 3,
386+
SessionCookieName: "tinyauth-session",
387+
}
388+
389+
userControllerCfg := controller.UserControllerConfig{
390+
CookieDomain: "example.com",
391+
}
392+
393+
app := bootstrap.NewBootstrapApp(config.Config{})
394+
db, err := app.SetupDatabase(path.Join(tempDir, "tinyauth_attrs.db"))
395+
require.NoError(t, err)
396+
397+
queries := repository.New(db)
398+
399+
docker := service.NewDockerService()
400+
err = docker.Init()
401+
require.NoError(t, err)
402+
403+
ldap := service.NewLdapService(service.LdapServiceConfig{})
404+
err = ldap.Init()
405+
require.NoError(t, err)
406+
407+
broker := service.NewOAuthBrokerService(make(map[string]config.OAuthServiceConfig))
408+
err = broker.Init()
409+
require.NoError(t, err)
410+
411+
authService := service.NewAuthService(authServiceCfg, docker, ldap, queries, broker)
412+
err = authService.Init()
413+
require.NoError(t, err)
414+
415+
makeRouter := func(extraMiddlewares ...gin.HandlerFunc) *gin.Engine {
416+
router := gin.Default()
417+
for _, m := range extraMiddlewares {
418+
router.Use(m)
419+
}
420+
gin.SetMode(gin.TestMode)
421+
group := router.Group("/api")
422+
ctrl := controller.NewUserController(userControllerCfg, group, authService)
423+
ctrl.SetupRoutes()
424+
return router
425+
}
426+
427+
t.Run("Login uses name and email from user attributes", func(t *testing.T) {
428+
authService.ClearRateLimitsTestingOnly()
429+
router := makeRouter()
430+
431+
loginReq := controller.LoginRequest{Username: "attruser", Password: "password"}
432+
body, err := json.Marshal(loginReq)
433+
require.NoError(t, err)
434+
435+
req := httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(body)))
436+
req.Header.Set("Content-Type", "application/json")
437+
rec := httptest.NewRecorder()
438+
router.ServeHTTP(rec, req)
439+
440+
require.Equal(t, 200, rec.Code)
441+
cookies := rec.Result().Cookies()
442+
require.Len(t, cookies, 1)
443+
assert.Equal(t, "tinyauth-session", cookies[0].Name)
444+
})
445+
446+
t.Run("Login with TOTP uses name and email from user attributes in pending session", func(t *testing.T) {
447+
authService.ClearRateLimitsTestingOnly()
448+
router := makeRouter()
449+
450+
loginReq := controller.LoginRequest{Username: "attrtotpuser", Password: "password"}
451+
body, err := json.Marshal(loginReq)
452+
require.NoError(t, err)
453+
454+
req := httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(body)))
455+
req.Header.Set("Content-Type", "application/json")
456+
rec := httptest.NewRecorder()
457+
router.ServeHTTP(rec, req)
458+
459+
require.Equal(t, 200, rec.Code)
460+
var res map[string]any
461+
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &res))
462+
assert.Equal(t, true, res["totpPending"])
463+
require.Len(t, rec.Result().Cookies(), 1)
464+
})
465+
466+
t.Run("TOTP completion uses name and email from user attributes", func(t *testing.T) {
467+
authService.ClearRateLimitsTestingOnly()
468+
469+
// First: login to get TOTP-pending session
470+
router := makeRouter(func(c *gin.Context) {
471+
c.Set("context", &config.UserContext{
472+
Username: "attrtotpuser",
473+
Name: "Bob Jones",
474+
Email: "bob@example.com",
475+
Provider: "local",
476+
TotpPending: true,
477+
TotpEnabled: true,
478+
})
479+
})
480+
481+
code, err := totp.GenerateCode("JPIEBDKJH6UGWJMX66RR3S55UFP2SGKK", time.Now())
482+
require.NoError(t, err)
483+
484+
totpReq := controller.TotpRequest{Code: code}
485+
body, err := json.Marshal(totpReq)
486+
require.NoError(t, err)
487+
488+
req := httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(body)))
489+
req.Header.Set("Content-Type", "application/json")
490+
rec := httptest.NewRecorder()
491+
router.ServeHTTP(rec, req)
492+
493+
require.Equal(t, 200, rec.Code)
494+
cookies := rec.Result().Cookies()
495+
require.Len(t, cookies, 1)
496+
assert.Equal(t, "tinyauth-session", cookies[0].Name)
497+
})
498+
499+
t.Cleanup(func() {
500+
err = db.Close()
501+
require.NoError(t, err)
502+
})
503+
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package service_test
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/steveiliop56/tinyauth/internal/config"
11+
"github.com/steveiliop56/tinyauth/internal/repository"
12+
"github.com/steveiliop56/tinyauth/internal/service"
13+
)
14+
15+
func newTestUser() repository.OidcUserinfo {
16+
addr := config.AddressClaim{
17+
Formatted: "123 Main St",
18+
StreetAddress: "123 Main St",
19+
Locality: "Springfield",
20+
Region: "IL",
21+
PostalCode: "62701",
22+
Country: "US",
23+
}
24+
addrJSON, _ := json.Marshal(addr)
25+
26+
return repository.OidcUserinfo{
27+
Sub: "test-sub",
28+
Name: "Test User",
29+
PreferredUsername: "testuser",
30+
Email: "test@example.com",
31+
Groups: "admins,users",
32+
UpdatedAt: 1234567890,
33+
GivenName: "Test",
34+
FamilyName: "User",
35+
MiddleName: "M",
36+
Nickname: "testy",
37+
Profile: "https://example.com/testuser",
38+
Picture: "https://example.com/testuser.jpg",
39+
Website: "https://testuser.example.com",
40+
Gender: "male",
41+
Birthdate: "1990-01-01",
42+
Zoneinfo: "America/Chicago",
43+
Locale: "en-US",
44+
PhoneNumber: "+15555550100",
45+
PhoneNumberVerified: true,
46+
Address: string(addrJSON),
47+
}
48+
}
49+
50+
func newOIDCService(t *testing.T) *service.OIDCService {
51+
t.Helper()
52+
dir := t.TempDir()
53+
svc := service.NewOIDCService(service.OIDCServiceConfig{
54+
PrivateKeyPath: dir + "/key.pem",
55+
PublicKeyPath: dir + "/key.pub",
56+
Issuer: "https://tinyauth.example.com",
57+
SessionExpiry: 3600,
58+
}, nil)
59+
require.NoError(t, svc.Init())
60+
return svc
61+
}
62+
63+
func TestCompileUserinfo_OpenidOnly(t *testing.T) {
64+
svc := newOIDCService(t)
65+
user := newTestUser()
66+
67+
info := svc.CompileUserinfo(user, "openid")
68+
69+
assert.Equal(t, "test-sub", info.Sub)
70+
assert.Equal(t, int64(1234567890), info.UpdatedAt)
71+
// profile fields not requested
72+
assert.Empty(t, info.Name)
73+
assert.Empty(t, info.Email)
74+
assert.Nil(t, info.Groups)
75+
assert.Nil(t, info.PhoneNumberVerified)
76+
assert.Nil(t, info.Address)
77+
}
78+
79+
func TestCompileUserinfo_ProfileScope(t *testing.T) {
80+
svc := newOIDCService(t)
81+
user := newTestUser()
82+
83+
info := svc.CompileUserinfo(user, "openid,profile")
84+
85+
assert.Equal(t, "Test User", info.Name)
86+
assert.Equal(t, "testuser", info.PreferredUsername)
87+
assert.Equal(t, "Test", info.GivenName)
88+
assert.Equal(t, "User", info.FamilyName)
89+
assert.Equal(t, "M", info.MiddleName)
90+
assert.Equal(t, "testy", info.Nickname)
91+
assert.Equal(t, "https://example.com/testuser", info.Profile)
92+
assert.Equal(t, "https://example.com/testuser.jpg", info.Picture)
93+
assert.Equal(t, "https://testuser.example.com", info.Website)
94+
assert.Equal(t, "male", info.Gender)
95+
assert.Equal(t, "1990-01-01", info.Birthdate)
96+
assert.Equal(t, "America/Chicago", info.Zoneinfo)
97+
assert.Equal(t, "en-US", info.Locale)
98+
// non-profile fields still absent
99+
assert.Empty(t, info.Email)
100+
}
101+
102+
func TestCompileUserinfo_EmailScope(t *testing.T) {
103+
svc := newOIDCService(t)
104+
user := newTestUser()
105+
106+
info := svc.CompileUserinfo(user, "openid,email")
107+
108+
assert.Equal(t, "test@example.com", info.Email)
109+
assert.True(t, info.EmailVerified)
110+
assert.Empty(t, info.Name) // profile not requested
111+
}
112+
113+
func TestCompileUserinfo_PhoneScope(t *testing.T) {
114+
svc := newOIDCService(t)
115+
user := newTestUser()
116+
117+
info := svc.CompileUserinfo(user, "openid,phone")
118+
119+
assert.Equal(t, "+15555550100", info.PhoneNumber)
120+
require.NotNil(t, info.PhoneNumberVerified)
121+
assert.True(t, *info.PhoneNumberVerified)
122+
}
123+
124+
func TestCompileUserinfo_PhoneScope_Unverified(t *testing.T) {
125+
svc := newOIDCService(t)
126+
user := newTestUser()
127+
user.PhoneNumberVerified = false
128+
129+
info := svc.CompileUserinfo(user, "openid,phone")
130+
131+
require.NotNil(t, info.PhoneNumberVerified)
132+
assert.False(t, *info.PhoneNumberVerified)
133+
}
134+
135+
func TestCompileUserinfo_AddressScope(t *testing.T) {
136+
svc := newOIDCService(t)
137+
user := newTestUser()
138+
139+
info := svc.CompileUserinfo(user, "openid,address")
140+
141+
require.NotNil(t, info.Address)
142+
assert.Equal(t, "123 Main St", info.Address.Formatted)
143+
assert.Equal(t, "123 Main St", info.Address.StreetAddress)
144+
assert.Equal(t, "Springfield", info.Address.Locality)
145+
assert.Equal(t, "IL", info.Address.Region)
146+
assert.Equal(t, "62701", info.Address.PostalCode)
147+
assert.Equal(t, "US", info.Address.Country)
148+
}
149+
150+
func TestCompileUserinfo_AddressScope_InvalidJSON(t *testing.T) {
151+
svc := newOIDCService(t)
152+
user := newTestUser()
153+
user.Address = "not-valid-json"
154+
155+
info := svc.CompileUserinfo(user, "openid,address")
156+
157+
// invalid JSON silently skipped, address omitted
158+
assert.Nil(t, info.Address)
159+
}
160+
161+
func TestCompileUserinfo_GroupsScope(t *testing.T) {
162+
svc := newOIDCService(t)
163+
user := newTestUser()
164+
165+
info := svc.CompileUserinfo(user, "openid,groups")
166+
167+
assert.Equal(t, []string{"admins", "users"}, info.Groups)
168+
}
169+
170+
func TestCompileUserinfo_GroupsScope_Empty(t *testing.T) {
171+
svc := newOIDCService(t)
172+
user := newTestUser()
173+
user.Groups = ""
174+
175+
info := svc.CompileUserinfo(user, "openid,groups")
176+
177+
assert.Equal(t, []string{}, info.Groups)
178+
}
179+
180+
func TestCompileUserinfo_AllScopes(t *testing.T) {
181+
svc := newOIDCService(t)
182+
user := newTestUser()
183+
184+
info := svc.CompileUserinfo(user, "openid,profile,email,phone,address,groups")
185+
186+
assert.Equal(t, "Test User", info.Name)
187+
assert.Equal(t, "test@example.com", info.Email)
188+
assert.Equal(t, "+15555550100", info.PhoneNumber)
189+
require.NotNil(t, info.PhoneNumberVerified)
190+
assert.True(t, *info.PhoneNumberVerified)
191+
require.NotNil(t, info.Address)
192+
assert.Equal(t, "Springfield", info.Address.Locality)
193+
assert.Equal(t, []string{"admins", "users"}, info.Groups)
194+
}

0 commit comments

Comments
 (0)