c553237f15
原 size:2048 在 utf8mb4 下产生 8192 字节索引,超过 InnoDB 3072 字节上限, 全新环境 AutoMigrate 必失败。改为 VARCHAR(768)(768×4=3072 字节,恰好 在上限内),schema.sql 同步更新列类型并将 uk_license_key 改为全列索引(无 需前缀)。新增测试 TestLicenseTokenFitsColumnLimit 验证最坏情况 token 长度 确实在 768 字符以内。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
111 lines
3.1 KiB
Go
111 lines
3.1 KiB
Go
package util
|
||
|
||
import (
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
func TestLicenseKeyRoundTrip(t *testing.T) {
|
||
priv, pub, err := GenerateEd25519KeyPair()
|
||
require.NoError(t, err)
|
||
|
||
exp := time.Now().Add(30 * 24 * time.Hour).Unix()
|
||
payload := LicensePayload{
|
||
ShopID: 42,
|
||
LicenseID: 7,
|
||
Type: "annual",
|
||
IssuedAt: time.Now().Unix(),
|
||
ExpiresAt: &exp,
|
||
MaxDevices: 3,
|
||
}
|
||
|
||
token, err := IssueLicenseToken(payload, priv)
|
||
require.NoError(t, err)
|
||
assert.NotEmpty(t, token)
|
||
|
||
got, err := VerifyLicenseToken(token, pub)
|
||
require.NoError(t, err)
|
||
assert.Equal(t, payload.ShopID, got.ShopID)
|
||
assert.Equal(t, payload.Type, got.Type)
|
||
assert.Equal(t, payload.MaxDevices, got.MaxDevices)
|
||
assert.Equal(t, *payload.ExpiresAt, *got.ExpiresAt)
|
||
}
|
||
|
||
func TestVerifyLicenseToken_TamperedPayload(t *testing.T) {
|
||
priv, pub, err := GenerateEd25519KeyPair()
|
||
require.NoError(t, err)
|
||
|
||
exp := time.Now().Add(30 * 24 * time.Hour).Unix()
|
||
token, err := IssueLicenseToken(LicensePayload{
|
||
ShopID: 1, Type: "trial", IssuedAt: time.Now().Unix(), ExpiresAt: &exp, MaxDevices: 1,
|
||
}, priv)
|
||
require.NoError(t, err)
|
||
|
||
// Flip the last byte of the signature to simulate tampering
|
||
tampered := token[:len(token)-2] + "XX"
|
||
_, err = VerifyLicenseToken(tampered, pub)
|
||
assert.Error(t, err, "tampered token must be rejected")
|
||
}
|
||
|
||
func TestVerifyLicenseToken_WrongKey(t *testing.T) {
|
||
priv, _, err := GenerateEd25519KeyPair()
|
||
require.NoError(t, err)
|
||
_, otherPub, err := GenerateEd25519KeyPair()
|
||
require.NoError(t, err)
|
||
|
||
exp := time.Now().Add(30 * 24 * time.Hour).Unix()
|
||
token, err := IssueLicenseToken(LicensePayload{
|
||
ShopID: 1, Type: "trial", IssuedAt: time.Now().Unix(), ExpiresAt: &exp, MaxDevices: 1,
|
||
}, priv)
|
||
require.NoError(t, err)
|
||
|
||
_, err = VerifyLicenseToken(token, otherPub)
|
||
assert.ErrorIs(t, err, ErrInvalidLicenseSignature)
|
||
}
|
||
|
||
func TestVerifyLicenseToken_InvalidFormat(t *testing.T) {
|
||
_, pub, err := GenerateEd25519KeyPair()
|
||
require.NoError(t, err)
|
||
|
||
_, err = VerifyLicenseToken("not-a-valid-token", pub)
|
||
assert.ErrorIs(t, err, ErrInvalidLicenseToken)
|
||
}
|
||
|
||
// TestLicenseTokenFitsColumnLimit verifies that a realistic (even worst-case)
|
||
// license token fits within the VARCHAR(768) column limit imposed by the
|
||
// InnoDB index constraint (768 chars × 4 bytes/char = 3072 bytes max).
|
||
func TestLicenseTokenFitsColumnLimit(t *testing.T) {
|
||
const columnLimit = 768
|
||
|
||
priv, _, err := GenerateEd25519KeyPair()
|
||
require.NoError(t, err)
|
||
|
||
// Use a large features map to simulate a worst-case payload.
|
||
exp := time.Now().Add(365 * 24 * time.Hour).Unix()
|
||
payload := LicensePayload{
|
||
ShopID: 999999999,
|
||
LicenseID: 999999999,
|
||
Type: "lifetime",
|
||
IssuedAt: time.Now().Unix(),
|
||
ExpiresAt: &exp,
|
||
MaxDevices: 99,
|
||
Features: map[string]any{
|
||
"finance": true,
|
||
"inventory": true,
|
||
"reports": true,
|
||
"export": true,
|
||
"multi_shop": true,
|
||
"api_access": true,
|
||
},
|
||
}
|
||
|
||
token, err := IssueLicenseToken(payload, priv)
|
||
require.NoError(t, err)
|
||
|
||
assert.LessOrEqual(t, len(token), columnLimit,
|
||
"license token length %d exceeds VARCHAR(%d) column limit", len(token), columnLimit)
|
||
}
|