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_RefreshToken(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, }, } // Create a base token first tokenReq := entity.AuthorizationReq{ GrantType: token.PasswordCredentials.ToString(), Data: map[string]string{ "uid": "user123", "role": "user", }, IsRefreshToken: true, } 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 req entity.RefreshTokenReq setup func() wantErr bool }{ { name: "successful token refresh", req: entity.RefreshTokenReq{ Token: tokenResp.RefreshToken, Scope: "read write", DeviceID: "device123", }, setup: func() { existingToken := entity.Token{ ID: "old-token-id", UID: "user123", AccessToken: tokenResp.AccessToken, ExpiresIn: int(time.Now().Add(time.Hour).Unix()), } mockRepo.On("GetAccessTokenByOneTimeToken", mock.Anything, tokenResp.RefreshToken). Return(existingToken, nil).Once() mockRepo.On("Create", mock.Anything, mock.AnythingOfType("entity.Token")). Return(nil).Once() mockRepo.On("Delete", mock.Anything, mock.AnythingOfType("entity.Token")). Return(nil).Once() }, wantErr: false, }, { name: "invalid refresh token", req: entity.RefreshTokenReq{ Token: "invalid-refresh-token", Scope: "read", DeviceID: "device123", }, setup: func() { mockRepo.On("GetAccessTokenByOneTimeToken", mock.Anything, "invalid-refresh-token"). Return(entity.Token{}, assert.AnError).Once() }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.setup() resp, err := useCase.RefreshToken(context.Background(), tt.req) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) assert.NotEmpty(t, resp.Token) assert.NotEmpty(t, resp.OneTimeToken) assert.Equal(t, token.TypeBearer.String(), resp.TokenType) } mockRepo.AssertExpectations(t) }) } } func TestTokenUseCase_GetUserTokensByUID(t *testing.T) { mockRepo := repository.NewMockTokenRepository(t) cfg := &config.Config{} useCase := &TokenUseCase{ TokenUseCaseParam: TokenUseCaseParam{ TokenRepo: mockRepo, Config: cfg, }, } tests := []struct { name string req entity.QueryTokenByUIDReq setup func() wantErr bool }{ { name: "get tokens successfully", req: entity.QueryTokenByUIDReq{ UID: "user123", }, setup: func() { tokens := []entity.Token{ { ID: "token1", UID: "user123", AccessToken: "access1", ExpiresIn: 3600, }, { ID: "token2", UID: "user123", AccessToken: "access2", ExpiresIn: 3600, }, } mockRepo.On("GetAccessTokensByUID", mock.Anything, "user123"). Return(tokens, nil).Once() }, wantErr: false, }, { name: "repository error", req: entity.QueryTokenByUIDReq{ UID: "user456", }, setup: func() { mockRepo.On("GetAccessTokensByUID", mock.Anything, "user456"). Return([]entity.Token(nil), assert.AnError).Once() }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.setup() tokens, err := useCase.GetUserTokensByUID(context.Background(), tt.req) if tt.wantErr { assert.Error(t, err) assert.Nil(t, tokens) } else { assert.NoError(t, err) assert.NotNil(t, tokens) assert.Greater(t, len(tokens), 0) } mockRepo.AssertExpectations(t) }) } } func TestTokenUseCase_GetUserTokensByDeviceID(t *testing.T) { mockRepo := repository.NewMockTokenRepository(t) cfg := &config.Config{} useCase := &TokenUseCase{ TokenUseCaseParam: TokenUseCaseParam{ TokenRepo: mockRepo, Config: cfg, }, } tests := []struct { name string req entity.DoTokenByDeviceIDReq setup func() wantErr bool }{ { name: "get tokens by device successfully", req: entity.DoTokenByDeviceIDReq{ DeviceID: "device123", }, setup: func() { tokens := []entity.Token{ { ID: "token1", UID: "user123", DeviceID: "device123", AccessToken: "access1", ExpiresIn: 3600, }, } mockRepo.On("GetAccessTokensByDeviceID", mock.Anything, "device123"). Return(tokens, nil).Once() }, wantErr: false, }, { name: "repository error", req: entity.DoTokenByDeviceIDReq{ DeviceID: "device456", }, setup: func() { mockRepo.On("GetAccessTokensByDeviceID", mock.Anything, "device456"). Return([]entity.Token(nil), assert.AnError).Once() }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.setup() tokens, err := useCase.GetUserTokensByDeviceID(context.Background(), tt.req) if tt.wantErr { assert.Error(t, err) assert.Nil(t, tokens) } else { assert.NoError(t, err) assert.NotNil(t, tokens) } mockRepo.AssertExpectations(t) }) } } func TestTokenUseCase_CancelTokenByDeviceID(t *testing.T) { mockRepo := repository.NewMockTokenRepository(t) cfg := &config.Config{} useCase := &TokenUseCase{ TokenUseCaseParam: TokenUseCaseParam{ TokenRepo: mockRepo, Config: cfg, }, } tests := []struct { name string req entity.DoTokenByDeviceIDReq setup func() wantErr bool }{ { name: "cancel tokens successfully", req: entity.DoTokenByDeviceIDReq{ DeviceID: "device123", }, setup: func() { mockRepo.On("DeleteAccessTokensByDeviceID", mock.Anything, "device123"). Return(nil).Once() }, wantErr: false, }, { name: "repository error", req: entity.DoTokenByDeviceIDReq{ DeviceID: "device456", }, setup: func() { mockRepo.On("DeleteAccessTokensByDeviceID", mock.Anything, "device456"). Return(assert.AnError).Once() }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.setup() err := useCase.CancelTokenByDeviceID(context.Background(), tt.req) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) } mockRepo.AssertExpectations(t) }) } } func TestTokenUseCase_NewOneTimeToken(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", AccessTokenExpiry: 15 * time.Minute, RefreshTokenExpiry: 7 * 24 * time.Hour, }, } useCase := &TokenUseCase{ TokenUseCaseParam: TokenUseCaseParam{ TokenRepo: mockRepo, Config: cfg, }, } // Create a base token first 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 req entity.CreateOneTimeTokenReq setup func() wantErr bool }{ { name: "create one-time token successfully", req: entity.CreateOneTimeTokenReq{ Token: tokenResp.AccessToken, }, setup: func() { existingToken := entity.Token{ ID: "token-id", UID: "user123", AccessToken: tokenResp.AccessToken, ExpiresIn: int(time.Now().Add(time.Hour).Unix()), } mockRepo.On("GetAccessTokenByID", mock.Anything, mock.AnythingOfType("string")). Return(existingToken, nil).Once() mockRepo.On("CreateOneTimeToken", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("entity.Ticket"), mock.AnythingOfType("time.Duration")). Return(nil).Once() }, wantErr: false, }, { name: "invalid token", req: entity.CreateOneTimeTokenReq{ Token: "invalid-token", }, setup: func() { // parseClaims will fail }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.setup() resp, err := useCase.NewOneTimeToken(context.Background(), tt.req) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) assert.NotEmpty(t, resp.OneTimeToken) } mockRepo.AssertExpectations(t) }) } } func TestTokenUseCase_CancelOneTimeToken(t *testing.T) { mockRepo := repository.NewMockTokenRepository(t) cfg := &config.Config{} useCase := &TokenUseCase{ TokenUseCaseParam: TokenUseCaseParam{ TokenRepo: mockRepo, Config: cfg, }, } tests := []struct { name string req entity.CancelOneTimeTokenReq setup func() wantErr bool }{ { name: "cancel one-time token successfully", req: entity.CancelOneTimeTokenReq{ Token: []string{"token1", "token2"}, }, setup: func() { mockRepo.On("DeleteOneTimeToken", mock.Anything, []string{"token1", "token2"}, mock.Anything). Return(nil).Once() }, wantErr: false, }, { name: "repository error", req: entity.CancelOneTimeTokenReq{ Token: []string{"token3"}, }, setup: func() { mockRepo.On("DeleteOneTimeToken", mock.Anything, []string{"token3"}, mock.Anything). Return(assert.AnError).Once() }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.setup() err := useCase.CancelOneTimeToken(context.Background(), tt.req) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) } mockRepo.AssertExpectations(t) }) } } func TestTokenUseCase_ReadTokenBasicData(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", AccessTokenExpiry: 15 * time.Minute, RefreshTokenExpiry: 7 * 24 * time.Hour, }, } useCase := &TokenUseCase{ TokenUseCaseParam: TokenUseCaseParam{ TokenRepo: mockRepo, Config: cfg, }, } // Create a valid token first tokenReq := entity.AuthorizationReq{ GrantType: token.PasswordCredentials.ToString(), Data: map[string]string{ "uid": "user123", "role": "admin", }, Role: "admin", } 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 wantErr bool }{ { name: "read valid token", token: tokenResp.AccessToken, wantErr: false, }, { name: "invalid token", token: "invalid-token", wantErr: true, }, { name: "empty token", token: "", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { claims, err := useCase.ReadTokenBasicData(context.Background(), tt.token) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) assert.NotNil(t, claims) assert.Equal(t, "user123", claims["uid"]) assert.Equal(t, "admin", claims["role"]) } mockRepo.AssertExpectations(t) }) } } // TestTokenUseCase_BlacklistAllUserTokens is commented out due to complexity of mocking // the JWT parsing within the loop. The functionality is tested through integration tests. // func TestTokenUseCase_BlacklistAllUserTokens(t *testing.T) { ... }