Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
f6de36cb
Commit
f6de36cb
authored
Dec 29, 2025
by
yangjianbo
Browse files
test(删除): 添加删除单测并修复中间件测试
新增 AdminService 删除路径单元测试与规范场景更新\n同步调整 Google API Key 中间件测试桩与签名
parent
100d9d20
Changes
5
Hide whitespace changes
Inline
Side-by-side
backend/internal/server/middleware/api_key_auth_google_test.go
View file @
f6de36cb
...
@@ -20,26 +20,39 @@ type fakeApiKeyRepo struct {
...
@@ -20,26 +20,39 @@ type fakeApiKeyRepo struct {
getByKey
func
(
ctx
context
.
Context
,
key
string
)
(
*
service
.
ApiKey
,
error
)
getByKey
func
(
ctx
context
.
Context
,
key
string
)
(
*
service
.
ApiKey
,
error
)
}
}
func
(
f
fakeApiKeyRepo
)
Create
(
ctx
context
.
Context
,
key
*
service
.
ApiKey
)
error
{
return
errors
.
New
(
"not implemented"
)
}
func
(
f
fakeApiKeyRepo
)
Create
(
ctx
context
.
Context
,
key
*
service
.
ApiKey
)
error
{
return
errors
.
New
(
"not implemented"
)
}
func
(
f
fakeApiKeyRepo
)
GetByID
(
ctx
context
.
Context
,
id
int64
)
(
*
service
.
ApiKey
,
error
)
{
func
(
f
fakeApiKeyRepo
)
GetByID
(
ctx
context
.
Context
,
id
int64
)
(
*
service
.
ApiKey
,
error
)
{
return
nil
,
errors
.
New
(
"not implemented"
)
return
nil
,
errors
.
New
(
"not implemented"
)
}
}
func
(
f
fakeApiKeyRepo
)
GetOwnerID
(
ctx
context
.
Context
,
id
int64
)
(
int64
,
error
)
{
return
0
,
errors
.
New
(
"not implemented"
)
}
func
(
f
fakeApiKeyRepo
)
GetByKey
(
ctx
context
.
Context
,
key
string
)
(
*
service
.
ApiKey
,
error
)
{
func
(
f
fakeApiKeyRepo
)
GetByKey
(
ctx
context
.
Context
,
key
string
)
(
*
service
.
ApiKey
,
error
)
{
if
f
.
getByKey
==
nil
{
if
f
.
getByKey
==
nil
{
return
nil
,
errors
.
New
(
"unexpected call"
)
return
nil
,
errors
.
New
(
"unexpected call"
)
}
}
return
f
.
getByKey
(
ctx
,
key
)
return
f
.
getByKey
(
ctx
,
key
)
}
}
func
(
f
fakeApiKeyRepo
)
Update
(
ctx
context
.
Context
,
key
*
service
.
ApiKey
)
error
{
return
errors
.
New
(
"not implemented"
)
}
func
(
f
fakeApiKeyRepo
)
Update
(
ctx
context
.
Context
,
key
*
service
.
ApiKey
)
error
{
func
(
f
fakeApiKeyRepo
)
Delete
(
ctx
context
.
Context
,
id
int64
)
error
{
return
errors
.
New
(
"not implemented"
)
}
return
errors
.
New
(
"not implemented"
)
}
func
(
f
fakeApiKeyRepo
)
Delete
(
ctx
context
.
Context
,
id
int64
)
error
{
return
errors
.
New
(
"not implemented"
)
}
func
(
f
fakeApiKeyRepo
)
ListByUserID
(
ctx
context
.
Context
,
userID
int64
,
params
pagination
.
PaginationParams
)
([]
service
.
ApiKey
,
*
pagination
.
PaginationResult
,
error
)
{
func
(
f
fakeApiKeyRepo
)
ListByUserID
(
ctx
context
.
Context
,
userID
int64
,
params
pagination
.
PaginationParams
)
([]
service
.
ApiKey
,
*
pagination
.
PaginationResult
,
error
)
{
return
nil
,
nil
,
errors
.
New
(
"not implemented"
)
return
nil
,
nil
,
errors
.
New
(
"not implemented"
)
}
}
func
(
f
fakeApiKeyRepo
)
VerifyOwnership
(
ctx
context
.
Context
,
userID
int64
,
apiKeyIDs
[]
int64
)
([]
int64
,
error
)
{
func
(
f
fakeApiKeyRepo
)
VerifyOwnership
(
ctx
context
.
Context
,
userID
int64
,
apiKeyIDs
[]
int64
)
([]
int64
,
error
)
{
return
nil
,
errors
.
New
(
"not implemented"
)
return
nil
,
errors
.
New
(
"not implemented"
)
}
}
func
(
f
fakeApiKeyRepo
)
CountByUserID
(
ctx
context
.
Context
,
userID
int64
)
(
int64
,
error
)
{
return
0
,
errors
.
New
(
"not implemented"
)
}
func
(
f
fakeApiKeyRepo
)
CountByUserID
(
ctx
context
.
Context
,
userID
int64
)
(
int64
,
error
)
{
func
(
f
fakeApiKeyRepo
)
ExistsByKey
(
ctx
context
.
Context
,
key
string
)
(
bool
,
error
)
{
return
false
,
errors
.
New
(
"not implemented"
)
}
return
0
,
errors
.
New
(
"not implemented"
)
}
func
(
f
fakeApiKeyRepo
)
ExistsByKey
(
ctx
context
.
Context
,
key
string
)
(
bool
,
error
)
{
return
false
,
errors
.
New
(
"not implemented"
)
}
func
(
f
fakeApiKeyRepo
)
ListByGroupID
(
ctx
context
.
Context
,
groupID
int64
,
params
pagination
.
PaginationParams
)
([]
service
.
ApiKey
,
*
pagination
.
PaginationResult
,
error
)
{
func
(
f
fakeApiKeyRepo
)
ListByGroupID
(
ctx
context
.
Context
,
groupID
int64
,
params
pagination
.
PaginationParams
)
([]
service
.
ApiKey
,
*
pagination
.
PaginationResult
,
error
)
{
return
nil
,
nil
,
errors
.
New
(
"not implemented"
)
return
nil
,
nil
,
errors
.
New
(
"not implemented"
)
}
}
...
@@ -81,7 +94,7 @@ func TestApiKeyAuthWithSubscriptionGoogle_MissingKey(t *testing.T) {
...
@@ -81,7 +94,7 @@ func TestApiKeyAuthWithSubscriptionGoogle_MissingKey(t *testing.T) {
return
nil
,
errors
.
New
(
"should not be called"
)
return
nil
,
errors
.
New
(
"should not be called"
)
},
},
})
})
r
.
Use
(
ApiKeyAuthWithSubscriptionGoogle
(
apiKeyService
,
nil
))
r
.
Use
(
ApiKeyAuthWithSubscriptionGoogle
(
apiKeyService
,
nil
,
&
config
.
Config
{}
))
r
.
GET
(
"/v1beta/test"
,
func
(
c
*
gin
.
Context
)
{
c
.
JSON
(
200
,
gin
.
H
{
"ok"
:
true
})
})
r
.
GET
(
"/v1beta/test"
,
func
(
c
*
gin
.
Context
)
{
c
.
JSON
(
200
,
gin
.
H
{
"ok"
:
true
})
})
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/v1beta/test"
,
nil
)
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/v1beta/test"
,
nil
)
...
@@ -105,7 +118,7 @@ func TestApiKeyAuthWithSubscriptionGoogle_InvalidKey(t *testing.T) {
...
@@ -105,7 +118,7 @@ func TestApiKeyAuthWithSubscriptionGoogle_InvalidKey(t *testing.T) {
return
nil
,
service
.
ErrApiKeyNotFound
return
nil
,
service
.
ErrApiKeyNotFound
},
},
})
})
r
.
Use
(
ApiKeyAuthWithSubscriptionGoogle
(
apiKeyService
,
nil
))
r
.
Use
(
ApiKeyAuthWithSubscriptionGoogle
(
apiKeyService
,
nil
,
&
config
.
Config
{}
))
r
.
GET
(
"/v1beta/test"
,
func
(
c
*
gin
.
Context
)
{
c
.
JSON
(
200
,
gin
.
H
{
"ok"
:
true
})
})
r
.
GET
(
"/v1beta/test"
,
func
(
c
*
gin
.
Context
)
{
c
.
JSON
(
200
,
gin
.
H
{
"ok"
:
true
})
})
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/v1beta/test"
,
nil
)
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/v1beta/test"
,
nil
)
...
@@ -130,7 +143,7 @@ func TestApiKeyAuthWithSubscriptionGoogle_RepoError(t *testing.T) {
...
@@ -130,7 +143,7 @@ func TestApiKeyAuthWithSubscriptionGoogle_RepoError(t *testing.T) {
return
nil
,
errors
.
New
(
"db down"
)
return
nil
,
errors
.
New
(
"db down"
)
},
},
})
})
r
.
Use
(
ApiKeyAuthWithSubscriptionGoogle
(
apiKeyService
,
nil
))
r
.
Use
(
ApiKeyAuthWithSubscriptionGoogle
(
apiKeyService
,
nil
,
&
config
.
Config
{}
))
r
.
GET
(
"/v1beta/test"
,
func
(
c
*
gin
.
Context
)
{
c
.
JSON
(
200
,
gin
.
H
{
"ok"
:
true
})
})
r
.
GET
(
"/v1beta/test"
,
func
(
c
*
gin
.
Context
)
{
c
.
JSON
(
200
,
gin
.
H
{
"ok"
:
true
})
})
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/v1beta/test"
,
nil
)
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/v1beta/test"
,
nil
)
...
@@ -163,7 +176,7 @@ func TestApiKeyAuthWithSubscriptionGoogle_DisabledKey(t *testing.T) {
...
@@ -163,7 +176,7 @@ func TestApiKeyAuthWithSubscriptionGoogle_DisabledKey(t *testing.T) {
},
nil
},
nil
},
},
})
})
r
.
Use
(
ApiKeyAuthWithSubscriptionGoogle
(
apiKeyService
,
nil
))
r
.
Use
(
ApiKeyAuthWithSubscriptionGoogle
(
apiKeyService
,
nil
,
&
config
.
Config
{}
))
r
.
GET
(
"/v1beta/test"
,
func
(
c
*
gin
.
Context
)
{
c
.
JSON
(
200
,
gin
.
H
{
"ok"
:
true
})
})
r
.
GET
(
"/v1beta/test"
,
func
(
c
*
gin
.
Context
)
{
c
.
JSON
(
200
,
gin
.
H
{
"ok"
:
true
})
})
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/v1beta/test"
,
nil
)
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/v1beta/test"
,
nil
)
...
@@ -197,7 +210,7 @@ func TestApiKeyAuthWithSubscriptionGoogle_InsufficientBalance(t *testing.T) {
...
@@ -197,7 +210,7 @@ func TestApiKeyAuthWithSubscriptionGoogle_InsufficientBalance(t *testing.T) {
},
nil
},
nil
},
},
})
})
r
.
Use
(
ApiKeyAuthWithSubscriptionGoogle
(
apiKeyService
,
nil
))
r
.
Use
(
ApiKeyAuthWithSubscriptionGoogle
(
apiKeyService
,
nil
,
&
config
.
Config
{}
))
r
.
GET
(
"/v1beta/test"
,
func
(
c
*
gin
.
Context
)
{
c
.
JSON
(
200
,
gin
.
H
{
"ok"
:
true
})
})
r
.
GET
(
"/v1beta/test"
,
func
(
c
*
gin
.
Context
)
{
c
.
JSON
(
200
,
gin
.
H
{
"ok"
:
true
})
})
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/v1beta/test"
,
nil
)
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/v1beta/test"
,
nil
)
...
...
backend/internal/service/admin_service_delete_test.go
0 → 100644
View file @
f6de36cb
//go:build unit
package
service
import
(
"context"
"errors"
"testing"
"time"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
"github.com/stretchr/testify/require"
)
type
userRepoStub
struct
{
user
*
User
getErr
error
deleteErr
error
deletedIDs
[]
int64
}
func
(
s
*
userRepoStub
)
Create
(
ctx
context
.
Context
,
user
*
User
)
error
{
panic
(
"unexpected Create call"
)
}
func
(
s
*
userRepoStub
)
GetByID
(
ctx
context
.
Context
,
id
int64
)
(
*
User
,
error
)
{
if
s
.
getErr
!=
nil
{
return
nil
,
s
.
getErr
}
if
s
.
user
==
nil
{
return
nil
,
ErrUserNotFound
}
return
s
.
user
,
nil
}
func
(
s
*
userRepoStub
)
GetByEmail
(
ctx
context
.
Context
,
email
string
)
(
*
User
,
error
)
{
panic
(
"unexpected GetByEmail call"
)
}
func
(
s
*
userRepoStub
)
GetFirstAdmin
(
ctx
context
.
Context
)
(
*
User
,
error
)
{
panic
(
"unexpected GetFirstAdmin call"
)
}
func
(
s
*
userRepoStub
)
Update
(
ctx
context
.
Context
,
user
*
User
)
error
{
panic
(
"unexpected Update call"
)
}
func
(
s
*
userRepoStub
)
Delete
(
ctx
context
.
Context
,
id
int64
)
error
{
s
.
deletedIDs
=
append
(
s
.
deletedIDs
,
id
)
return
s
.
deleteErr
}
func
(
s
*
userRepoStub
)
List
(
ctx
context
.
Context
,
params
pagination
.
PaginationParams
)
([]
User
,
*
pagination
.
PaginationResult
,
error
)
{
panic
(
"unexpected List call"
)
}
func
(
s
*
userRepoStub
)
ListWithFilters
(
ctx
context
.
Context
,
params
pagination
.
PaginationParams
,
status
,
role
,
search
string
)
([]
User
,
*
pagination
.
PaginationResult
,
error
)
{
panic
(
"unexpected ListWithFilters call"
)
}
func
(
s
*
userRepoStub
)
UpdateBalance
(
ctx
context
.
Context
,
id
int64
,
amount
float64
)
error
{
panic
(
"unexpected UpdateBalance call"
)
}
func
(
s
*
userRepoStub
)
DeductBalance
(
ctx
context
.
Context
,
id
int64
,
amount
float64
)
error
{
panic
(
"unexpected DeductBalance call"
)
}
func
(
s
*
userRepoStub
)
UpdateConcurrency
(
ctx
context
.
Context
,
id
int64
,
amount
int
)
error
{
panic
(
"unexpected UpdateConcurrency call"
)
}
func
(
s
*
userRepoStub
)
ExistsByEmail
(
ctx
context
.
Context
,
email
string
)
(
bool
,
error
)
{
panic
(
"unexpected ExistsByEmail call"
)
}
func
(
s
*
userRepoStub
)
RemoveGroupFromAllowedGroups
(
ctx
context
.
Context
,
groupID
int64
)
(
int64
,
error
)
{
panic
(
"unexpected RemoveGroupFromAllowedGroups call"
)
}
type
groupRepoStub
struct
{
affectedUserIDs
[]
int64
deleteErr
error
deleteCalls
[]
int64
}
func
(
s
*
groupRepoStub
)
Create
(
ctx
context
.
Context
,
group
*
Group
)
error
{
panic
(
"unexpected Create call"
)
}
func
(
s
*
groupRepoStub
)
GetByID
(
ctx
context
.
Context
,
id
int64
)
(
*
Group
,
error
)
{
panic
(
"unexpected GetByID call"
)
}
func
(
s
*
groupRepoStub
)
Update
(
ctx
context
.
Context
,
group
*
Group
)
error
{
panic
(
"unexpected Update call"
)
}
func
(
s
*
groupRepoStub
)
Delete
(
ctx
context
.
Context
,
id
int64
)
error
{
panic
(
"unexpected Delete call"
)
}
func
(
s
*
groupRepoStub
)
DeleteCascade
(
ctx
context
.
Context
,
id
int64
)
([]
int64
,
error
)
{
s
.
deleteCalls
=
append
(
s
.
deleteCalls
,
id
)
return
s
.
affectedUserIDs
,
s
.
deleteErr
}
func
(
s
*
groupRepoStub
)
List
(
ctx
context
.
Context
,
params
pagination
.
PaginationParams
)
([]
Group
,
*
pagination
.
PaginationResult
,
error
)
{
panic
(
"unexpected List call"
)
}
func
(
s
*
groupRepoStub
)
ListWithFilters
(
ctx
context
.
Context
,
params
pagination
.
PaginationParams
,
platform
,
status
string
,
isExclusive
*
bool
)
([]
Group
,
*
pagination
.
PaginationResult
,
error
)
{
panic
(
"unexpected ListWithFilters call"
)
}
func
(
s
*
groupRepoStub
)
ListActive
(
ctx
context
.
Context
)
([]
Group
,
error
)
{
panic
(
"unexpected ListActive call"
)
}
func
(
s
*
groupRepoStub
)
ListActiveByPlatform
(
ctx
context
.
Context
,
platform
string
)
([]
Group
,
error
)
{
panic
(
"unexpected ListActiveByPlatform call"
)
}
func
(
s
*
groupRepoStub
)
ExistsByName
(
ctx
context
.
Context
,
name
string
)
(
bool
,
error
)
{
panic
(
"unexpected ExistsByName call"
)
}
func
(
s
*
groupRepoStub
)
GetAccountCount
(
ctx
context
.
Context
,
groupID
int64
)
(
int64
,
error
)
{
panic
(
"unexpected GetAccountCount call"
)
}
func
(
s
*
groupRepoStub
)
DeleteAccountGroupsByGroupID
(
ctx
context
.
Context
,
groupID
int64
)
(
int64
,
error
)
{
panic
(
"unexpected DeleteAccountGroupsByGroupID call"
)
}
type
proxyRepoStub
struct
{
deleteErr
error
deletedIDs
[]
int64
}
func
(
s
*
proxyRepoStub
)
Create
(
ctx
context
.
Context
,
proxy
*
Proxy
)
error
{
panic
(
"unexpected Create call"
)
}
func
(
s
*
proxyRepoStub
)
GetByID
(
ctx
context
.
Context
,
id
int64
)
(
*
Proxy
,
error
)
{
panic
(
"unexpected GetByID call"
)
}
func
(
s
*
proxyRepoStub
)
Update
(
ctx
context
.
Context
,
proxy
*
Proxy
)
error
{
panic
(
"unexpected Update call"
)
}
func
(
s
*
proxyRepoStub
)
Delete
(
ctx
context
.
Context
,
id
int64
)
error
{
s
.
deletedIDs
=
append
(
s
.
deletedIDs
,
id
)
return
s
.
deleteErr
}
func
(
s
*
proxyRepoStub
)
List
(
ctx
context
.
Context
,
params
pagination
.
PaginationParams
)
([]
Proxy
,
*
pagination
.
PaginationResult
,
error
)
{
panic
(
"unexpected List call"
)
}
func
(
s
*
proxyRepoStub
)
ListWithFilters
(
ctx
context
.
Context
,
params
pagination
.
PaginationParams
,
protocol
,
status
,
search
string
)
([]
Proxy
,
*
pagination
.
PaginationResult
,
error
)
{
panic
(
"unexpected ListWithFilters call"
)
}
func
(
s
*
proxyRepoStub
)
ListActive
(
ctx
context
.
Context
)
([]
Proxy
,
error
)
{
panic
(
"unexpected ListActive call"
)
}
func
(
s
*
proxyRepoStub
)
ListActiveWithAccountCount
(
ctx
context
.
Context
)
([]
ProxyWithAccountCount
,
error
)
{
panic
(
"unexpected ListActiveWithAccountCount call"
)
}
func
(
s
*
proxyRepoStub
)
ExistsByHostPortAuth
(
ctx
context
.
Context
,
host
string
,
port
int
,
username
,
password
string
)
(
bool
,
error
)
{
panic
(
"unexpected ExistsByHostPortAuth call"
)
}
func
(
s
*
proxyRepoStub
)
CountAccountsByProxyID
(
ctx
context
.
Context
,
proxyID
int64
)
(
int64
,
error
)
{
panic
(
"unexpected CountAccountsByProxyID call"
)
}
type
redeemRepoStub
struct
{
deleteErrByID
map
[
int64
]
error
deletedIDs
[]
int64
}
func
(
s
*
redeemRepoStub
)
Create
(
ctx
context
.
Context
,
code
*
RedeemCode
)
error
{
panic
(
"unexpected Create call"
)
}
func
(
s
*
redeemRepoStub
)
CreateBatch
(
ctx
context
.
Context
,
codes
[]
RedeemCode
)
error
{
panic
(
"unexpected CreateBatch call"
)
}
func
(
s
*
redeemRepoStub
)
GetByID
(
ctx
context
.
Context
,
id
int64
)
(
*
RedeemCode
,
error
)
{
panic
(
"unexpected GetByID call"
)
}
func
(
s
*
redeemRepoStub
)
GetByCode
(
ctx
context
.
Context
,
code
string
)
(
*
RedeemCode
,
error
)
{
panic
(
"unexpected GetByCode call"
)
}
func
(
s
*
redeemRepoStub
)
Update
(
ctx
context
.
Context
,
code
*
RedeemCode
)
error
{
panic
(
"unexpected Update call"
)
}
func
(
s
*
redeemRepoStub
)
Delete
(
ctx
context
.
Context
,
id
int64
)
error
{
s
.
deletedIDs
=
append
(
s
.
deletedIDs
,
id
)
if
s
.
deleteErrByID
!=
nil
{
if
err
,
ok
:=
s
.
deleteErrByID
[
id
];
ok
{
return
err
}
}
return
nil
}
func
(
s
*
redeemRepoStub
)
Use
(
ctx
context
.
Context
,
id
,
userID
int64
)
error
{
panic
(
"unexpected Use call"
)
}
func
(
s
*
redeemRepoStub
)
List
(
ctx
context
.
Context
,
params
pagination
.
PaginationParams
)
([]
RedeemCode
,
*
pagination
.
PaginationResult
,
error
)
{
panic
(
"unexpected List call"
)
}
func
(
s
*
redeemRepoStub
)
ListWithFilters
(
ctx
context
.
Context
,
params
pagination
.
PaginationParams
,
codeType
,
status
,
search
string
)
([]
RedeemCode
,
*
pagination
.
PaginationResult
,
error
)
{
panic
(
"unexpected ListWithFilters call"
)
}
func
(
s
*
redeemRepoStub
)
ListByUser
(
ctx
context
.
Context
,
userID
int64
,
limit
int
)
([]
RedeemCode
,
error
)
{
panic
(
"unexpected ListByUser call"
)
}
type
subscriptionInvalidateCall
struct
{
userID
int64
groupID
int64
}
type
billingCacheStub
struct
{
invalidations
chan
subscriptionInvalidateCall
}
func
newBillingCacheStub
(
buffer
int
)
*
billingCacheStub
{
return
&
billingCacheStub
{
invalidations
:
make
(
chan
subscriptionInvalidateCall
,
buffer
)}
}
func
(
s
*
billingCacheStub
)
GetUserBalance
(
ctx
context
.
Context
,
userID
int64
)
(
float64
,
error
)
{
panic
(
"unexpected GetUserBalance call"
)
}
func
(
s
*
billingCacheStub
)
SetUserBalance
(
ctx
context
.
Context
,
userID
int64
,
balance
float64
)
error
{
panic
(
"unexpected SetUserBalance call"
)
}
func
(
s
*
billingCacheStub
)
DeductUserBalance
(
ctx
context
.
Context
,
userID
int64
,
amount
float64
)
error
{
panic
(
"unexpected DeductUserBalance call"
)
}
func
(
s
*
billingCacheStub
)
InvalidateUserBalance
(
ctx
context
.
Context
,
userID
int64
)
error
{
panic
(
"unexpected InvalidateUserBalance call"
)
}
func
(
s
*
billingCacheStub
)
GetSubscriptionCache
(
ctx
context
.
Context
,
userID
,
groupID
int64
)
(
*
SubscriptionCacheData
,
error
)
{
panic
(
"unexpected GetSubscriptionCache call"
)
}
func
(
s
*
billingCacheStub
)
SetSubscriptionCache
(
ctx
context
.
Context
,
userID
,
groupID
int64
,
data
*
SubscriptionCacheData
)
error
{
panic
(
"unexpected SetSubscriptionCache call"
)
}
func
(
s
*
billingCacheStub
)
UpdateSubscriptionUsage
(
ctx
context
.
Context
,
userID
,
groupID
int64
,
cost
float64
)
error
{
panic
(
"unexpected UpdateSubscriptionUsage call"
)
}
func
(
s
*
billingCacheStub
)
InvalidateSubscriptionCache
(
ctx
context
.
Context
,
userID
,
groupID
int64
)
error
{
s
.
invalidations
<-
subscriptionInvalidateCall
{
userID
:
userID
,
groupID
:
groupID
}
return
nil
}
func
waitForInvalidations
(
t
*
testing
.
T
,
ch
<-
chan
subscriptionInvalidateCall
,
expected
int
)
[]
subscriptionInvalidateCall
{
t
.
Helper
()
calls
:=
make
([]
subscriptionInvalidateCall
,
0
,
expected
)
timeout
:=
time
.
After
(
2
*
time
.
Second
)
for
len
(
calls
)
<
expected
{
select
{
case
call
:=
<-
ch
:
calls
=
append
(
calls
,
call
)
case
<-
timeout
:
t
.
Fatalf
(
"timeout waiting for %d invalidations, got %d"
,
expected
,
len
(
calls
))
}
}
return
calls
}
func
TestAdminService_DeleteUser_Success
(
t
*
testing
.
T
)
{
repo
:=
&
userRepoStub
{
user
:
&
User
{
ID
:
7
,
Role
:
RoleUser
}}
svc
:=
&
adminServiceImpl
{
userRepo
:
repo
}
err
:=
svc
.
DeleteUser
(
context
.
Background
(),
7
)
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
[]
int64
{
7
},
repo
.
deletedIDs
)
}
func
TestAdminService_DeleteUser_NotFound
(
t
*
testing
.
T
)
{
repo
:=
&
userRepoStub
{
getErr
:
ErrUserNotFound
}
svc
:=
&
adminServiceImpl
{
userRepo
:
repo
}
err
:=
svc
.
DeleteUser
(
context
.
Background
(),
404
)
require
.
ErrorIs
(
t
,
err
,
ErrUserNotFound
)
require
.
Empty
(
t
,
repo
.
deletedIDs
)
}
func
TestAdminService_DeleteUser_AdminGuard
(
t
*
testing
.
T
)
{
repo
:=
&
userRepoStub
{
user
:
&
User
{
ID
:
1
,
Role
:
RoleAdmin
}}
svc
:=
&
adminServiceImpl
{
userRepo
:
repo
}
err
:=
svc
.
DeleteUser
(
context
.
Background
(),
1
)
require
.
Error
(
t
,
err
)
require
.
ErrorContains
(
t
,
err
,
"cannot delete admin user"
)
require
.
Empty
(
t
,
repo
.
deletedIDs
)
}
func
TestAdminService_DeleteUser_DeleteError
(
t
*
testing
.
T
)
{
deleteErr
:=
errors
.
New
(
"delete failed"
)
repo
:=
&
userRepoStub
{
user
:
&
User
{
ID
:
9
,
Role
:
RoleUser
},
deleteErr
:
deleteErr
,
}
svc
:=
&
adminServiceImpl
{
userRepo
:
repo
}
err
:=
svc
.
DeleteUser
(
context
.
Background
(),
9
)
require
.
ErrorIs
(
t
,
err
,
deleteErr
)
require
.
Equal
(
t
,
[]
int64
{
9
},
repo
.
deletedIDs
)
}
func
TestAdminService_DeleteGroup_Success_WithCacheInvalidation
(
t
*
testing
.
T
)
{
cache
:=
newBillingCacheStub
(
2
)
repo
:=
&
groupRepoStub
{
affectedUserIDs
:
[]
int64
{
11
,
12
}}
svc
:=
&
adminServiceImpl
{
groupRepo
:
repo
,
billingCacheService
:
&
BillingCacheService
{
cache
:
cache
},
}
err
:=
svc
.
DeleteGroup
(
context
.
Background
(),
5
)
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
[]
int64
{
5
},
repo
.
deleteCalls
)
calls
:=
waitForInvalidations
(
t
,
cache
.
invalidations
,
2
)
require
.
ElementsMatch
(
t
,
[]
subscriptionInvalidateCall
{
{
userID
:
11
,
groupID
:
5
},
{
userID
:
12
,
groupID
:
5
},
},
calls
)
}
func
TestAdminService_DeleteGroup_NotFound
(
t
*
testing
.
T
)
{
repo
:=
&
groupRepoStub
{
deleteErr
:
ErrGroupNotFound
}
svc
:=
&
adminServiceImpl
{
groupRepo
:
repo
}
err
:=
svc
.
DeleteGroup
(
context
.
Background
(),
99
)
require
.
ErrorIs
(
t
,
err
,
ErrGroupNotFound
)
}
func
TestAdminService_DeleteGroup_Error
(
t
*
testing
.
T
)
{
deleteErr
:=
errors
.
New
(
"delete failed"
)
repo
:=
&
groupRepoStub
{
deleteErr
:
deleteErr
}
svc
:=
&
adminServiceImpl
{
groupRepo
:
repo
}
err
:=
svc
.
DeleteGroup
(
context
.
Background
(),
42
)
require
.
ErrorIs
(
t
,
err
,
deleteErr
)
}
func
TestAdminService_DeleteProxy_Success
(
t
*
testing
.
T
)
{
repo
:=
&
proxyRepoStub
{}
svc
:=
&
adminServiceImpl
{
proxyRepo
:
repo
}
err
:=
svc
.
DeleteProxy
(
context
.
Background
(),
7
)
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
[]
int64
{
7
},
repo
.
deletedIDs
)
}
func
TestAdminService_DeleteProxy_Idempotent
(
t
*
testing
.
T
)
{
repo
:=
&
proxyRepoStub
{}
svc
:=
&
adminServiceImpl
{
proxyRepo
:
repo
}
err
:=
svc
.
DeleteProxy
(
context
.
Background
(),
404
)
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
[]
int64
{
404
},
repo
.
deletedIDs
)
}
func
TestAdminService_DeleteProxy_Error
(
t
*
testing
.
T
)
{
deleteErr
:=
errors
.
New
(
"delete failed"
)
repo
:=
&
proxyRepoStub
{
deleteErr
:
deleteErr
}
svc
:=
&
adminServiceImpl
{
proxyRepo
:
repo
}
err
:=
svc
.
DeleteProxy
(
context
.
Background
(),
33
)
require
.
ErrorIs
(
t
,
err
,
deleteErr
)
}
func
TestAdminService_DeleteRedeemCode_Success
(
t
*
testing
.
T
)
{
repo
:=
&
redeemRepoStub
{}
svc
:=
&
adminServiceImpl
{
redeemCodeRepo
:
repo
}
err
:=
svc
.
DeleteRedeemCode
(
context
.
Background
(),
10
)
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
[]
int64
{
10
},
repo
.
deletedIDs
)
}
func
TestAdminService_DeleteRedeemCode_Idempotent
(
t
*
testing
.
T
)
{
repo
:=
&
redeemRepoStub
{}
svc
:=
&
adminServiceImpl
{
redeemCodeRepo
:
repo
}
err
:=
svc
.
DeleteRedeemCode
(
context
.
Background
(),
999
)
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
[]
int64
{
999
},
repo
.
deletedIDs
)
}
func
TestAdminService_DeleteRedeemCode_Error
(
t
*
testing
.
T
)
{
deleteErr
:=
errors
.
New
(
"delete failed"
)
repo
:=
&
redeemRepoStub
{
deleteErrByID
:
map
[
int64
]
error
{
1
:
deleteErr
}}
svc
:=
&
adminServiceImpl
{
redeemCodeRepo
:
repo
}
err
:=
svc
.
DeleteRedeemCode
(
context
.
Background
(),
1
)
require
.
ErrorIs
(
t
,
err
,
deleteErr
)
require
.
Equal
(
t
,
[]
int64
{
1
},
repo
.
deletedIDs
)
}
func
TestAdminService_BatchDeleteRedeemCodes_Success
(
t
*
testing
.
T
)
{
repo
:=
&
redeemRepoStub
{}
svc
:=
&
adminServiceImpl
{
redeemCodeRepo
:
repo
}
deleted
,
err
:=
svc
.
BatchDeleteRedeemCodes
(
context
.
Background
(),
[]
int64
{
1
,
2
,
3
})
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
int64
(
3
),
deleted
)
require
.
Equal
(
t
,
[]
int64
{
1
,
2
,
3
},
repo
.
deletedIDs
)
}
func
TestAdminService_BatchDeleteRedeemCodes_PartialFailures
(
t
*
testing
.
T
)
{
repo
:=
&
redeemRepoStub
{
deleteErrByID
:
map
[
int64
]
error
{
2
:
errors
.
New
(
"db error"
),
},
}
svc
:=
&
adminServiceImpl
{
redeemCodeRepo
:
repo
}
deleted
,
err
:=
svc
.
BatchDeleteRedeemCodes
(
context
.
Background
(),
[]
int64
{
1
,
2
,
3
})
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
int64
(
2
),
deleted
)
require
.
Equal
(
t
,
[]
int64
{
1
,
2
,
3
},
repo
.
deletedIDs
)
}
openspec/changes/add-delete-unit-tests/proposal.md
0 → 100644
View file @
f6de36cb
# Change: Add unit tests for delete paths (user/group/proxy/redeem)
## Why
删除流程缺少单元测试,容易在重构或边界条件变化时回归,且问题排查成本高。
## What Changes
-
新增服务层删除流程单元测试(覆盖 AdminService 删除入口与对应 Repo 错误传播)
-
覆盖成功/不存在/权限保护/幂等删除/底层错误等关键分支
-
需要的轻量测试替身(repositories / cache)
## Impact
-
Affected specs: testing (new)
-
Affected code: backend/internal/service/admin_service.go
openspec/changes/add-delete-unit-tests/specs/testing/spec.md
0 → 100644
View file @
f6de36cb
## ADDED Requirements
### Requirement: Delete path unit coverage
服务层删除流程 SHALL 具备单元测试覆盖用户、分组、代理、兑换码等资源的关键分支,且覆盖 AdminService 删除入口的权限保护、幂等删除与错误传播。
#### Scenario: User delete success
-
**WHEN**
删除存在的用户
-
**THEN**
返回成功且仓储删除被调用
#### Scenario: User delete not found
-
**WHEN**
删除不存在的用户
-
**THEN**
返回未找到错误
#### Scenario: User delete propagates errors
-
**WHEN**
删除用户时仓储返回错误
-
**THEN**
错误被向上返回且不吞掉
#### Scenario: User delete rejects admin accounts
-
**WHEN**
删除管理员用户
-
**THEN**
返回拒绝删除的错误
#### Scenario: Group delete success
-
**WHEN**
删除存在的分组
-
**THEN**
返回成功且仓储级联删除被调用
#### Scenario: Group delete not found
-
**WHEN**
删除不存在的分组
-
**THEN**
返回 ErrGroupNotFound
#### Scenario: Group delete propagates errors
-
**WHEN**
删除分组时仓储返回错误
-
**THEN**
错误被向上返回且不吞掉
#### Scenario: Proxy delete success
-
**WHEN**
删除存在的代理
-
**THEN**
返回成功且仓储删除被调用
#### Scenario: Proxy delete is idempotent
-
**WHEN**
删除不存在的代理
-
**THEN**
不返回错误且调用删除流程
#### Scenario: Proxy delete propagates errors
-
**WHEN**
删除代理时仓储返回错误
-
**THEN**
错误被向上返回且不吞掉
#### Scenario: Redeem code delete success
-
**WHEN**
删除存在的兑换码
-
**THEN**
返回成功且仓储删除被调用
#### Scenario: Redeem code delete is idempotent
-
**WHEN**
删除不存在的兑换码
-
**THEN**
不返回错误且调用删除流程
#### Scenario: Redeem code delete propagates errors
-
**WHEN**
删除兑换码时仓储返回错误
-
**THEN**
错误被向上返回且不吞掉
#### Scenario: Batch redeem code delete success
-
**WHEN**
批量删除兑换码且全部成功
-
**THEN**
返回删除数量等于输入数量且不返回错误
#### Scenario: Batch redeem code delete partial failures
-
**WHEN**
批量删除兑换码且部分失败
-
**THEN**
返回删除数量小于输入数量且不返回错误
openspec/changes/add-delete-unit-tests/tasks.md
0 → 100644
View file @
f6de36cb
## 1. Implementation
-
[x] 1.1 为 AdminService 删除入口准备测试替身(user/group/proxy/redeem repo 与 cache)
-
[x] 1.2 新增 AdminService.DeleteUser 单元测试(成功/不存在/错误传播/管理员保护)
-
[x] 1.3 新增 AdminService.DeleteGroup 单元测试(成功/不存在/错误传播,缓存失效逻辑如适用)
-
[x] 1.4 新增 AdminService.DeleteProxy 单元测试(成功/幂等删除/错误传播)
-
[x] 1.5 新增 AdminService.DeleteRedeemCode 与 BatchDeleteRedeemCodes 单元测试(成功/幂等删除/错误传播/部分失败)
-
[x] 1.6 运行 unit 测试并将结果记录在本 tasks.md 末尾
## Test Results
-
`go test -tags=unit ./internal/service/...`
(workdir:
`backend`
)
-
ok github.com/Wei-Shaw/sub2api/internal/service 0.475s
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment