backend/pkg/permission/usecase/token_test.go

435 lines
9.5 KiB
Go

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)
})
}
}