435 lines
9.5 KiB
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)
|
|
})
|
|
}
|
|
} |