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
c86d445c
Commit
c86d445c
authored
Jan 04, 2026
by
IanShaw027
Browse files
fix(frontend): sync with main and finalize i18n & component optimizations
parents
6c036d7b
e78c8646
Changes
186
Hide whitespace changes
Inline
Side-by-side
backend/internal/repository/account_repo_integration_test.go
View file @
c86d445c
...
...
@@ -135,12 +135,12 @@ func (s *AccountRepoSuite) TestListWithFilters() {
name
:
"filter_by_type"
,
setup
:
func
(
client
*
dbent
.
Client
)
{
mustCreateAccount
(
s
.
T
(),
client
,
&
service
.
Account
{
Name
:
"t1"
,
Type
:
service
.
AccountTypeOAuth
})
mustCreateAccount
(
s
.
T
(),
client
,
&
service
.
Account
{
Name
:
"t2"
,
Type
:
service
.
AccountTypeA
pi
Key
})
mustCreateAccount
(
s
.
T
(),
client
,
&
service
.
Account
{
Name
:
"t2"
,
Type
:
service
.
AccountTypeA
PI
Key
})
},
accType
:
service
.
AccountTypeA
pi
Key
,
accType
:
service
.
AccountTypeA
PI
Key
,
wantCount
:
1
,
validate
:
func
(
accounts
[]
service
.
Account
)
{
s
.
Require
()
.
Equal
(
service
.
AccountTypeA
pi
Key
,
accounts
[
0
]
.
Type
)
s
.
Require
()
.
Equal
(
service
.
AccountTypeA
PI
Key
,
accounts
[
0
]
.
Type
)
},
},
{
...
...
backend/internal/repository/allowed_groups_contract_integration_test.go
View file @
c86d445c
...
...
@@ -98,7 +98,7 @@ func TestGroupRepository_DeleteCascade_RemovesAllowedGroupsAndClearsApiKeys(t *t
userRepo
:=
newUserRepositoryWithSQL
(
entClient
,
tx
)
groupRepo
:=
newGroupRepositoryWithSQL
(
entClient
,
tx
)
apiKeyRepo
:=
NewA
pi
KeyRepository
(
entClient
)
apiKeyRepo
:=
NewA
PI
KeyRepository
(
entClient
)
u
:=
&
service
.
User
{
Email
:
uniqueTestValue
(
t
,
"cascade-user"
)
+
"@example.com"
,
...
...
@@ -110,7 +110,7 @@ func TestGroupRepository_DeleteCascade_RemovesAllowedGroupsAndClearsApiKeys(t *t
}
require
.
NoError
(
t
,
userRepo
.
Create
(
ctx
,
u
))
key
:=
&
service
.
A
pi
Key
{
key
:=
&
service
.
A
PI
Key
{
UserID
:
u
.
ID
,
Key
:
uniqueTestValue
(
t
,
"sk-test-delete-cascade"
),
Name
:
"test key"
,
...
...
backend/internal/repository/api_key_cache.go
View file @
c86d445c
...
...
@@ -24,7 +24,7 @@ type apiKeyCache struct {
rdb
*
redis
.
Client
}
func
NewA
pi
KeyCache
(
rdb
*
redis
.
Client
)
service
.
A
pi
KeyCache
{
func
NewA
PI
KeyCache
(
rdb
*
redis
.
Client
)
service
.
A
PI
KeyCache
{
return
&
apiKeyCache
{
rdb
:
rdb
}
}
...
...
backend/internal/repository/api_key_repo.go
View file @
c86d445c
...
...
@@ -16,17 +16,17 @@ type apiKeyRepository struct {
client
*
dbent
.
Client
}
func
NewA
pi
KeyRepository
(
client
*
dbent
.
Client
)
service
.
A
pi
KeyRepository
{
func
NewA
PI
KeyRepository
(
client
*
dbent
.
Client
)
service
.
A
PI
KeyRepository
{
return
&
apiKeyRepository
{
client
:
client
}
}
func
(
r
*
apiKeyRepository
)
activeQuery
()
*
dbent
.
A
pi
KeyQuery
{
func
(
r
*
apiKeyRepository
)
activeQuery
()
*
dbent
.
A
PI
KeyQuery
{
// 默认过滤已软删除记录,避免删除后仍被查询到。
return
r
.
client
.
A
pi
Key
.
Query
()
.
Where
(
apikey
.
DeletedAtIsNil
())
return
r
.
client
.
A
PI
Key
.
Query
()
.
Where
(
apikey
.
DeletedAtIsNil
())
}
func
(
r
*
apiKeyRepository
)
Create
(
ctx
context
.
Context
,
key
*
service
.
A
pi
Key
)
error
{
created
,
err
:=
r
.
client
.
A
pi
Key
.
Create
()
.
func
(
r
*
apiKeyRepository
)
Create
(
ctx
context
.
Context
,
key
*
service
.
A
PI
Key
)
error
{
created
,
err
:=
r
.
client
.
A
PI
Key
.
Create
()
.
SetUserID
(
key
.
UserID
)
.
SetKey
(
key
.
Key
)
.
SetName
(
key
.
Name
)
.
...
...
@@ -38,10 +38,10 @@ func (r *apiKeyRepository) Create(ctx context.Context, key *service.ApiKey) erro
key
.
CreatedAt
=
created
.
CreatedAt
key
.
UpdatedAt
=
created
.
UpdatedAt
}
return
translatePersistenceError
(
err
,
nil
,
service
.
ErrA
pi
KeyExists
)
return
translatePersistenceError
(
err
,
nil
,
service
.
ErrA
PI
KeyExists
)
}
func
(
r
*
apiKeyRepository
)
GetByID
(
ctx
context
.
Context
,
id
int64
)
(
*
service
.
A
pi
Key
,
error
)
{
func
(
r
*
apiKeyRepository
)
GetByID
(
ctx
context
.
Context
,
id
int64
)
(
*
service
.
A
PI
Key
,
error
)
{
m
,
err
:=
r
.
activeQuery
()
.
Where
(
apikey
.
IDEQ
(
id
))
.
WithUser
()
.
...
...
@@ -49,7 +49,7 @@ func (r *apiKeyRepository) GetByID(ctx context.Context, id int64) (*service.ApiK
Only
(
ctx
)
if
err
!=
nil
{
if
dbent
.
IsNotFound
(
err
)
{
return
nil
,
service
.
ErrA
pi
KeyNotFound
return
nil
,
service
.
ErrA
PI
KeyNotFound
}
return
nil
,
err
}
...
...
@@ -59,7 +59,7 @@ func (r *apiKeyRepository) GetByID(ctx context.Context, id int64) (*service.ApiK
// GetOwnerID 根据 API Key ID 获取其所有者(用户)的 ID。
// 相比 GetByID,此方法性能更优,因为:
// - 使用 Select() 只查询 user_id 字段,减少数据传输量
// - 不加载完整的 A
pi
Key 实体及其关联数据(User、Group 等)
// - 不加载完整的 A
PI
Key 实体及其关联数据(User、Group 等)
// - 适用于权限验证等只需用户 ID 的场景(如删除前的所有权检查)
func
(
r
*
apiKeyRepository
)
GetOwnerID
(
ctx
context
.
Context
,
id
int64
)
(
int64
,
error
)
{
m
,
err
:=
r
.
activeQuery
()
.
...
...
@@ -68,14 +68,14 @@ func (r *apiKeyRepository) GetOwnerID(ctx context.Context, id int64) (int64, err
Only
(
ctx
)
if
err
!=
nil
{
if
dbent
.
IsNotFound
(
err
)
{
return
0
,
service
.
ErrA
pi
KeyNotFound
return
0
,
service
.
ErrA
PI
KeyNotFound
}
return
0
,
err
}
return
m
.
UserID
,
nil
}
func
(
r
*
apiKeyRepository
)
GetByKey
(
ctx
context
.
Context
,
key
string
)
(
*
service
.
A
pi
Key
,
error
)
{
func
(
r
*
apiKeyRepository
)
GetByKey
(
ctx
context
.
Context
,
key
string
)
(
*
service
.
A
PI
Key
,
error
)
{
m
,
err
:=
r
.
activeQuery
()
.
Where
(
apikey
.
KeyEQ
(
key
))
.
WithUser
()
.
...
...
@@ -83,21 +83,21 @@ func (r *apiKeyRepository) GetByKey(ctx context.Context, key string) (*service.A
Only
(
ctx
)
if
err
!=
nil
{
if
dbent
.
IsNotFound
(
err
)
{
return
nil
,
service
.
ErrA
pi
KeyNotFound
return
nil
,
service
.
ErrA
PI
KeyNotFound
}
return
nil
,
err
}
return
apiKeyEntityToService
(
m
),
nil
}
func
(
r
*
apiKeyRepository
)
Update
(
ctx
context
.
Context
,
key
*
service
.
A
pi
Key
)
error
{
func
(
r
*
apiKeyRepository
)
Update
(
ctx
context
.
Context
,
key
*
service
.
A
PI
Key
)
error
{
// 使用原子操作:将软删除检查与更新合并到同一语句,避免竞态条件。
// 之前的实现先检查 Exist 再 UpdateOneID,若在两步之间发生软删除,
// 则会更新已删除的记录。
// 这里选择 Update().Where(),确保只有未软删除记录能被更新。
// 同时显式设置 updated_at,避免二次查询带来的并发可见性问题。
now
:=
time
.
Now
()
builder
:=
r
.
client
.
A
pi
Key
.
Update
()
.
builder
:=
r
.
client
.
A
PI
Key
.
Update
()
.
Where
(
apikey
.
IDEQ
(
key
.
ID
),
apikey
.
DeletedAtIsNil
())
.
SetName
(
key
.
Name
)
.
SetStatus
(
key
.
Status
)
.
...
...
@@ -114,7 +114,7 @@ func (r *apiKeyRepository) Update(ctx context.Context, key *service.ApiKey) erro
}
if
affected
==
0
{
// 更新影响行数为 0,说明记录不存在或已被软删除。
return
service
.
ErrA
pi
KeyNotFound
return
service
.
ErrA
PI
KeyNotFound
}
// 使用同一时间戳回填,避免并发删除导致二次查询失败。
...
...
@@ -124,18 +124,18 @@ func (r *apiKeyRepository) Update(ctx context.Context, key *service.ApiKey) erro
func
(
r
*
apiKeyRepository
)
Delete
(
ctx
context
.
Context
,
id
int64
)
error
{
// 显式软删除:避免依赖 Hook 行为,确保 deleted_at 一定被设置。
affected
,
err
:=
r
.
client
.
A
pi
Key
.
Update
()
.
affected
,
err
:=
r
.
client
.
A
PI
Key
.
Update
()
.
Where
(
apikey
.
IDEQ
(
id
),
apikey
.
DeletedAtIsNil
())
.
SetDeletedAt
(
time
.
Now
())
.
Save
(
ctx
)
if
err
!=
nil
{
if
dbent
.
IsNotFound
(
err
)
{
return
service
.
ErrA
pi
KeyNotFound
return
service
.
ErrA
PI
KeyNotFound
}
return
err
}
if
affected
==
0
{
exists
,
err
:=
r
.
client
.
A
pi
Key
.
Query
()
.
exists
,
err
:=
r
.
client
.
A
PI
Key
.
Query
()
.
Where
(
apikey
.
IDEQ
(
id
))
.
Exist
(
mixins
.
SkipSoftDelete
(
ctx
))
if
err
!=
nil
{
...
...
@@ -144,12 +144,12 @@ func (r *apiKeyRepository) Delete(ctx context.Context, id int64) error {
if
exists
{
return
nil
}
return
service
.
ErrA
pi
KeyNotFound
return
service
.
ErrA
PI
KeyNotFound
}
return
nil
}
func
(
r
*
apiKeyRepository
)
ListByUserID
(
ctx
context
.
Context
,
userID
int64
,
params
pagination
.
PaginationParams
)
([]
service
.
A
pi
Key
,
*
pagination
.
PaginationResult
,
error
)
{
func
(
r
*
apiKeyRepository
)
ListByUserID
(
ctx
context
.
Context
,
userID
int64
,
params
pagination
.
PaginationParams
)
([]
service
.
A
PI
Key
,
*
pagination
.
PaginationResult
,
error
)
{
q
:=
r
.
activeQuery
()
.
Where
(
apikey
.
UserIDEQ
(
userID
))
total
,
err
:=
q
.
Count
(
ctx
)
...
...
@@ -167,7 +167,7 @@ func (r *apiKeyRepository) ListByUserID(ctx context.Context, userID int64, param
return
nil
,
nil
,
err
}
outKeys
:=
make
([]
service
.
A
pi
Key
,
0
,
len
(
keys
))
outKeys
:=
make
([]
service
.
A
PI
Key
,
0
,
len
(
keys
))
for
i
:=
range
keys
{
outKeys
=
append
(
outKeys
,
*
apiKeyEntityToService
(
keys
[
i
]))
}
...
...
@@ -180,7 +180,7 @@ func (r *apiKeyRepository) VerifyOwnership(ctx context.Context, userID int64, ap
return
[]
int64
{},
nil
}
ids
,
err
:=
r
.
client
.
A
pi
Key
.
Query
()
.
ids
,
err
:=
r
.
client
.
A
PI
Key
.
Query
()
.
Where
(
apikey
.
UserIDEQ
(
userID
),
apikey
.
IDIn
(
apiKeyIDs
...
),
apikey
.
DeletedAtIsNil
())
.
IDs
(
ctx
)
if
err
!=
nil
{
...
...
@@ -199,7 +199,7 @@ func (r *apiKeyRepository) ExistsByKey(ctx context.Context, key string) (bool, e
return
count
>
0
,
err
}
func
(
r
*
apiKeyRepository
)
ListByGroupID
(
ctx
context
.
Context
,
groupID
int64
,
params
pagination
.
PaginationParams
)
([]
service
.
A
pi
Key
,
*
pagination
.
PaginationResult
,
error
)
{
func
(
r
*
apiKeyRepository
)
ListByGroupID
(
ctx
context
.
Context
,
groupID
int64
,
params
pagination
.
PaginationParams
)
([]
service
.
A
PI
Key
,
*
pagination
.
PaginationResult
,
error
)
{
q
:=
r
.
activeQuery
()
.
Where
(
apikey
.
GroupIDEQ
(
groupID
))
total
,
err
:=
q
.
Count
(
ctx
)
...
...
@@ -217,7 +217,7 @@ func (r *apiKeyRepository) ListByGroupID(ctx context.Context, groupID int64, par
return
nil
,
nil
,
err
}
outKeys
:=
make
([]
service
.
A
pi
Key
,
0
,
len
(
keys
))
outKeys
:=
make
([]
service
.
A
PI
Key
,
0
,
len
(
keys
))
for
i
:=
range
keys
{
outKeys
=
append
(
outKeys
,
*
apiKeyEntityToService
(
keys
[
i
]))
}
...
...
@@ -225,8 +225,8 @@ func (r *apiKeyRepository) ListByGroupID(ctx context.Context, groupID int64, par
return
outKeys
,
paginationResultFromTotal
(
int64
(
total
),
params
),
nil
}
// SearchA
pi
Keys searches API keys by user ID and/or keyword (name)
func
(
r
*
apiKeyRepository
)
SearchA
pi
Keys
(
ctx
context
.
Context
,
userID
int64
,
keyword
string
,
limit
int
)
([]
service
.
A
pi
Key
,
error
)
{
// SearchA
PI
Keys searches API keys by user ID and/or keyword (name)
func
(
r
*
apiKeyRepository
)
SearchA
PI
Keys
(
ctx
context
.
Context
,
userID
int64
,
keyword
string
,
limit
int
)
([]
service
.
A
PI
Key
,
error
)
{
q
:=
r
.
activeQuery
()
if
userID
>
0
{
q
=
q
.
Where
(
apikey
.
UserIDEQ
(
userID
))
...
...
@@ -241,7 +241,7 @@ func (r *apiKeyRepository) SearchApiKeys(ctx context.Context, userID int64, keyw
return
nil
,
err
}
outKeys
:=
make
([]
service
.
A
pi
Key
,
0
,
len
(
keys
))
outKeys
:=
make
([]
service
.
A
PI
Key
,
0
,
len
(
keys
))
for
i
:=
range
keys
{
outKeys
=
append
(
outKeys
,
*
apiKeyEntityToService
(
keys
[
i
]))
}
...
...
@@ -250,7 +250,7 @@ func (r *apiKeyRepository) SearchApiKeys(ctx context.Context, userID int64, keyw
// ClearGroupIDByGroupID 将指定分组的所有 API Key 的 group_id 设为 nil
func
(
r
*
apiKeyRepository
)
ClearGroupIDByGroupID
(
ctx
context
.
Context
,
groupID
int64
)
(
int64
,
error
)
{
n
,
err
:=
r
.
client
.
A
pi
Key
.
Update
()
.
n
,
err
:=
r
.
client
.
A
PI
Key
.
Update
()
.
Where
(
apikey
.
GroupIDEQ
(
groupID
),
apikey
.
DeletedAtIsNil
())
.
ClearGroupID
()
.
Save
(
ctx
)
...
...
@@ -263,11 +263,11 @@ func (r *apiKeyRepository) CountByGroupID(ctx context.Context, groupID int64) (i
return
int64
(
count
),
err
}
func
apiKeyEntityToService
(
m
*
dbent
.
A
pi
Key
)
*
service
.
A
pi
Key
{
func
apiKeyEntityToService
(
m
*
dbent
.
A
PI
Key
)
*
service
.
A
PI
Key
{
if
m
==
nil
{
return
nil
}
out
:=
&
service
.
A
pi
Key
{
out
:=
&
service
.
A
PI
Key
{
ID
:
m
.
ID
,
UserID
:
m
.
UserID
,
Key
:
m
.
Key
,
...
...
backend/internal/repository/api_key_repo_integration_test.go
View file @
c86d445c
...
...
@@ -12,30 +12,30 @@ import (
"github.com/stretchr/testify/suite"
)
type
A
pi
KeyRepoSuite
struct
{
type
A
PI
KeyRepoSuite
struct
{
suite
.
Suite
ctx
context
.
Context
client
*
dbent
.
Client
repo
*
apiKeyRepository
}
func
(
s
*
A
pi
KeyRepoSuite
)
SetupTest
()
{
func
(
s
*
A
PI
KeyRepoSuite
)
SetupTest
()
{
s
.
ctx
=
context
.
Background
()
tx
:=
testEntTx
(
s
.
T
())
s
.
client
=
tx
.
Client
()
s
.
repo
=
NewA
pi
KeyRepository
(
s
.
client
)
.
(
*
apiKeyRepository
)
s
.
repo
=
NewA
PI
KeyRepository
(
s
.
client
)
.
(
*
apiKeyRepository
)
}
func
TestA
pi
KeyRepoSuite
(
t
*
testing
.
T
)
{
suite
.
Run
(
t
,
new
(
A
pi
KeyRepoSuite
))
func
TestA
PI
KeyRepoSuite
(
t
*
testing
.
T
)
{
suite
.
Run
(
t
,
new
(
A
PI
KeyRepoSuite
))
}
// --- Create / GetByID / GetByKey ---
func
(
s
*
A
pi
KeyRepoSuite
)
TestCreate
()
{
func
(
s
*
A
PI
KeyRepoSuite
)
TestCreate
()
{
user
:=
s
.
mustCreateUser
(
"create@test.com"
)
key
:=
&
service
.
A
pi
Key
{
key
:=
&
service
.
A
PI
Key
{
UserID
:
user
.
ID
,
Key
:
"sk-create-test"
,
Name
:
"Test Key"
,
...
...
@@ -51,16 +51,16 @@ func (s *ApiKeyRepoSuite) TestCreate() {
s
.
Require
()
.
Equal
(
"sk-create-test"
,
got
.
Key
)
}
func
(
s
*
A
pi
KeyRepoSuite
)
TestGetByID_NotFound
()
{
func
(
s
*
A
PI
KeyRepoSuite
)
TestGetByID_NotFound
()
{
_
,
err
:=
s
.
repo
.
GetByID
(
s
.
ctx
,
999999
)
s
.
Require
()
.
Error
(
err
,
"expected error for non-existent ID"
)
}
func
(
s
*
A
pi
KeyRepoSuite
)
TestGetByKey
()
{
func
(
s
*
A
PI
KeyRepoSuite
)
TestGetByKey
()
{
user
:=
s
.
mustCreateUser
(
"getbykey@test.com"
)
group
:=
s
.
mustCreateGroup
(
"g-key"
)
key
:=
&
service
.
A
pi
Key
{
key
:=
&
service
.
A
PI
Key
{
UserID
:
user
.
ID
,
Key
:
"sk-getbykey"
,
Name
:
"My Key"
,
...
...
@@ -78,16 +78,16 @@ func (s *ApiKeyRepoSuite) TestGetByKey() {
s
.
Require
()
.
Equal
(
group
.
ID
,
got
.
Group
.
ID
)
}
func
(
s
*
A
pi
KeyRepoSuite
)
TestGetByKey_NotFound
()
{
func
(
s
*
A
PI
KeyRepoSuite
)
TestGetByKey_NotFound
()
{
_
,
err
:=
s
.
repo
.
GetByKey
(
s
.
ctx
,
"non-existent-key"
)
s
.
Require
()
.
Error
(
err
,
"expected error for non-existent key"
)
}
// --- Update ---
func
(
s
*
A
pi
KeyRepoSuite
)
TestUpdate
()
{
func
(
s
*
A
PI
KeyRepoSuite
)
TestUpdate
()
{
user
:=
s
.
mustCreateUser
(
"update@test.com"
)
key
:=
&
service
.
A
pi
Key
{
key
:=
&
service
.
A
PI
Key
{
UserID
:
user
.
ID
,
Key
:
"sk-update"
,
Name
:
"Original"
,
...
...
@@ -108,10 +108,10 @@ func (s *ApiKeyRepoSuite) TestUpdate() {
s
.
Require
()
.
Equal
(
service
.
StatusDisabled
,
got
.
Status
)
}
func
(
s
*
A
pi
KeyRepoSuite
)
TestUpdate_ClearGroupID
()
{
func
(
s
*
A
PI
KeyRepoSuite
)
TestUpdate_ClearGroupID
()
{
user
:=
s
.
mustCreateUser
(
"cleargroup@test.com"
)
group
:=
s
.
mustCreateGroup
(
"g-clear"
)
key
:=
&
service
.
A
pi
Key
{
key
:=
&
service
.
A
PI
Key
{
UserID
:
user
.
ID
,
Key
:
"sk-clear-group"
,
Name
:
"Group Key"
,
...
...
@@ -131,9 +131,9 @@ func (s *ApiKeyRepoSuite) TestUpdate_ClearGroupID() {
// --- Delete ---
func
(
s
*
A
pi
KeyRepoSuite
)
TestDelete
()
{
func
(
s
*
A
PI
KeyRepoSuite
)
TestDelete
()
{
user
:=
s
.
mustCreateUser
(
"delete@test.com"
)
key
:=
&
service
.
A
pi
Key
{
key
:=
&
service
.
A
PI
Key
{
UserID
:
user
.
ID
,
Key
:
"sk-delete"
,
Name
:
"Delete Me"
,
...
...
@@ -150,7 +150,7 @@ func (s *ApiKeyRepoSuite) TestDelete() {
// --- ListByUserID / CountByUserID ---
func
(
s
*
A
pi
KeyRepoSuite
)
TestListByUserID
()
{
func
(
s
*
A
PI
KeyRepoSuite
)
TestListByUserID
()
{
user
:=
s
.
mustCreateUser
(
"listbyuser@test.com"
)
s
.
mustCreateApiKey
(
user
.
ID
,
"sk-list-1"
,
"Key 1"
,
nil
)
s
.
mustCreateApiKey
(
user
.
ID
,
"sk-list-2"
,
"Key 2"
,
nil
)
...
...
@@ -161,7 +161,7 @@ func (s *ApiKeyRepoSuite) TestListByUserID() {
s
.
Require
()
.
Equal
(
int64
(
2
),
page
.
Total
)
}
func
(
s
*
A
pi
KeyRepoSuite
)
TestListByUserID_Pagination
()
{
func
(
s
*
A
PI
KeyRepoSuite
)
TestListByUserID_Pagination
()
{
user
:=
s
.
mustCreateUser
(
"paging@test.com"
)
for
i
:=
0
;
i
<
5
;
i
++
{
s
.
mustCreateApiKey
(
user
.
ID
,
"sk-page-"
+
string
(
rune
(
'a'
+
i
)),
"Key"
,
nil
)
...
...
@@ -174,7 +174,7 @@ func (s *ApiKeyRepoSuite) TestListByUserID_Pagination() {
s
.
Require
()
.
Equal
(
3
,
page
.
Pages
)
}
func
(
s
*
A
pi
KeyRepoSuite
)
TestCountByUserID
()
{
func
(
s
*
A
PI
KeyRepoSuite
)
TestCountByUserID
()
{
user
:=
s
.
mustCreateUser
(
"count@test.com"
)
s
.
mustCreateApiKey
(
user
.
ID
,
"sk-count-1"
,
"K1"
,
nil
)
s
.
mustCreateApiKey
(
user
.
ID
,
"sk-count-2"
,
"K2"
,
nil
)
...
...
@@ -186,7 +186,7 @@ func (s *ApiKeyRepoSuite) TestCountByUserID() {
// --- ListByGroupID / CountByGroupID ---
func
(
s
*
A
pi
KeyRepoSuite
)
TestListByGroupID
()
{
func
(
s
*
A
PI
KeyRepoSuite
)
TestListByGroupID
()
{
user
:=
s
.
mustCreateUser
(
"listbygroup@test.com"
)
group
:=
s
.
mustCreateGroup
(
"g-list"
)
...
...
@@ -202,7 +202,7 @@ func (s *ApiKeyRepoSuite) TestListByGroupID() {
s
.
Require
()
.
NotNil
(
keys
[
0
]
.
User
)
}
func
(
s
*
A
pi
KeyRepoSuite
)
TestCountByGroupID
()
{
func
(
s
*
A
PI
KeyRepoSuite
)
TestCountByGroupID
()
{
user
:=
s
.
mustCreateUser
(
"countgroup@test.com"
)
group
:=
s
.
mustCreateGroup
(
"g-count"
)
s
.
mustCreateApiKey
(
user
.
ID
,
"sk-gc-1"
,
"K1"
,
&
group
.
ID
)
...
...
@@ -214,7 +214,7 @@ func (s *ApiKeyRepoSuite) TestCountByGroupID() {
// --- ExistsByKey ---
func
(
s
*
A
pi
KeyRepoSuite
)
TestExistsByKey
()
{
func
(
s
*
A
PI
KeyRepoSuite
)
TestExistsByKey
()
{
user
:=
s
.
mustCreateUser
(
"exists@test.com"
)
s
.
mustCreateApiKey
(
user
.
ID
,
"sk-exists"
,
"K"
,
nil
)
...
...
@@ -227,41 +227,41 @@ func (s *ApiKeyRepoSuite) TestExistsByKey() {
s
.
Require
()
.
False
(
notExists
)
}
// --- SearchA
pi
Keys ---
// --- SearchA
PI
Keys ---
func
(
s
*
A
pi
KeyRepoSuite
)
TestSearchA
pi
Keys
()
{
func
(
s
*
A
PI
KeyRepoSuite
)
TestSearchA
PI
Keys
()
{
user
:=
s
.
mustCreateUser
(
"search@test.com"
)
s
.
mustCreateApiKey
(
user
.
ID
,
"sk-search-1"
,
"Production Key"
,
nil
)
s
.
mustCreateApiKey
(
user
.
ID
,
"sk-search-2"
,
"Development Key"
,
nil
)
found
,
err
:=
s
.
repo
.
SearchA
pi
Keys
(
s
.
ctx
,
user
.
ID
,
"prod"
,
10
)
s
.
Require
()
.
NoError
(
err
,
"SearchA
pi
Keys"
)
found
,
err
:=
s
.
repo
.
SearchA
PI
Keys
(
s
.
ctx
,
user
.
ID
,
"prod"
,
10
)
s
.
Require
()
.
NoError
(
err
,
"SearchA
PI
Keys"
)
s
.
Require
()
.
Len
(
found
,
1
)
s
.
Require
()
.
Contains
(
found
[
0
]
.
Name
,
"Production"
)
}
func
(
s
*
A
pi
KeyRepoSuite
)
TestSearchA
pi
Keys_NoKeyword
()
{
func
(
s
*
A
PI
KeyRepoSuite
)
TestSearchA
PI
Keys_NoKeyword
()
{
user
:=
s
.
mustCreateUser
(
"searchnokw@test.com"
)
s
.
mustCreateApiKey
(
user
.
ID
,
"sk-nk-1"
,
"K1"
,
nil
)
s
.
mustCreateApiKey
(
user
.
ID
,
"sk-nk-2"
,
"K2"
,
nil
)
found
,
err
:=
s
.
repo
.
SearchA
pi
Keys
(
s
.
ctx
,
user
.
ID
,
""
,
10
)
found
,
err
:=
s
.
repo
.
SearchA
PI
Keys
(
s
.
ctx
,
user
.
ID
,
""
,
10
)
s
.
Require
()
.
NoError
(
err
)
s
.
Require
()
.
Len
(
found
,
2
)
}
func
(
s
*
A
pi
KeyRepoSuite
)
TestSearchA
pi
Keys_NoUserID
()
{
func
(
s
*
A
PI
KeyRepoSuite
)
TestSearchA
PI
Keys_NoUserID
()
{
user
:=
s
.
mustCreateUser
(
"searchnouid@test.com"
)
s
.
mustCreateApiKey
(
user
.
ID
,
"sk-nu-1"
,
"TestKey"
,
nil
)
found
,
err
:=
s
.
repo
.
SearchA
pi
Keys
(
s
.
ctx
,
0
,
"testkey"
,
10
)
found
,
err
:=
s
.
repo
.
SearchA
PI
Keys
(
s
.
ctx
,
0
,
"testkey"
,
10
)
s
.
Require
()
.
NoError
(
err
)
s
.
Require
()
.
Len
(
found
,
1
)
}
// --- ClearGroupIDByGroupID ---
func
(
s
*
A
pi
KeyRepoSuite
)
TestClearGroupIDByGroupID
()
{
func
(
s
*
A
PI
KeyRepoSuite
)
TestClearGroupIDByGroupID
()
{
user
:=
s
.
mustCreateUser
(
"cleargrp@test.com"
)
group
:=
s
.
mustCreateGroup
(
"g-clear-bulk"
)
...
...
@@ -284,7 +284,7 @@ func (s *ApiKeyRepoSuite) TestClearGroupIDByGroupID() {
// --- Combined CRUD/Search/ClearGroupID (original test preserved as integration) ---
func
(
s
*
A
pi
KeyRepoSuite
)
TestCRUD_Search_ClearGroupID
()
{
func
(
s
*
A
PI
KeyRepoSuite
)
TestCRUD_Search_ClearGroupID
()
{
user
:=
s
.
mustCreateUser
(
"k@example.com"
)
group
:=
s
.
mustCreateGroup
(
"g-k"
)
key
:=
s
.
mustCreateApiKey
(
user
.
ID
,
"sk-test-1"
,
"My Key"
,
&
group
.
ID
)
...
...
@@ -320,8 +320,8 @@ func (s *ApiKeyRepoSuite) TestCRUD_Search_ClearGroupID() {
s
.
Require
()
.
NoError
(
err
,
"ExistsByKey"
)
s
.
Require
()
.
True
(
exists
,
"expected key to exist"
)
found
,
err
:=
s
.
repo
.
SearchA
pi
Keys
(
s
.
ctx
,
user
.
ID
,
"renam"
,
10
)
s
.
Require
()
.
NoError
(
err
,
"SearchA
pi
Keys"
)
found
,
err
:=
s
.
repo
.
SearchA
PI
Keys
(
s
.
ctx
,
user
.
ID
,
"renam"
,
10
)
s
.
Require
()
.
NoError
(
err
,
"SearchA
PI
Keys"
)
s
.
Require
()
.
Len
(
found
,
1
)
s
.
Require
()
.
Equal
(
key
.
ID
,
found
[
0
]
.
ID
)
...
...
@@ -346,7 +346,7 @@ func (s *ApiKeyRepoSuite) TestCRUD_Search_ClearGroupID() {
s
.
Require
()
.
Equal
(
int64
(
0
),
countAfter
,
"expected 0 keys in group after clear"
)
}
func
(
s
*
A
pi
KeyRepoSuite
)
mustCreateUser
(
email
string
)
*
service
.
User
{
func
(
s
*
A
PI
KeyRepoSuite
)
mustCreateUser
(
email
string
)
*
service
.
User
{
s
.
T
()
.
Helper
()
u
,
err
:=
s
.
client
.
User
.
Create
()
.
...
...
@@ -359,7 +359,7 @@ func (s *ApiKeyRepoSuite) mustCreateUser(email string) *service.User {
return
userEntityToService
(
u
)
}
func
(
s
*
A
pi
KeyRepoSuite
)
mustCreateGroup
(
name
string
)
*
service
.
Group
{
func
(
s
*
A
PI
KeyRepoSuite
)
mustCreateGroup
(
name
string
)
*
service
.
Group
{
s
.
T
()
.
Helper
()
g
,
err
:=
s
.
client
.
Group
.
Create
()
.
...
...
@@ -370,10 +370,10 @@ func (s *ApiKeyRepoSuite) mustCreateGroup(name string) *service.Group {
return
groupEntityToService
(
g
)
}
func
(
s
*
A
pi
KeyRepoSuite
)
mustCreateApiKey
(
userID
int64
,
key
,
name
string
,
groupID
*
int64
)
*
service
.
A
pi
Key
{
func
(
s
*
A
PI
KeyRepoSuite
)
mustCreateApiKey
(
userID
int64
,
key
,
name
string
,
groupID
*
int64
)
*
service
.
A
PI
Key
{
s
.
T
()
.
Helper
()
k
:=
&
service
.
A
pi
Key
{
k
:=
&
service
.
A
PI
Key
{
UserID
:
userID
,
Key
:
key
,
Name
:
name
,
...
...
backend/internal/repository/claude_oauth_service_test.go
View file @
c86d445c
...
...
@@ -5,28 +5,20 @@ import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/Wei-Shaw/sub2api/internal/pkg/oauth"
"github.com/imroc/req/v3"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type
ClaudeOAuthServiceSuite
struct
{
suite
.
Suite
srv
*
httptest
.
Server
client
*
claudeOAuthService
}
func
(
s
*
ClaudeOAuthServiceSuite
)
TearDownTest
()
{
if
s
.
srv
!=
nil
{
s
.
srv
.
Close
()
s
.
srv
=
nil
}
}
// requestCapture holds captured request data for assertions in the main goroutine.
type
requestCapture
struct
{
path
string
...
...
@@ -37,6 +29,12 @@ type requestCapture struct {
contentType
string
}
func
newTestReqClient
(
rt
http
.
RoundTripper
)
*
req
.
Client
{
c
:=
req
.
C
()
c
.
GetClient
()
.
Transport
=
rt
return
c
}
func
(
s
*
ClaudeOAuthServiceSuite
)
TestGetOrganizationUUID
()
{
tests
:=
[]
struct
{
name
string
...
...
@@ -83,17 +81,17 @@ func (s *ClaudeOAuthServiceSuite) TestGetOrganizationUUID() {
s
.
Run
(
tt
.
name
,
func
()
{
var
captured
requestCapture
s
.
srv
=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
rt
:=
newInProcessTransport
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
captured
.
path
=
r
.
URL
.
Path
captured
.
cookies
=
r
.
Cookies
()
tt
.
handler
(
w
,
r
)
}))
defer
s
.
srv
.
Close
()
}),
nil
)
client
,
ok
:=
NewClaudeOAuthClient
()
.
(
*
claudeOAuthService
)
require
.
True
(
s
.
T
(),
ok
,
"type assertion failed"
)
s
.
client
=
client
s
.
client
.
baseURL
=
s
.
srv
.
URL
s
.
client
.
baseURL
=
"http://in-process"
s
.
client
.
clientFactory
=
func
(
string
)
*
req
.
Client
{
return
newTestReqClient
(
rt
)
}
got
,
err
:=
s
.
client
.
GetOrganizationUUID
(
context
.
Background
(),
"sess"
,
""
)
...
...
@@ -158,20 +156,20 @@ func (s *ClaudeOAuthServiceSuite) TestGetAuthorizationCode() {
s
.
Run
(
tt
.
name
,
func
()
{
var
captured
requestCapture
s
.
srv
=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
rt
:=
newInProcessTransport
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
captured
.
path
=
r
.
URL
.
Path
captured
.
method
=
r
.
Method
captured
.
cookies
=
r
.
Cookies
()
captured
.
body
,
_
=
io
.
ReadAll
(
r
.
Body
)
_
=
json
.
Unmarshal
(
captured
.
body
,
&
captured
.
bodyJSON
)
tt
.
handler
(
w
,
r
)
}))
defer
s
.
srv
.
Close
()
}),
nil
)
client
,
ok
:=
NewClaudeOAuthClient
()
.
(
*
claudeOAuthService
)
require
.
True
(
s
.
T
(),
ok
,
"type assertion failed"
)
s
.
client
=
client
s
.
client
.
baseURL
=
s
.
srv
.
URL
s
.
client
.
baseURL
=
"http://in-process"
s
.
client
.
clientFactory
=
func
(
string
)
*
req
.
Client
{
return
newTestReqClient
(
rt
)
}
code
,
err
:=
s
.
client
.
GetAuthorizationCode
(
context
.
Background
(),
"sess"
,
"org-1"
,
oauth
.
ScopeProfile
,
"cc"
,
"st"
,
""
)
...
...
@@ -266,19 +264,19 @@ func (s *ClaudeOAuthServiceSuite) TestExchangeCodeForToken() {
s
.
Run
(
tt
.
name
,
func
()
{
var
captured
requestCapture
s
.
srv
=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
rt
:=
newInProcessTransport
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
captured
.
method
=
r
.
Method
captured
.
contentType
=
r
.
Header
.
Get
(
"Content-Type"
)
captured
.
body
,
_
=
io
.
ReadAll
(
r
.
Body
)
_
=
json
.
Unmarshal
(
captured
.
body
,
&
captured
.
bodyJSON
)
tt
.
handler
(
w
,
r
)
}))
defer
s
.
srv
.
Close
()
}),
nil
)
client
,
ok
:=
NewClaudeOAuthClient
()
.
(
*
claudeOAuthService
)
require
.
True
(
s
.
T
(),
ok
,
"type assertion failed"
)
s
.
client
=
client
s
.
client
.
tokenURL
=
s
.
srv
.
URL
s
.
client
.
tokenURL
=
"http://in-process/token"
s
.
client
.
clientFactory
=
func
(
string
)
*
req
.
Client
{
return
newTestReqClient
(
rt
)
}
resp
,
err
:=
s
.
client
.
ExchangeCodeForToken
(
context
.
Background
(),
tt
.
code
,
"ver"
,
""
,
""
,
tt
.
isSetupToken
)
...
...
@@ -362,19 +360,19 @@ func (s *ClaudeOAuthServiceSuite) TestRefreshToken() {
s
.
Run
(
tt
.
name
,
func
()
{
var
captured
requestCapture
s
.
srv
=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
rt
:=
newInProcessTransport
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
captured
.
method
=
r
.
Method
captured
.
contentType
=
r
.
Header
.
Get
(
"Content-Type"
)
captured
.
body
,
_
=
io
.
ReadAll
(
r
.
Body
)
_
=
json
.
Unmarshal
(
captured
.
body
,
&
captured
.
bodyJSON
)
tt
.
handler
(
w
,
r
)
}))
defer
s
.
srv
.
Close
()
}),
nil
)
client
,
ok
:=
NewClaudeOAuthClient
()
.
(
*
claudeOAuthService
)
require
.
True
(
s
.
T
(),
ok
,
"type assertion failed"
)
s
.
client
=
client
s
.
client
.
tokenURL
=
s
.
srv
.
URL
s
.
client
.
tokenURL
=
"http://in-process/token"
s
.
client
.
clientFactory
=
func
(
string
)
*
req
.
Client
{
return
newTestReqClient
(
rt
)
}
resp
,
err
:=
s
.
client
.
RefreshToken
(
context
.
Background
(),
"rt"
,
""
)
...
...
backend/internal/repository/claude_usage_service_test.go
View file @
c86d445c
...
...
@@ -33,7 +33,7 @@ type usageRequestCapture struct {
func
(
s
*
ClaudeUsageServiceSuite
)
TestFetchUsage_Success
()
{
var
captured
usageRequestCapture
s
.
srv
=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
s
.
srv
=
newLocalTestServer
(
s
.
T
(),
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
captured
.
authorization
=
r
.
Header
.
Get
(
"Authorization"
)
captured
.
anthropicBeta
=
r
.
Header
.
Get
(
"anthropic-beta"
)
...
...
@@ -59,7 +59,7 @@ func (s *ClaudeUsageServiceSuite) TestFetchUsage_Success() {
}
func
(
s
*
ClaudeUsageServiceSuite
)
TestFetchUsage_NonOK
()
{
s
.
srv
=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
s
.
srv
=
newLocalTestServer
(
s
.
T
(),
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
w
.
WriteHeader
(
http
.
StatusUnauthorized
)
_
,
_
=
io
.
WriteString
(
w
,
"nope"
)
}))
...
...
@@ -73,7 +73,7 @@ func (s *ClaudeUsageServiceSuite) TestFetchUsage_NonOK() {
}
func
(
s
*
ClaudeUsageServiceSuite
)
TestFetchUsage_BadJSON
()
{
s
.
srv
=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
s
.
srv
=
newLocalTestServer
(
s
.
T
(),
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
w
.
Header
()
.
Set
(
"Content-Type"
,
"application/json"
)
_
,
_
=
io
.
WriteString
(
w
,
"not-json"
)
}))
...
...
@@ -86,7 +86,7 @@ func (s *ClaudeUsageServiceSuite) TestFetchUsage_BadJSON() {
}
func
(
s
*
ClaudeUsageServiceSuite
)
TestFetchUsage_ContextCancel
()
{
s
.
srv
=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
s
.
srv
=
newLocalTestServer
(
s
.
T
(),
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
// Never respond - simulate slow server
<-
r
.
Context
()
.
Done
()
}))
...
...
backend/internal/repository/concurrency_cache.go
View file @
c86d445c
...
...
@@ -309,7 +309,7 @@ func (c *concurrencyCache) GetUserConcurrency(ctx context.Context, userID int64)
func
(
c
*
concurrencyCache
)
IncrementWaitCount
(
ctx
context
.
Context
,
userID
int64
,
maxWait
int
)
(
bool
,
error
)
{
key
:=
waitQueueKey
(
userID
)
result
,
err
:=
incrementWaitScript
.
Run
(
ctx
,
c
.
rdb
,
[]
string
{
key
},
maxWait
,
c
.
slot
TTLSeconds
)
.
Int
()
result
,
err
:=
incrementWaitScript
.
Run
(
ctx
,
c
.
rdb
,
[]
string
{
key
},
maxWait
,
c
.
waitQueue
TTLSeconds
)
.
Int
()
if
err
!=
nil
{
return
false
,
err
}
...
...
backend/internal/repository/ent.go
View file @
c86d445c
// Package
infrastructure
提供应用程序的基础设施层组件。
// Package
repository
提供应用程序的基础设施层组件。
// 包括数据库连接初始化、ORM 客户端管理、Redis 连接、数据库迁移等核心功能。
package
repository
...
...
backend/internal/repository/fixtures_integration_test.go
View file @
c86d445c
...
...
@@ -243,7 +243,7 @@ func mustCreateAccount(t *testing.T, client *dbent.Client, a *service.Account) *
return
a
}
func
mustCreateApiKey
(
t
*
testing
.
T
,
client
*
dbent
.
Client
,
k
*
service
.
A
pi
Key
)
*
service
.
A
pi
Key
{
func
mustCreateApiKey
(
t
*
testing
.
T
,
client
*
dbent
.
Client
,
k
*
service
.
A
PI
Key
)
*
service
.
A
PI
Key
{
t
.
Helper
()
ctx
:=
context
.
Background
()
...
...
@@ -257,7 +257,7 @@ func mustCreateApiKey(t *testing.T, client *dbent.Client, k *service.ApiKey) *se
k
.
Name
=
"default"
}
create
:=
client
.
A
pi
Key
.
Create
()
.
create
:=
client
.
A
PI
Key
.
Create
()
.
SetUserID
(
k
.
UserID
)
.
SetKey
(
k
.
Key
)
.
SetName
(
k
.
Name
)
.
...
...
backend/internal/repository/gemini_oauth_client.go
View file @
c86d445c
...
...
@@ -30,6 +30,7 @@ func (c *geminiOAuthClient) ExchangeCode(ctx context.Context, oauthType, code, c
// Use different OAuth clients based on oauthType:
// - code_assist: always use built-in Gemini CLI OAuth client (public)
// - google_one: uses configured OAuth client when provided; otherwise falls back to built-in client
// - ai_studio: requires a user-provided OAuth client
oauthCfgInput
:=
geminicli
.
OAuthConfig
{
ClientID
:
c
.
cfg
.
Gemini
.
OAuth
.
ClientID
,
...
...
backend/internal/repository/github_release_service_test.go
View file @
c86d445c
...
...
@@ -49,7 +49,7 @@ func (s *GitHubReleaseServiceSuite) TearDownTest() {
}
func
(
s
*
GitHubReleaseServiceSuite
)
TestDownloadFile_EnforcesMaxSize_ContentLength
()
{
s
.
srv
=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
s
.
srv
=
newLocalTestServer
(
s
.
T
(),
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
w
.
Header
()
.
Set
(
"Content-Length"
,
"100"
)
w
.
WriteHeader
(
http
.
StatusOK
)
_
,
_
=
w
.
Write
(
bytes
.
Repeat
([]
byte
(
"a"
),
100
))
...
...
@@ -68,7 +68,7 @@ func (s *GitHubReleaseServiceSuite) TestDownloadFile_EnforcesMaxSize_ContentLeng
}
func
(
s
*
GitHubReleaseServiceSuite
)
TestDownloadFile_EnforcesMaxSize_Chunked
()
{
s
.
srv
=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
s
.
srv
=
newLocalTestServer
(
s
.
T
(),
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
// Force chunked encoding (unknown Content-Length) by flushing headers before writing.
w
.
WriteHeader
(
http
.
StatusOK
)
if
fl
,
ok
:=
w
.
(
http
.
Flusher
);
ok
{
...
...
@@ -95,7 +95,7 @@ func (s *GitHubReleaseServiceSuite) TestDownloadFile_EnforcesMaxSize_Chunked() {
}
func
(
s
*
GitHubReleaseServiceSuite
)
TestDownloadFile_Success
()
{
s
.
srv
=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
s
.
srv
=
newLocalTestServer
(
s
.
T
(),
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
w
.
WriteHeader
(
http
.
StatusOK
)
if
fl
,
ok
:=
w
.
(
http
.
Flusher
);
ok
{
fl
.
Flush
()
...
...
@@ -123,7 +123,7 @@ func (s *GitHubReleaseServiceSuite) TestDownloadFile_Success() {
}
func
(
s
*
GitHubReleaseServiceSuite
)
TestDownloadFile_404
()
{
s
.
srv
=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
s
.
srv
=
newLocalTestServer
(
s
.
T
(),
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
w
.
WriteHeader
(
http
.
StatusNotFound
)
}))
...
...
@@ -140,7 +140,7 @@ func (s *GitHubReleaseServiceSuite) TestDownloadFile_404() {
}
func
(
s
*
GitHubReleaseServiceSuite
)
TestFetchChecksumFile_Success
()
{
s
.
srv
=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
s
.
srv
=
newLocalTestServer
(
s
.
T
(),
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
w
.
WriteHeader
(
http
.
StatusOK
)
_
,
_
=
w
.
Write
([]
byte
(
"sum"
))
}))
...
...
@@ -155,7 +155,7 @@ func (s *GitHubReleaseServiceSuite) TestFetchChecksumFile_Success() {
}
func
(
s
*
GitHubReleaseServiceSuite
)
TestFetchChecksumFile_Non200
()
{
s
.
srv
=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
s
.
srv
=
newLocalTestServer
(
s
.
T
(),
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
w
.
WriteHeader
(
http
.
StatusInternalServerError
)
}))
...
...
@@ -168,7 +168,7 @@ func (s *GitHubReleaseServiceSuite) TestFetchChecksumFile_Non200() {
}
func
(
s
*
GitHubReleaseServiceSuite
)
TestDownloadFile_ContextCancel
()
{
s
.
srv
=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
s
.
srv
=
newLocalTestServer
(
s
.
T
(),
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
<-
r
.
Context
()
.
Done
()
}))
...
...
@@ -195,7 +195,7 @@ func (s *GitHubReleaseServiceSuite) TestDownloadFile_InvalidURL() {
}
func
(
s
*
GitHubReleaseServiceSuite
)
TestDownloadFile_InvalidDestPath
()
{
s
.
srv
=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
s
.
srv
=
newLocalTestServer
(
s
.
T
(),
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
w
.
WriteHeader
(
http
.
StatusOK
)
_
,
_
=
w
.
Write
([]
byte
(
"content"
))
}))
...
...
@@ -233,7 +233,7 @@ func (s *GitHubReleaseServiceSuite) TestFetchLatestRelease_Success() {
]
}`
s
.
srv
=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
s
.
srv
=
newLocalTestServer
(
s
.
T
(),
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
require
.
Equal
(
s
.
T
(),
"/repos/test/repo/releases/latest"
,
r
.
URL
.
Path
)
require
.
Equal
(
s
.
T
(),
"application/vnd.github.v3+json"
,
r
.
Header
.
Get
(
"Accept"
))
require
.
Equal
(
s
.
T
(),
"Sub2API-Updater"
,
r
.
Header
.
Get
(
"User-Agent"
))
...
...
@@ -258,7 +258,7 @@ func (s *GitHubReleaseServiceSuite) TestFetchLatestRelease_Success() {
}
func
(
s
*
GitHubReleaseServiceSuite
)
TestFetchLatestRelease_Non200
()
{
s
.
srv
=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
s
.
srv
=
newLocalTestServer
(
s
.
T
(),
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
w
.
WriteHeader
(
http
.
StatusNotFound
)
}))
...
...
@@ -274,7 +274,7 @@ func (s *GitHubReleaseServiceSuite) TestFetchLatestRelease_Non200() {
}
func
(
s
*
GitHubReleaseServiceSuite
)
TestFetchLatestRelease_InvalidJSON
()
{
s
.
srv
=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
s
.
srv
=
newLocalTestServer
(
s
.
T
(),
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
w
.
WriteHeader
(
http
.
StatusOK
)
_
,
_
=
w
.
Write
([]
byte
(
"not valid json"
))
}))
...
...
@@ -290,7 +290,7 @@ func (s *GitHubReleaseServiceSuite) TestFetchLatestRelease_InvalidJSON() {
}
func
(
s
*
GitHubReleaseServiceSuite
)
TestFetchLatestRelease_ContextCancel
()
{
s
.
srv
=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
s
.
srv
=
newLocalTestServer
(
s
.
T
(),
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
<-
r
.
Context
()
.
Done
()
}))
...
...
@@ -308,7 +308,7 @@ func (s *GitHubReleaseServiceSuite) TestFetchLatestRelease_ContextCancel() {
}
func
(
s
*
GitHubReleaseServiceSuite
)
TestFetchChecksumFile_ContextCancel
()
{
s
.
srv
=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
s
.
srv
=
newLocalTestServer
(
s
.
T
(),
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
<-
r
.
Context
()
.
Done
()
}))
...
...
backend/internal/repository/group_repo.go
View file @
c86d445c
...
...
@@ -293,8 +293,8 @@ func (r *groupRepository) DeleteCascade(ctx context.Context, id int64) ([]int64,
// 2. Clear group_id for api keys bound to this group.
// 仅更新未软删除的记录,避免修改已删除数据,保证审计与历史回溯一致性。
// 与 A
pi
KeyRepository 的软删除语义保持一致,减少跨模块行为差异。
if
_
,
err
:=
txClient
.
A
pi
Key
.
Update
()
.
// 与 A
PI
KeyRepository 的软删除语义保持一致,减少跨模块行为差异。
if
_
,
err
:=
txClient
.
A
PI
Key
.
Update
()
.
Where
(
apikey
.
GroupIDEQ
(
id
),
apikey
.
DeletedAtIsNil
())
.
ClearGroupID
()
.
Save
(
ctx
);
err
!=
nil
{
...
...
backend/internal/repository/http_upstream_test.go
View file @
c86d445c
...
...
@@ -3,7 +3,6 @@ package repository
import
(
"io"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
...
...
@@ -93,7 +92,7 @@ func (s *HTTPUpstreamSuite) TestAcquireClient_OverLimitReturnsError() {
// 验证空代理 URL 时请求直接发送到目标服务器
func
(
s
*
HTTPUpstreamSuite
)
TestDo_WithoutProxy_GoesDirect
()
{
// 创建模拟上游服务器
upstream
:=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
upstream
:=
newLocalTestServer
(
s
.
T
(),
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
_
,
_
=
io
.
WriteString
(
w
,
"direct"
)
}))
s
.
T
()
.
Cleanup
(
upstream
.
Close
)
...
...
@@ -115,7 +114,7 @@ func (s *HTTPUpstreamSuite) TestDo_WithHTTPProxy_UsesProxy() {
// 用于接收代理请求的通道
seen
:=
make
(
chan
string
,
1
)
// 创建模拟代理服务器
proxySrv
:=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
proxySrv
:=
newLocalTestServer
(
s
.
T
(),
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
seen
<-
r
.
RequestURI
// 记录请求 URI
_
,
_
=
io
.
WriteString
(
w
,
"proxied"
)
}))
...
...
@@ -145,7 +144,7 @@ func (s *HTTPUpstreamSuite) TestDo_WithHTTPProxy_UsesProxy() {
// TestDo_EmptyProxy_UsesDirect 测试空代理字符串
// 验证空字符串代理等同于直连
func
(
s
*
HTTPUpstreamSuite
)
TestDo_EmptyProxy_UsesDirect
()
{
upstream
:=
httptest
.
NewServer
(
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
upstream
:=
newLocalTestServer
(
s
.
T
(),
http
.
HandlerFunc
(
func
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
_
,
_
=
io
.
WriteString
(
w
,
"direct-empty"
)
}))
s
.
T
()
.
Cleanup
(
upstream
.
Close
)
...
...
backend/internal/repository/inprocess_transport_test.go
0 → 100644
View file @
c86d445c
package
repository
import
(
"bytes"
"io"
"net"
"net/http"
"net/http/httptest"
"sync"
"testing"
)
type
roundTripFunc
func
(
*
http
.
Request
)
(
*
http
.
Response
,
error
)
func
(
f
roundTripFunc
)
RoundTrip
(
r
*
http
.
Request
)
(
*
http
.
Response
,
error
)
{
return
f
(
r
)
}
// newInProcessTransport adapts an http.HandlerFunc into an http.RoundTripper without opening sockets.
// It captures the request body (if any) and then rewinds it before invoking the handler.
func
newInProcessTransport
(
handler
http
.
HandlerFunc
,
capture
func
(
r
*
http
.
Request
,
body
[]
byte
))
http
.
RoundTripper
{
return
roundTripFunc
(
func
(
r
*
http
.
Request
)
(
*
http
.
Response
,
error
)
{
var
body
[]
byte
if
r
.
Body
!=
nil
{
body
,
_
=
io
.
ReadAll
(
r
.
Body
)
_
=
r
.
Body
.
Close
()
r
.
Body
=
io
.
NopCloser
(
bytes
.
NewReader
(
body
))
}
if
capture
!=
nil
{
capture
(
r
,
body
)
}
rec
:=
httptest
.
NewRecorder
()
handler
(
rec
,
r
)
return
rec
.
Result
(),
nil
})
}
var
(
canListenOnce
sync
.
Once
canListen
bool
canListenErr
error
)
func
localListenerAvailable
()
bool
{
canListenOnce
.
Do
(
func
()
{
ln
,
err
:=
net
.
Listen
(
"tcp"
,
"127.0.0.1:0"
)
if
err
!=
nil
{
canListenErr
=
err
canListen
=
false
return
}
_
=
ln
.
Close
()
canListen
=
true
})
return
canListen
}
func
newLocalTestServer
(
tb
testing
.
TB
,
handler
http
.
Handler
)
*
httptest
.
Server
{
tb
.
Helper
()
if
!
localListenerAvailable
()
{
tb
.
Skipf
(
"local listeners are not permitted in this environment: %v"
,
canListenErr
)
}
return
httptest
.
NewServer
(
handler
)
}
backend/internal/repository/openai_oauth_service_test.go
View file @
c86d445c
...
...
@@ -34,7 +34,7 @@ func (s *OpenAIOAuthServiceSuite) TearDownTest() {
}
func
(
s
*
OpenAIOAuthServiceSuite
)
setupServer
(
handler
http
.
HandlerFunc
)
{
s
.
srv
=
httptest
.
NewServer
(
handler
)
s
.
srv
=
newLocalTestServer
(
s
.
T
(),
handler
)
s
.
svc
=
&
openaiOAuthService
{
tokenURL
:
s
.
srv
.
URL
}
}
...
...
backend/internal/repository/pricing_service_test.go
View file @
c86d445c
...
...
@@ -32,7 +32,7 @@ func (s *PricingServiceSuite) TearDownTest() {
}
func
(
s
*
PricingServiceSuite
)
setupServer
(
handler
http
.
HandlerFunc
)
{
s
.
srv
=
httptest
.
NewServer
(
handler
)
s
.
srv
=
newLocalTestServer
(
s
.
T
(),
handler
)
}
func
(
s
*
PricingServiceSuite
)
TestFetchPricingJSON_Success
()
{
...
...
backend/internal/repository/proxy_probe_service_test.go
View file @
c86d445c
...
...
@@ -31,7 +31,7 @@ func (s *ProxyProbeServiceSuite) TearDownTest() {
}
func
(
s
*
ProxyProbeServiceSuite
)
setupProxyServer
(
handler
http
.
HandlerFunc
)
{
s
.
proxySrv
=
httptest
.
NewServer
(
handler
)
s
.
proxySrv
=
newLocalTestServer
(
s
.
T
(),
handler
)
}
func
(
s
*
ProxyProbeServiceSuite
)
TestProbeProxy_InvalidProxyURL
()
{
...
...
backend/internal/repository/soft_delete_ent_integration_test.go
View file @
c86d445c
...
...
@@ -41,8 +41,8 @@ func TestEntSoftDelete_ApiKey_DefaultFilterAndSkip(t *testing.T) {
u
:=
createEntUser
(
t
,
ctx
,
client
,
uniqueSoftDeleteValue
(
t
,
"sd-user"
)
+
"@example.com"
)
repo
:=
NewA
pi
KeyRepository
(
client
)
key
:=
&
service
.
A
pi
Key
{
repo
:=
NewA
PI
KeyRepository
(
client
)
key
:=
&
service
.
A
PI
Key
{
UserID
:
u
.
ID
,
Key
:
uniqueSoftDeleteValue
(
t
,
"sk-soft-delete"
),
Name
:
"soft-delete"
,
...
...
@@ -53,13 +53,13 @@ func TestEntSoftDelete_ApiKey_DefaultFilterAndSkip(t *testing.T) {
require
.
NoError
(
t
,
repo
.
Delete
(
ctx
,
key
.
ID
),
"soft delete api key"
)
_
,
err
:=
repo
.
GetByID
(
ctx
,
key
.
ID
)
require
.
ErrorIs
(
t
,
err
,
service
.
ErrA
pi
KeyNotFound
,
"deleted rows should be hidden by default"
)
require
.
ErrorIs
(
t
,
err
,
service
.
ErrA
PI
KeyNotFound
,
"deleted rows should be hidden by default"
)
_
,
err
=
client
.
A
pi
Key
.
Query
()
.
Where
(
apikey
.
IDEQ
(
key
.
ID
))
.
Only
(
ctx
)
_
,
err
=
client
.
A
PI
Key
.
Query
()
.
Where
(
apikey
.
IDEQ
(
key
.
ID
))
.
Only
(
ctx
)
require
.
Error
(
t
,
err
,
"default ent query should not see soft-deleted rows"
)
require
.
True
(
t
,
dbent
.
IsNotFound
(
err
),
"expected ent not-found after default soft delete filter"
)
got
,
err
:=
client
.
A
pi
Key
.
Query
()
.
got
,
err
:=
client
.
A
PI
Key
.
Query
()
.
Where
(
apikey
.
IDEQ
(
key
.
ID
))
.
Only
(
mixins
.
SkipSoftDelete
(
ctx
))
require
.
NoError
(
t
,
err
,
"SkipSoftDelete should include soft-deleted rows"
)
...
...
@@ -73,8 +73,8 @@ func TestEntSoftDelete_ApiKey_DeleteIdempotent(t *testing.T) {
u
:=
createEntUser
(
t
,
ctx
,
client
,
uniqueSoftDeleteValue
(
t
,
"sd-user2"
)
+
"@example.com"
)
repo
:=
NewA
pi
KeyRepository
(
client
)
key
:=
&
service
.
A
pi
Key
{
repo
:=
NewA
PI
KeyRepository
(
client
)
key
:=
&
service
.
A
PI
Key
{
UserID
:
u
.
ID
,
Key
:
uniqueSoftDeleteValue
(
t
,
"sk-soft-delete2"
),
Name
:
"soft-delete2"
,
...
...
@@ -93,8 +93,8 @@ func TestEntSoftDelete_ApiKey_HardDeleteViaSkipSoftDelete(t *testing.T) {
u
:=
createEntUser
(
t
,
ctx
,
client
,
uniqueSoftDeleteValue
(
t
,
"sd-user3"
)
+
"@example.com"
)
repo
:=
NewA
pi
KeyRepository
(
client
)
key
:=
&
service
.
A
pi
Key
{
repo
:=
NewA
PI
KeyRepository
(
client
)
key
:=
&
service
.
A
PI
Key
{
UserID
:
u
.
ID
,
Key
:
uniqueSoftDeleteValue
(
t
,
"sk-soft-delete3"
),
Name
:
"soft-delete3"
,
...
...
@@ -105,10 +105,10 @@ func TestEntSoftDelete_ApiKey_HardDeleteViaSkipSoftDelete(t *testing.T) {
require
.
NoError
(
t
,
repo
.
Delete
(
ctx
,
key
.
ID
),
"soft delete api key"
)
// Hard delete using SkipSoftDelete so the hook doesn't convert it to update-deleted_at.
_
,
err
:=
client
.
A
pi
Key
.
Delete
()
.
Where
(
apikey
.
IDEQ
(
key
.
ID
))
.
Exec
(
mixins
.
SkipSoftDelete
(
ctx
))
_
,
err
:=
client
.
A
PI
Key
.
Delete
()
.
Where
(
apikey
.
IDEQ
(
key
.
ID
))
.
Exec
(
mixins
.
SkipSoftDelete
(
ctx
))
require
.
NoError
(
t
,
err
,
"hard delete"
)
_
,
err
=
client
.
A
pi
Key
.
Query
()
.
_
,
err
=
client
.
A
PI
Key
.
Query
()
.
Where
(
apikey
.
IDEQ
(
key
.
ID
))
.
Only
(
mixins
.
SkipSoftDelete
(
ctx
))
require
.
True
(
t
,
dbent
.
IsNotFound
(
err
),
"expected row to be hard deleted"
)
...
...
backend/internal/repository/temp_unsched_cache.go
0 → 100644
View file @
c86d445c
package
repository
import
(
"context"
"encoding/json"
"fmt"
"time"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/redis/go-redis/v9"
)
const
tempUnschedPrefix
=
"temp_unsched:account:"
var
tempUnschedSetScript
=
redis
.
NewScript
(
`
local key = KEYS[1]
local new_until = tonumber(ARGV[1])
local new_value = ARGV[2]
local new_ttl = tonumber(ARGV[3])
local existing = redis.call('GET', key)
if existing then
local ok, existing_data = pcall(cjson.decode, existing)
if ok and existing_data and existing_data.until_unix then
local existing_until = tonumber(existing_data.until_unix)
if existing_until and new_until <= existing_until then
return 0
end
end
end
redis.call('SET', key, new_value, 'EX', new_ttl)
return 1
`
)
type
tempUnschedCache
struct
{
rdb
*
redis
.
Client
}
func
NewTempUnschedCache
(
rdb
*
redis
.
Client
)
service
.
TempUnschedCache
{
return
&
tempUnschedCache
{
rdb
:
rdb
}
}
// SetTempUnsched 设置临时不可调度状态(只延长不缩短)
func
(
c
*
tempUnschedCache
)
SetTempUnsched
(
ctx
context
.
Context
,
accountID
int64
,
state
*
service
.
TempUnschedState
)
error
{
key
:=
fmt
.
Sprintf
(
"%s%d"
,
tempUnschedPrefix
,
accountID
)
stateJSON
,
err
:=
json
.
Marshal
(
state
)
if
err
!=
nil
{
return
fmt
.
Errorf
(
"marshal state: %w"
,
err
)
}
ttl
:=
time
.
Until
(
time
.
Unix
(
state
.
UntilUnix
,
0
))
if
ttl
<=
0
{
return
nil
// 已过期,不设置
}
ttlSeconds
:=
int
(
ttl
.
Seconds
())
if
ttlSeconds
<
1
{
ttlSeconds
=
1
}
_
,
err
=
tempUnschedSetScript
.
Run
(
ctx
,
c
.
rdb
,
[]
string
{
key
},
state
.
UntilUnix
,
string
(
stateJSON
),
ttlSeconds
)
.
Result
()
return
err
}
// GetTempUnsched 获取临时不可调度状态
func
(
c
*
tempUnschedCache
)
GetTempUnsched
(
ctx
context
.
Context
,
accountID
int64
)
(
*
service
.
TempUnschedState
,
error
)
{
key
:=
fmt
.
Sprintf
(
"%s%d"
,
tempUnschedPrefix
,
accountID
)
val
,
err
:=
c
.
rdb
.
Get
(
ctx
,
key
)
.
Result
()
if
err
==
redis
.
Nil
{
return
nil
,
nil
}
if
err
!=
nil
{
return
nil
,
err
}
var
state
service
.
TempUnschedState
if
err
:=
json
.
Unmarshal
([]
byte
(
val
),
&
state
);
err
!=
nil
{
return
nil
,
fmt
.
Errorf
(
"unmarshal state: %w"
,
err
)
}
return
&
state
,
nil
}
// DeleteTempUnsched 删除临时不可调度状态
func
(
c
*
tempUnschedCache
)
DeleteTempUnsched
(
ctx
context
.
Context
,
accountID
int64
)
error
{
key
:=
fmt
.
Sprintf
(
"%s%d"
,
tempUnschedPrefix
,
accountID
)
return
c
.
rdb
.
Del
(
ctx
,
key
)
.
Err
()
}
Prev
1
2
3
4
5
6
7
8
9
10
Next
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