23dff69c62
- 新增 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>
246 lines
8.5 KiB
Go
246 lines
8.5 KiB
Go
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)
|
||
}
|