package usecase import ( "context" "testing" "time" "backend/internal/config" "backend/pkg/permission/domain/entity" "backend/pkg/permission/domain/token" "backend/pkg/permission/mock/repository" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) func TestTokenUseCase_NewToken(t *testing.T) { mockRepo := repository.NewMockTokenRepository(t) cfg := &config.Config{ Token: struct { AccessSecret string RefreshSecret string AccessTokenExpiry time.Duration RefreshTokenExpiry time.Duration OneTimeTokenExpiry time.Duration MaxTokensPerUser int MaxTokensPerDevice int }{ AccessSecret: "test-access-secret", RefreshSecret: "test-refresh-secret", AccessTokenExpiry: 15 * time.Minute, RefreshTokenExpiry: 7 * 24 * time.Hour, MaxTokensPerUser: 10, MaxTokensPerDevice: 5, }, } useCase := &TokenUseCase{ TokenUseCaseParam: TokenUseCaseParam{ TokenRepo: mockRepo, Config: cfg, }, } tests := []struct { name string req entity.AuthorizationReq setup func() wantErr bool }{ { name: "successful token creation", req: entity.AuthorizationReq{ GrantType: token.PasswordCredentials.ToString(), Scope: "read write", DeviceID: "device123", IsRefreshToken: true, Data: map[string]string{ "uid": "user123", "role": "user", }, }, setup: func() { mockRepo.On("Create", mock.Anything, mock.AnythingOfType("entity.Token")). Return(nil).Once() }, wantErr: false, }, { name: "repository error", req: entity.AuthorizationReq{ GrantType: token.PasswordCredentials.ToString(), Scope: "read", DeviceID: "device123", }, setup: func() { mockRepo.On("Create", mock.Anything, mock.AnythingOfType("entity.Token")). Return(assert.AnError).Once() }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.setup() resp, err := useCase.NewToken(context.Background(), tt.req) if tt.wantErr { assert.Error(t, err) assert.Empty(t, resp.AccessToken) } else { assert.NoError(t, err) assert.NotEmpty(t, resp.AccessToken) assert.Equal(t, token.TypeBearer.String(), resp.TokenType) assert.Greater(t, resp.ExpiresIn, int64(0)) if tt.req.IsRefreshToken { assert.NotEmpty(t, resp.RefreshToken) } } mockRepo.AssertExpectations(t) }) } } func TestTokenUseCase_ValidationToken(t *testing.T) { mockRepo := repository.NewMockTokenRepository(t) cfg := &config.Config{ Token: struct { AccessSecret string RefreshSecret string AccessTokenExpiry time.Duration RefreshTokenExpiry time.Duration OneTimeTokenExpiry time.Duration MaxTokensPerUser int MaxTokensPerDevice int }{ AccessSecret: "test-access-secret", RefreshSecret: "test-refresh-secret", AccessTokenExpiry: 15 * time.Minute, RefreshTokenExpiry: 7 * 24 * time.Hour, }, } useCase := &TokenUseCase{ TokenUseCaseParam: TokenUseCaseParam{ TokenRepo: mockRepo, Config: cfg, }, } // 先創建一個有效的 token 用於測試 tokenReq := entity.AuthorizationReq{ GrantType: token.PasswordCredentials.ToString(), Data: map[string]string{ "uid": "user123", "role": "user", }, } mockRepo.On("Create", mock.Anything, mock.AnythingOfType("entity.Token")). Return(nil).Once() tokenResp, err := useCase.NewToken(context.Background(), tokenReq) assert.NoError(t, err) assert.NotEmpty(t, tokenResp.AccessToken) // 測試驗證 tests := []struct { name string req entity.ValidationTokenReq setup func() wantErr bool }{ { name: "valid token", req: entity.ValidationTokenReq{ Token: tokenResp.AccessToken, }, setup: func() { mockRepo.On("GetAccessTokenByID", mock.Anything, mock.AnythingOfType("string")). Return(entity.Token{ ID: "test-id", UID: "user123", AccessToken: tokenResp.AccessToken, ExpiresIn: int(cfg.Token.AccessTokenExpiry.Seconds()), }, nil).Once() }, wantErr: false, }, { name: "invalid token", req: entity.ValidationTokenReq{ Token: "invalid-token", }, setup: func() { // parseClaims will fail for invalid token }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.setup() resp, err := useCase.ValidationToken(context.Background(), tt.req) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) assert.NotEmpty(t, resp.Token.ID) assert.Equal(t, "user123", resp.Token.UID) } mockRepo.AssertExpectations(t) }) } } func TestTokenUseCase_BlacklistToken(t *testing.T) { mockRepo := repository.NewMockTokenRepository(t) cfg := &config.Config{ Token: struct { AccessSecret string RefreshSecret string AccessTokenExpiry time.Duration RefreshTokenExpiry time.Duration OneTimeTokenExpiry time.Duration MaxTokensPerUser int MaxTokensPerDevice int }{ AccessSecret: "test-access-secret", RefreshSecret: "test-refresh-secret", AccessTokenExpiry: 15 * time.Minute, RefreshTokenExpiry: 7 * 24 * time.Hour, }, } useCase := &TokenUseCase{ TokenUseCaseParam: TokenUseCaseParam{ TokenRepo: mockRepo, Config: cfg, }, } // 先創建一個有效的 token tokenReq := entity.AuthorizationReq{ GrantType: token.PasswordCredentials.ToString(), Data: map[string]string{ "uid": "user123", "role": "user", }, } mockRepo.On("Create", mock.Anything, mock.AnythingOfType("entity.Token")). Return(nil).Once() tokenResp, err := useCase.NewToken(context.Background(), tokenReq) assert.NoError(t, err) tests := []struct { name string token string reason string setup func() wantErr bool }{ { name: "successful blacklist", token: tokenResp.AccessToken, reason: "user logout", setup: func() { mockRepo.On("AddToBlacklist", mock.Anything, mock.AnythingOfType("*entity.BlacklistEntry"), mock.AnythingOfType("time.Duration")). Return(nil).Once() }, wantErr: false, }, { name: "invalid token", token: "invalid-token", reason: "test", setup: func() {}, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.setup() err := useCase.BlacklistToken(context.Background(), tt.token, tt.reason) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) } mockRepo.AssertExpectations(t) }) } } func TestTokenUseCase_IsTokenBlacklisted(t *testing.T) { mockRepo := repository.NewMockTokenRepository(t) cfg := &config.Config{ Token: struct { AccessSecret string RefreshSecret string AccessTokenExpiry time.Duration RefreshTokenExpiry time.Duration OneTimeTokenExpiry time.Duration MaxTokensPerUser int MaxTokensPerDevice int }{ AccessSecret: "test-secret", }, } useCase := &TokenUseCase{ TokenUseCaseParam: TokenUseCaseParam{ TokenRepo: mockRepo, Config: cfg, }, } tests := []struct { name string jti string setup func() wantResult bool wantErr bool }{ { name: "token is blacklisted", jti: "test-jti-123", setup: func() { mockRepo.On("IsBlacklisted", mock.Anything, "test-jti-123"). Return(true, nil).Once() }, wantResult: true, wantErr: false, }, { name: "token is not blacklisted", jti: "test-jti-456", setup: func() { mockRepo.On("IsBlacklisted", mock.Anything, "test-jti-456"). Return(false, nil).Once() }, wantResult: false, wantErr: false, }, { name: "repository error", jti: "test-jti-error", setup: func() { mockRepo.On("IsBlacklisted", mock.Anything, "test-jti-error"). Return(false, assert.AnError).Once() }, wantResult: false, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.setup() result, err := useCase.IsTokenBlacklisted(context.Background(), tt.jti) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) assert.Equal(t, tt.wantResult, result) } mockRepo.AssertExpectations(t) }) } } func TestTokenUseCase_CancelTokens(t *testing.T) { mockRepo := repository.NewMockTokenRepository(t) cfg := &config.Config{} useCase := &TokenUseCase{ TokenUseCaseParam: TokenUseCaseParam{ TokenRepo: mockRepo, Config: cfg, }, } tests := []struct { name string req entity.DoTokenByUIDReq setup func() wantErr bool }{ { name: "cancel by UID", req: entity.DoTokenByUIDReq{ UID: "user123", }, setup: func() { mockRepo.On("DeleteAccessTokensByUID", mock.Anything, "user123"). Return(nil).Once() }, wantErr: false, }, { name: "cancel by token IDs", req: entity.DoTokenByUIDReq{ IDs: []string{"token1", "token2"}, }, setup: func() { mockRepo.On("DeleteAccessTokenByID", mock.Anything, []string{"token1", "token2"}). Return(nil).Once() }, wantErr: false, }, { name: "repository error", req: entity.DoTokenByUIDReq{ UID: "user123", }, setup: func() { mockRepo.On("DeleteAccessTokensByUID", mock.Anything, "user123"). Return(assert.AnError).Once() }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.setup() err := useCase.CancelTokens(context.Background(), tt.req) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) } mockRepo.AssertExpectations(t) }) } }