@@ -11,7 +11,9 @@ import (
1111
1212 domainuser "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/domain/user"
1313 "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/infra/config"
14+ "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/pkg/secretbox"
1415 "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/repository"
16+ "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/shared/requestmeta"
1517)
1618
1719func boolPtr (value bool ) * bool {
@@ -135,13 +137,11 @@ func TestResolveProviderUserLoginRequiresRegistrationEnabledForNewAccount(t *tes
135137 }
136138}
137139
138- func TestResolveProviderUserAutoLinksVerifiedEmailBeforeProvisioning (t * testing.T ) {
139- now := time .Now ()
140+ func TestResolveProviderUserAutoLinksVerifiedProviderEmailBeforeProvisioning (t * testing.T ) {
140141 existing := & domainuser.User {
141- ID : 42 ,
142- Email : "verified@example.com" ,
143- EmailVerifiedAt : & now ,
144- Status : domainuser .StatusActive ,
142+ ID : 42 ,
143+ Email : "verified@example.com" ,
144+ Status : domainuser .StatusActive ,
145145 }
146146 repo := & providerLoginRepo {usersByEmail : map [string ]* domainuser.User {existing .Email : existing }}
147147 service := NewService (config.Config {JWTSecret : "test-secret" , AutoLinkVerifiedEmail : true }, repo , nil )
@@ -170,6 +170,124 @@ func TestResolveProviderUserAutoLinksVerifiedEmailBeforeProvisioning(t *testing.
170170 }
171171}
172172
173+ func TestResolveProviderUserNormalizesProviderEmailBeforeAutoLink (t * testing.T ) {
174+ existing := & domainuser.User {
175+ ID : 42 ,
176+ Email : "verified@example.com" ,
177+ Status : domainuser .StatusActive ,
178+ }
179+ repo := & providerLoginRepo {usersByEmail : map [string ]* domainuser.User {existing .Email : existing }}
180+ service := NewService (config.Config {JWTSecret : "test-secret" , AutoLinkVerifiedEmail : true }, repo , nil )
181+ provider := domainuser.IdentityProvider {
182+ ID : 10 ,
183+ Type : domainuser .IdentityProviderTypeOIDC ,
184+ Name : "Acme SSO" ,
185+ Slug : "acme" ,
186+ LoginEnabled : true ,
187+ RegistrationEnabled : false ,
188+ DefaultRole : domainuser .RoleUser ,
189+ }
190+
191+ userItem , err := service .resolveProviderUser (context .Background (), provider , "sub-1" , "Verified@Example.com" , "Verified User" , "" , true , `{"sub":"sub-1"}` , providerIntentLogin )
192+ if err != nil {
193+ t .Fatalf ("expected normalized provider email to auto-link, got %v" , err )
194+ }
195+ if userItem .ID != existing .ID {
196+ t .Fatalf ("expected existing user %d, got %d" , existing .ID , userItem .ID )
197+ }
198+ if len (repo .identities ) != 1 || repo .identities [0 ].Email != existing .Email {
199+ t .Fatalf ("expected normalized linked identity email, got %#v" , repo .identities )
200+ }
201+ }
202+
203+ func TestResolveProviderEmailVerifiedUsesConfiguredField (t * testing.T ) {
204+ provider := domainuser.IdentityProvider {EmailVerifiedField : "verified" }
205+ profile := map [string ]interface {}{
206+ "email" : "verified@example.com" ,
207+ "verified" : true ,
208+ }
209+
210+ if ! resolveProviderEmailVerified (profile , provider ) {
211+ t .Fatalf ("expected configured email verified field to be recognized" )
212+ }
213+ }
214+
215+ func TestCompleteProviderBindAllowsSameAccountWithoutProviderEmailVerification (t * testing.T ) {
216+ dataKey := "test-data-key"
217+ clientSecret , err := secretbox .EncryptString (dataKey , "client-secret" )
218+ if err != nil {
219+ t .Fatalf ("encrypt client secret: %v" , err )
220+ }
221+ server := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
222+ w .Header ().Set ("Content-Type" , "application/json" )
223+ switch r .URL .Path {
224+ case "/token" :
225+ _ , _ = w .Write ([]byte (`{"access_token":"access-token","token_type":"Bearer"}` ))
226+ case "/userinfo" :
227+ if r .Header .Get ("Authorization" ) != "Bearer access-token" {
228+ t .Fatalf ("unexpected authorization header %q" , r .Header .Get ("Authorization" ))
229+ }
230+ _ , _ = w .Write ([]byte (`{"sub":"sub-1","email":"user@example.com","name":"Provider User"}` ))
231+ default :
232+ http .NotFound (w , r )
233+ }
234+ }))
235+ defer server .Close ()
236+
237+ provider := & domainuser.IdentityProvider {
238+ ID : 10 ,
239+ Type : domainuser .IdentityProviderTypeOAuth2 ,
240+ Name : "Acme SSO" ,
241+ Slug : "acme" ,
242+ LoginEnabled : true ,
243+ RegistrationEnabled : false ,
244+ ClientID : "client" ,
245+ ClientSecret : clientSecret ,
246+ AuthURL : server .URL + "/auth" ,
247+ TokenURL : server .URL + "/token" ,
248+ UserInfoURL : server .URL + "/userinfo" ,
249+ SubjectField : "sub" ,
250+ EmailField : "email" ,
251+ EmailVerifiedField : "email_verified" ,
252+ NameField : "name" ,
253+ AvatarField : "picture" ,
254+ }
255+ repo := & providerLoginRepo {
256+ providersBySlug : map [string ]* domainuser.IdentityProvider {"acme" : provider },
257+ usersByEmail : map [string ]* domainuser.User {
258+ "user@example.com" : {ID : 42 , Email : "user@example.com" , Status : domainuser .StatusActive },
259+ },
260+ }
261+ service := NewService (config.Config {
262+ JWTSecret : "test-secret" ,
263+ DataEncryptionKey : dataKey ,
264+ ThirdPartyLoginEnabled : true ,
265+ }, repo , nil )
266+ redirectURI := "http://localhost/auth/callback?provider=acme"
267+ codeVerifier := strings .Repeat ("a" , 43 )
268+ state , err := service .signProviderState (providerOAuthState {
269+ Provider : "acme" ,
270+ RedirectURI : redirectURI ,
271+ Intent : providerIntentBind ,
272+ CodeChallenge : providerCodeChallenge (codeVerifier ),
273+ ExpiresAt : time .Now ().Add (time .Minute ).Unix (),
274+ })
275+ if err != nil {
276+ t .Fatalf ("sign provider state: %v" , err )
277+ }
278+
279+ identity , err := service .CompleteProviderBind (context .Background (), 42 , "acme" , "code" , state , redirectURI , codeVerifier , "request-id" , requestmeta.SessionAuditContext {})
280+ if err != nil {
281+ t .Fatalf ("expected manual bind to succeed without provider email verification claim, got %v" , err )
282+ }
283+ if identity .EmailVerified {
284+ t .Fatalf ("expected linked identity to remain unverified" )
285+ }
286+ if len (repo .identities ) != 1 || repo .identities [0 ].UserID != 42 || repo .identities [0 ].EmailVerified {
287+ t .Fatalf ("expected identity linked to current user without verified email, got %#v" , repo .identities )
288+ }
289+ }
290+
173291func TestResolveProviderUserRejectsInactiveBoundUserWithoutUpdatingIdentity (t * testing.T ) {
174292 repo := & providerLoginRepo {
175293 usersByID : map [uint ]* domainuser.User {
@@ -549,6 +667,10 @@ func (r *providerLoginRepo) UpdateUserIdentityLogin(ctx context.Context, identit
549667 return nil
550668}
551669
670+ func (r * providerLoginRepo ) RecordAuthEvent (ctx context.Context , userID uint , requestID string , eventType string , result string , reason string , clientIP string , userAgent string , detailJSON string ) error {
671+ return nil
672+ }
673+
552674func (r * providerLoginRepo ) DeleteAccountHard (ctx context.Context , userID uint ) error {
553675 r .deletedUserID = userID
554676 return nil
0 commit comments