Files
jiu/backend/internal/handler/license_test.go
T
wangjia 23dff69c62 feat(backend): 授权改为时长兑换券体系 + 退役 ed25519/HMAC + 平台生码工具
- 新增 license_codes 码池表 + model.LicenseCode;licenses 加 tier 档位列
- LicenseService.Redeem:单事务 FOR UPDATE 校验码未用 → 时长叠加(可叠加,0=永久)
  → 写 type/tier/max_devices → 绑设备(超限整笔回滚) → 标记已用 → 即时失效 phase 缓存
  路由仍 POST /license/activate,客户端零破坏
- util.GenerateRedeemCode/NormalizeCode:JIUKU-XXXX-XXXX 短码(crypto/rand)
- cmd/gencode:平台批量生成兑换码并落库;删除 cmd/issue、cmd/genkey
- 退役 ed25519 + HMAC:删 util/license_key、GenerateKey、License 全部 config 字段
  及生产启动私钥校验;trial 改直接建行(无需私钥、去 Fatal)
- tier 档位钩子默认 standard,分档消费模式后续设计
- 测试:Redeem 全场景(叠加/过期重置/永久/一码一次/无效/设备上限回滚)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 12:14:27 +08:00

246 lines
8.5 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 handler
import (
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/wangjia/jiu/backend/internal/model"
"github.com/wangjia/jiu/backend/testutil"
)
func TestLicenseHandler_Activate_Success(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "LH001")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
// 码池里放一张未用兑换码(30 天)
require.NoError(t, db.Create(&model.LicenseCode{
Code: "JIUKUTEST0001", Type: "annual", Tier: "standard",
DurationDays: 30, MaxDevices: 3, Status: "unused",
}).Error)
// 请求带连字符/会被归一化为 JIUKUTEST0001
w := makeRequest(r, "POST", "/api/v1/license/activate", token, map[string]interface{}{
"license_key": "JIUKU-TEST-0001",
"device_id": "device-123",
"device_name": "Test Machine",
"platform": "windows",
})
assert.Equal(t, http.StatusOK, w.Code)
// 兑换后门店生成授权行 + 设备绑定
var lic model.License
require.NoError(t, db.Where("shop_id = ?", shop.ID).First(&lic).Error)
var dev model.LicenseDevice
require.NoError(t, db.Where("license_id = ? AND device_id = ?", lic.ID, "device-123").First(&dev).Error)
assert.Equal(t, "Test Machine", dev.DeviceName)
// 码被标记已用
var lc model.LicenseCode
require.NoError(t, db.Where("code = ?", "JIUKUTEST0001").First(&lc).Error)
assert.Equal(t, "redeemed", lc.Status)
}
func TestLicenseHandler_Activate_MissingFields(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "LH002")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
// 缺少 device_id
w := makeRequest(r, "POST", "/api/v1/license/activate", token, map[string]interface{}{
"license_key": "LHACT-BBBBB-CCCCC-DDDDD",
})
assert.Equal(t, http.StatusBadRequest, w.Code)
// 缺少 license_key
w = makeRequest(r, "POST", "/api/v1/license/activate", token, map[string]interface{}{
"device_id": "device-123",
})
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestLicenseHandler_Activate_NotFound(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "LH003")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
w := makeRequest(r, "POST", "/api/v1/license/activate", token, map[string]interface{}{
"license_key": "NONEX-ISTEN-TTTTT-LICCC",
"device_id": "device-123",
})
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestLicenseHandler_Activate_DeviceLimitExceeded(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "LH004")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
// 既有授权:1 个设备名额已占满
future := time.Now().Add(30 * 24 * time.Hour)
lic := &model.License{
ShopID: shop.ID, LicenseKey: "TRIAL-LH004", IsActive: true, MaxDevices: 1, ExpiresAt: &future,
}
require.NoError(t, db.Create(lic).Error)
require.NoError(t, db.Create(&model.LicenseDevice{
LicenseID: lic.ID, ShopID: shop.ID, DeviceID: "existing-device",
}).Error)
// 码不提升设备上限(max_devices=0
require.NoError(t, db.Create(&model.LicenseCode{
Code: "JIUKUDEVLIMIT1", Type: "annual", Tier: "standard",
DurationDays: 365, MaxDevices: 0, Status: "unused",
}).Error)
w := makeRequest(r, "POST", "/api/v1/license/activate", token, map[string]interface{}{
"license_key": "JIUKU-DEVL-IMIT1",
"device_id": "different-device",
})
assert.Equal(t, http.StatusBadRequest, w.Code)
// 设备超限 → 整笔回滚:码仍未使用
var lc model.LicenseCode
require.NoError(t, db.Where("code = ?", "JIUKUDEVLIMIT1").First(&lc).Error)
assert.Equal(t, "unused", lc.Status)
}
func TestLicenseHandler_Activate_NoAuth(t *testing.T) {
db := testutil.SetupTestDB()
r := setupProtectedRouter(db)
w := makeRequest(r, "POST", "/api/v1/license/activate", "", map[string]interface{}{
"license_key": "XXXXX-XXXXX-XXXXX-XXXXX",
"device_id": "device-123",
})
assert.Equal(t, http.StatusUnauthorized, w.Code)
}
func TestLicenseHandler_Verify_Success(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "LH006")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
expiry := time.Now().Add(30 * 24 * time.Hour)
lic := &model.License{
ShopID: shop.ID, LicenseKey: "LHVFY-BBBBB-CCCCC-DDDDD", IsActive: true, ExpiresAt: &expiry,
}
require.NoError(t, db.Create(lic).Error)
require.NoError(t, db.Create(&model.LicenseDevice{
LicenseID: lic.ID, ShopID: shop.ID, DeviceID: "my-device",
}).Error)
w := makeRequest(r, "GET", "/api/v1/license/verify?device_id=my-device", token, nil)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestLicenseHandler_Verify_MissingDeviceID(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "LH007")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
w := makeRequest(r, "GET", "/api/v1/license/verify", token, nil)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestLicenseHandler_Verify_NotFound(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "LH008")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
w := makeRequest(r, "GET", "/api/v1/license/verify?device_id=nonexistent-device", token, nil)
assert.Equal(t, http.StatusForbidden, w.Code)
}
func TestLicenseHandler_Verify_Expired(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "LH009")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
expiry := time.Now().Add(-1 * time.Hour)
lic := &model.License{
ShopID: shop.ID, LicenseKey: "LHVEX-BBBBB-CCCCC-DDDDD", IsActive: true, ExpiresAt: &expiry,
}
require.NoError(t, db.Create(lic).Error)
require.NoError(t, db.Create(&model.LicenseDevice{
LicenseID: lic.ID, ShopID: shop.ID, DeviceID: "expired-device",
}).Error)
w := makeRequest(r, "GET", "/api/v1/license/verify?device_id=expired-device", token, nil)
assert.Equal(t, http.StatusForbidden, w.Code)
}
func TestLicenseHandler_Verify_NoAuth(t *testing.T) {
db := testutil.SetupTestDB()
r := setupProtectedRouter(db)
w := makeRequest(r, "GET", "/api/v1/license/verify?device_id=any", "", nil)
assert.Equal(t, http.StatusUnauthorized, w.Code)
}
func TestLicenseHandler_Deactivate_Success(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "LH010")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
expiry := time.Now().Add(30 * 24 * time.Hour)
lic := &model.License{
ShopID: shop.ID, LicenseKey: "LHDAC-BBBBB-CCCCC-DDDDD", IsActive: true, ExpiresAt: &expiry,
}
require.NoError(t, db.Create(lic).Error)
require.NoError(t, db.Create(&model.LicenseDevice{
LicenseID: lic.ID, ShopID: shop.ID, DeviceID: "deactivate-device",
}).Error)
w := makeRequest(r, "POST", "/api/v1/license/deactivate", token, map[string]interface{}{
"device_id": "deactivate-device",
})
assert.Equal(t, http.StatusOK, w.Code)
// Verify device was removed
var count int64
db.Model(&model.LicenseDevice{}).Where("shop_id = ? AND device_id = ?", shop.ID, "deactivate-device").Count(&count)
assert.Equal(t, int64(0), count)
}
func TestLicenseHandler_Deactivate_MissingDeviceID(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "LH011")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
w := makeRequest(r, "POST", "/api/v1/license/deactivate", token, map[string]interface{}{})
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestLicenseHandler_Deactivate_NoAuth(t *testing.T) {
db := testutil.SetupTestDB()
r := setupProtectedRouter(db)
w := makeRequest(r, "POST", "/api/v1/license/deactivate", "", map[string]interface{}{
"device_id": "any-device",
})
assert.Equal(t, http.StatusUnauthorized, w.Code)
}