Files
affiliate_dash/backend/internal/service/rock_restock_priority_test.go
T

340 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}