566 lines
12 KiB
Go
566 lines
12 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_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) { ... }
|
||
|
|