@@ -0,0 +1,339 @@
package service
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"affiliate_dash/internal/model"
"gorm.io/gorm"
)
// newRockRestockMockServer 模拟上游:刷新返回空对象(视为未绑定),提货详情返回固定绑定链接。
func newRockRestockMockServer ( t * testing . T ) * httptest . Server {
t . Helper ( )
return httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
w . Header ( ) . Set ( "Content-Type" , "application/json" )
switch {
case strings . HasPrefix ( r . URL . Path , "/api/redeem/" ) :
_ = json . NewEncoder ( w ) . Encode ( map [ string ] interface { } {
"id" : 151153 ,
"redeem_code" : "CODE-RESTOCK" ,
"bind_url" : "https://gp.qq.com/cp/mock_bind" ,
} )
case strings . HasSuffix ( r . URL . Path , "/refresh" ) :
_ = json . NewEncoder ( w ) . Encode ( map [ string ] interface { } { } )
default :
w . WriteHeader ( http . StatusNotFound )
}
} ) )
}
// mustCreateRestockToken 创建一条测试 Token,状态与上游编号可指定。
func mustCreateRestockToken ( t * testing . T , db * gorm . DB , upstreamID , redeemCode , status string ) model . RockToken {
t . Helper ( )
token := model . RockToken {
UpstreamTokenID : upstreamID ,
Token : "jwt-" + upstreamID ,
RedeemCode : redeemCode ,
Color : model . RockColorBlack ,
Status : status ,
}
if err := db . Create ( & token ) . Error ; err != nil {
t . Fatalf ( "创建测试 Token 失败: %v" , err )
}
return token
}
// TestReleaseUnboundUsingToken_NextAssignPrioritizesRestocked 验证人工释放回库存的 Token
// 会写入 restocked_at,并在下一次分配时优先于更早入库的普通 Token。
func TestReleaseUnboundUsingToken_NextAssignPrioritizesRestocked ( t * testing . T ) {
db := newRockTestDB ( t )
server := newRockRestockMockServer ( t )
defer server . Close ( )
svc := NewRockService ( db , NewRockClient ( server . URL ) )
setRockTestBearer ( t , svc , "bearer-test" )
ctx := context . Background ( )
// 先入库存的普通 Token(id 更小),正常情况下会先被分配。
mustCreateRestockToken ( t , db , "restock-normal" , "CODE-RESTOCK-NORMAL" , model . RockStatusAvailable )
usingAt := time . Now ( ) . Add ( - time . Hour )
releasedToken := model . RockToken {
UpstreamTokenID : "restock-released" ,
Token : "jwt-restock-released" ,
RedeemCode : "CODE-RESTOCK-RELEASED" ,
Color : model . RockColorBlack ,
Status : model . RockStatusUsing ,
AssignedOrderNo : "ORDER-RESTOCK-OLD" ,
ExpectedUID : "11111111" ,
UsingAt : & usingAt ,
}
if err := db . Create ( & releasedToken ) . Error ; err != nil {
t . Fatalf ( "创建使用中 Token 失败: %v" , err )
}
if err := svc . ReleaseUnboundUsingToken ( ctx , releasedToken . ID , false , nil ) ; err != nil {
t . Fatalf ( "释放回库存失败: %v" , err )
}
var released model . RockToken
if err := db . First ( & released , releasedToken . ID ) . Error ; err != nil {
t . Fatalf ( "读取释放后的 Token 失败: %v" , err )
}
if released . Status != model . RockStatusAvailable {
t . Fatalf ( "释放后应为 available,实际 %s" , released . Status )
}
if released . RestockedAt == nil {
t . Fatalf ( "人工释放回库存必须写入 restocked_at" )
}
assigned , _ , err := svc . AssignTokenForOrder ( ctx , "ORDER-RESTOCK-NEW" , "22222222" )
if err != nil {
t . Fatalf ( "分配 Token 失败: %v" , err )
}
if assigned . ID != releasedToken . ID {
t . Fatalf ( "应优先分配人工释放回库存的 Token(id=%d),实际选中 id=%d" , releasedToken . ID , assigned . ID )
}
if assigned . RestockedAt != nil {
t . Fatalf ( "分配后必须清空 restocked_at,实际 %v" , assigned . RestockedAt )
}
var stored model . RockToken
if err := db . First ( & stored , assigned . ID ) . Error ; err != nil {
t . Fatalf ( "读取分配结果失败: %v" , err )
}
if stored . Status != model . RockStatusUsing || stored . RestockedAt != nil {
t . Fatalf ( "数据库分配结果不符: status=%s restocked_at=%v" , stored . Status , stored . RestockedAt )
}
}
// TestReleaseUnboundUsingToken_ForceAlsoMarksRestocked 验证强制释放同样产生优先权。
func TestReleaseUnboundUsingToken_ForceAlsoMarksRestocked ( t * testing . T ) {
db := newRockTestDB ( t )
server := newBoundReleaseServer ( t )
defer server . Close ( )
svc := NewRockService ( db , NewRockClient ( server . URL ) )
setRockTestBearer ( t , svc , "bearer-test" )
ctx := context . Background ( )
usingAt := time . Now ( ) . Add ( - 30 * time . Minute )
token := model . RockToken {
UpstreamTokenID : "restock-force" ,
Token : "jwt-restock-force" ,
RedeemCode : "CODE-RESTOCK-FORCE" ,
Color : model . RockColorBlack ,
Status : model . RockStatusUsing ,
AssignedOrderNo : "ORDER-RESTOCK-FORCE" ,
ExpectedUID : "33333333" ,
UsingAt : & usingAt ,
}
if err := db . Create ( & token ) . Error ; err != nil {
t . Fatalf ( "创建使用中 Token 失败: %v" , err )
}
if err := svc . ReleaseUnboundUsingToken ( ctx , token . ID , true , nil ) ; err != nil {
t . Fatalf ( "强制释放失败: %v" , err )
}
var released model . RockToken
if err := db . First ( & released , token . ID ) . Error ; err != nil {
t . Fatalf ( "读取释放后的 Token 失败: %v" , err )
}
if released . Status != model . RockStatusAvailable || released . RestockedAt == nil {
t . Fatalf ( "强制释放后应回到 available 且写入 restocked_at,实际 status=%s restocked_at=%v" ,
released . Status , released . RestockedAt )
}
}
// TestAssignTokenForOrder_RestockedTokensFollowFIFO 验证多个释放 Token 按最早回库优先排序。
func TestAssignTokenForOrder_RestockedTokensFollowFIFO ( t * testing . T ) {
db := newRockTestDB ( t )
server := newRockRestockMockServer ( t )
defer server . Close ( )
svc := NewRockService ( db , NewRockClient ( server . URL ) )
setRockTestBearer ( t , svc , "bearer-test" )
ctx := context . Background ( )
// 普通库存 id 最小,但没有任何优先权,应排到最后。
plain := mustCreateRestockToken ( t , db , "fifo-plain" , "CODE-FIFO-PLAIN" , model . RockStatusAvailable )
later := mustCreateRestockToken ( t , db , "fifo-later" , "CODE-FIFO-LATER" , model . RockStatusAvailable )
earlier := mustCreateRestockToken ( t , db , "fifo-earlier" , "CODE-FIFO-EARLIER" , model . RockStatusAvailable )
if err := db . Model ( & model . RockToken { } ) . Where ( "id = ?" , later . ID ) .
Update ( "restocked_at" , time . Now ( ) . Add ( - time . Hour ) ) . Error ; err != nil {
t . Fatalf ( "写入较晚回库时间失败: %v" , err )
}
if err := db . Model ( & model . RockToken { } ) . Where ( "id = ?" , earlier . ID ) .
Update ( "restocked_at" , time . Now ( ) . Add ( - 2 * time . Hour ) ) . Error ; err != nil {
t . Fatalf ( "写入较早回库时间失败: %v" , err )
}
wantOrder := [ ] uint { earlier . ID , later . ID , plain . ID }
for i , wantID := range wantOrder {
orderNo := fmt . Sprintf ( "ORDER-FIFO-%d" , i )
assigned , _ , err := svc . AssignTokenForOrder ( ctx , orderNo , fmt . Sprintf ( "9000000%d" , i ) )
if err != nil {
t . Fatalf ( "第 %d 次分配失败: %v" , i + 1 , err )
}
if assigned . ID != wantID {
t . Fatalf ( "第 %d 次分配应选中 id=%d,实际 id=%d" , i + 1 , wantID , assigned . ID )
}
}
}
// TestResetMismatchToken_ClearsRestockPriority 验证解冻重置不产生优先权。
func TestResetMismatchToken_ClearsRestockPriority ( t * testing . T ) {
db := newRockTestDB ( t )
svc := NewRockService ( db , NewRockClient ( "https://example.com" ) )
ctx := context . Background ( )
stale := time . Now ( ) . Add ( - time . Hour )
token := model . RockToken {
UpstreamTokenID : "reset-restock" ,
Token : "jwt-reset-restock" ,
RedeemCode : "CODE-RESET-RESTOCK" ,
Color : model . RockColorBlack ,
Status : model . RockStatusMismatch ,
AssignedOrderNo : "ORDER-RESET-RESTOCK" ,
MismatchAt : & stale ,
MismatchReason : "历史不一致" ,
RestockedAt : & stale ,
}
if err := db . Create ( & token ) . Error ; err != nil {
t . Fatalf ( "创建 mismatch Token 失败: %v" , err )
}
if err := svc . ResetMismatchToken ( ctx , token . ID , nil ) ; err != nil {
t . Fatalf ( "解冻重置失败: %v" , err )
}
var reset model . RockToken
if err := db . First ( & reset , token . ID ) . Error ; err != nil {
t . Fatalf ( "读取重置结果失败: %v" , err )
}
if reset . Status != model . RockStatusAvailable {
t . Fatalf ( "重置后应为 available,实际 %s" , reset . Status )
}
if reset . RestockedAt != nil {
t . Fatalf ( "解冻重置不应产生优先权,restocked_at=%v" , reset . RestockedAt )
}
}
// TestUpdateTokenRemark_ManualAvailableClearsRestockPriority 验证人工备注改回可用不产生优先权。
func TestUpdateTokenRemark_ManualAvailableClearsRestockPriority ( t * testing . T ) {
db := newRockTestDB ( t )
svc := NewRockService ( db , NewRockClient ( "https://example.com" ) )
ctx := context . Background ( )
stale := time . Now ( ) . Add ( - time . Hour )
token := model . RockToken {
UpstreamTokenID : "remark-restock" ,
Token : "jwt-remark-restock" ,
RedeemCode : "CODE-REMARK-RESTOCK" ,
Color : model . RockColorBlack ,
Status : model . RockStatusAbnormal ,
RestockedAt : & stale ,
}
if err := db . Create ( & token ) . Error ; err != nil {
t . Fatalf ( "创建 abnormal Token 失败: %v" , err )
}
if _ , err := svc . UpdateTokenRemark ( ctx , token . ID , "人工改回可用" , model . RockStatusAvailable , nil ) ; err != nil {
t . Fatalf ( "人工改回可用失败: %v" , err )
}
var updated model . RockToken
if err := db . First ( & updated , token . ID ) . Error ; err != nil {
t . Fatalf ( "读取更新结果失败: %v" , err )
}
if updated . Status != model . RockStatusAvailable {
t . Fatalf ( "人工更新后应为 available,实际 %s" , updated . Status )
}
if updated . RestockedAt != nil {
t . Fatalf ( "人工改回可用不应产生优先权,restocked_at=%v" , updated . RestockedAt )
}
}
// TestRockProviderCancel_ClearsRestockPriority 验证订单取消回滚不产生优先权。
func TestRockProviderCancel_ClearsRestockPriority ( t * testing . T ) {
db := newRockTestDB ( t )
svc := NewRockService ( db , NewRockClient ( "https://example.com" ) )
provider := NewRockProvider ( svc )
ctx := context . Background ( )
stale := time . Now ( ) . Add ( - time . Hour )
token := model . RockToken {
UpstreamTokenID : "cancel-restock" ,
Token : "jwt-cancel-restock" ,
RedeemCode : "CODE-CANCEL-RESTOCK" ,
Color : model . RockColorBlack ,
Status : model . RockStatusUsing ,
AssignedOrderNo : "ORDER-CANCEL-RESTOCK" ,
UsingAt : & stale ,
RestockedAt : & stale ,
}
if err := db . Create ( & token ) . Error ; err != nil {
t . Fatalf ( "创建使用中 Token 失败: %v" , err )
}
if err := provider . Cancel ( ctx , & model . FulfillmentOrder { OrderNo : "ORDER-CANCEL-RESTOCK" } ) ; err != nil {
t . Fatalf ( "取消订单回滚失败: %v" , err )
}
var canceled model . RockToken
if err := db . First ( & canceled , token . ID ) . Error ; err != nil {
t . Fatalf ( "读取取消结果失败: %v" , err )
}
if canceled . Status != model . RockStatusAvailable {
t . Fatalf ( "取消后应为 available,实际 %s" , canceled . Status )
}
if canceled . RestockedAt != nil {
t . Fatalf ( "订单取消回滚不应产生优先权,restocked_at=%v" , canceled . RestockedAt )
}
}
// TestRestoreRockTokens_ClearsRestockPriority 验证回收站恢复不产生优先权。
func TestRestoreRockTokens_ClearsRestockPriority ( t * testing . T ) {
db := newRockTestDB ( t )
svc := NewRockService ( db , NewRockClient ( "https://example.com" ) )
ctx := context . Background ( )
stale := time . Now ( ) . Add ( - time . Hour )
token := mustCreateRestockToken ( t , db , "restore-restock" , "CODE-RESTORE-RESTOCK" , model . RockStatusAvailable )
if err := db . Model ( & model . RockToken { } ) . Where ( "id = ?" , token . ID ) .
Update ( "restocked_at" , stale ) . Error ; err != nil {
t . Fatalf ( "写入历史优先权失败: %v" , err )
}
if err := db . Delete ( & token ) . Error ; err != nil {
t . Fatalf ( "软删除 Token 失败: %v" , err )
}
result , err := svc . RestoreRockTokens ( ctx , [ ] uint { token . ID } , 42 )
if err != nil {
t . Fatalf ( "恢复回收站 Token 失败: %v" , err )
}
if result . Restored != 1 {
t . Fatalf ( "期望恢复 1 条,实际 %d 条(跳过 %+v) " , result . Restored , result . SkippedItems )
}
var restored model . RockToken
if err := db . First ( & restored , token . ID ) . Error ; err != nil {
t . Fatalf ( "读取恢复结果失败: %v" , err )
}
if restored . DeletedAt . Valid {
t . Fatalf ( "恢复后 deleted_at 必须为空" )
}
if restored . RestockedAt != nil {
t . Fatalf ( "回收站恢复不应产生优先权,restocked_at=%v" , restored . RestockedAt )
}
}