mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-19 17:50:12 +00:00
New pkg/idcodec wraps sqids with an alphabet derived from the JWT secret via HKDF-SHA256 (info="nezha/idcodec/alphabet/v1"). Rotating NZ_JWTSECRETKEY automatically reshuffles the alphabet, which doubles as a kill switch for outstanding hashids without touching the encoder itself. The base alphabet drops visually-confusable characters (0/O/o/I/l/1) and MinLength=8 hides small integer ids. Decode round-trips through Encode to reject inputs that decode by accident under the same alphabet. Co-authored-by: cloudcode <cloudcode@users.noreply.github.com>
131 lines
2.9 KiB
Go
131 lines
2.9 KiB
Go
package idcodec
|
|
|
|
import (
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
const testMasterKey = "this-is-a-32-byte-master-key-ok!"
|
|
|
|
func resetEncoder(t *testing.T) {
|
|
t.Helper()
|
|
mu.Lock()
|
|
encoder = nil
|
|
mu.Unlock()
|
|
}
|
|
|
|
func TestEncodeDecodeRoundTrip(t *testing.T) {
|
|
resetEncoder(t)
|
|
if err := Init([]byte(testMasterKey)); err != nil {
|
|
t.Fatalf("Init: %v", err)
|
|
}
|
|
|
|
cases := []uint64{1, 2, 42, 1_000_000, 1<<63 - 1}
|
|
for _, id := range cases {
|
|
code, err := Encode(id)
|
|
if err != nil {
|
|
t.Fatalf("Encode(%d): %v", id, err)
|
|
}
|
|
if len(code) < minLength {
|
|
t.Fatalf("code %q shorter than min %d", code, minLength)
|
|
}
|
|
got, err := Decode(code)
|
|
if err != nil {
|
|
t.Fatalf("Decode(%q): %v", code, err)
|
|
}
|
|
if got != id {
|
|
t.Fatalf("round-trip mismatch: got %d, want %d", got, id)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEncodeBeforeInit(t *testing.T) {
|
|
resetEncoder(t)
|
|
if _, err := Encode(1); err != ErrNotInitialized {
|
|
t.Fatalf("Encode without Init: want ErrNotInitialized, got %v", err)
|
|
}
|
|
if _, err := Decode("abcdefgh"); err != ErrNotInitialized {
|
|
t.Fatalf("Decode without Init: want ErrNotInitialized, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestInitRejectsShortMasterKey(t *testing.T) {
|
|
resetEncoder(t)
|
|
if err := Init([]byte("too-short")); err != ErrMasterKeyShort {
|
|
t.Fatalf("Init short master key: want ErrMasterKeyShort, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDecodeInvalidInputs(t *testing.T) {
|
|
resetEncoder(t)
|
|
if err := Init([]byte(testMasterKey)); err != nil {
|
|
t.Fatalf("Init: %v", err)
|
|
}
|
|
|
|
for _, code := range []string{"", "@@@@", strings.Repeat("!", 16)} {
|
|
if _, err := Decode(code); err == nil {
|
|
t.Fatalf("Decode(%q) must fail", code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAlphabetChangesWithMasterKey(t *testing.T) {
|
|
resetEncoder(t)
|
|
if err := Init([]byte(testMasterKey)); err != nil {
|
|
t.Fatalf("Init A: %v", err)
|
|
}
|
|
codeA, err := Encode(42)
|
|
if err != nil {
|
|
t.Fatalf("Encode A: %v", err)
|
|
}
|
|
|
|
resetEncoder(t)
|
|
if err := Init([]byte(testMasterKey + "rotated-suffix-makes-key-longer!")); err != nil {
|
|
t.Fatalf("Init B: %v", err)
|
|
}
|
|
codeB, err := Encode(42)
|
|
if err != nil {
|
|
t.Fatalf("Encode B: %v", err)
|
|
}
|
|
if codeA == codeB {
|
|
t.Fatalf("rotating master key must change hashid encoding for the same id; both produced %q", codeA)
|
|
}
|
|
|
|
if _, err := Decode(codeA); err == nil {
|
|
t.Fatalf("after rotation, old hashid %q must not decode under new key", codeA)
|
|
}
|
|
}
|
|
|
|
func TestConcurrentEncodeDecodeIsSafe(t *testing.T) {
|
|
resetEncoder(t)
|
|
if err := Init([]byte(testMasterKey)); err != nil {
|
|
t.Fatalf("Init: %v", err)
|
|
}
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 16; i++ {
|
|
wg.Add(1)
|
|
go func(seed uint64) {
|
|
defer wg.Done()
|
|
for j := uint64(0); j < 1000; j++ {
|
|
id := seed*1000 + j
|
|
code, err := Encode(id)
|
|
if err != nil {
|
|
t.Errorf("Encode(%d): %v", id, err)
|
|
return
|
|
}
|
|
got, err := Decode(code)
|
|
if err != nil {
|
|
t.Errorf("Decode(%q): %v", code, err)
|
|
return
|
|
}
|
|
if got != id {
|
|
t.Errorf("round-trip: got %d, want %d", got, id)
|
|
return
|
|
}
|
|
}
|
|
}(uint64(i))
|
|
}
|
|
wg.Wait()
|
|
}
|