276 lines
6.2 KiB
Go
276 lines
6.2 KiB
Go
// Copyright (c) 2026 Micha Hoiting
|
|
|
|
package serial_test
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"testing"
|
|
"testing/quick"
|
|
|
|
"git.hoiting.org/micha/triplex/serial"
|
|
)
|
|
|
|
// Property: Decode(Encode(code)) == code (for valid codes)
|
|
func TestEncodeDecodeRoundtrip(t *testing.T) {
|
|
f := func(idx uint32) bool {
|
|
if idx >= serial.MaxIndex {
|
|
return true
|
|
}
|
|
|
|
code, err := serial.Decode(idx)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
idx2, err := serial.Encode(code)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
return idx == idx2
|
|
}
|
|
|
|
if err := quick.Check(f, nil); err != nil {
|
|
t.Fatalf("Encode/Decode roundtrip failed: %v", err)
|
|
}
|
|
}
|
|
|
|
// Property: Decode(Encode(Decode(i))) == Decode(i) (idempotent on valid indices)
|
|
func TestDecodeEncodeDecodeIdempotent(t *testing.T) {
|
|
f := func(idx uint32) bool {
|
|
if idx >= serial.MaxIndex {
|
|
return true
|
|
}
|
|
|
|
code1, err := serial.Decode(idx)
|
|
if err != nil {
|
|
return true // decode may reject indices (e.g. forbidden pair)
|
|
}
|
|
|
|
idx2, err := serial.Encode(code1)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
code2, err := serial.Decode(idx2)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
return code1 == code2
|
|
}
|
|
|
|
if err := quick.Check(f, nil); err != nil {
|
|
t.Fatalf("Decode/Encode/Decode idempotence failed: %v", err)
|
|
}
|
|
}
|
|
|
|
// Property: forbidden first three letters are never returned by Decode
|
|
func TestForbiddenTripletsNeverDecoded(t *testing.T) {
|
|
f := func(idx uint32) bool {
|
|
if idx >= serial.MaxIndex {
|
|
return true
|
|
}
|
|
|
|
code, err := serial.Decode(idx)
|
|
if err != nil {
|
|
return true // decode may reject indices
|
|
}
|
|
|
|
l1 := code[0]
|
|
l2 := code[1]
|
|
l3 := code[2]
|
|
|
|
return !serial.IsForbiddenTriplet(l1, l2, l3)
|
|
}
|
|
|
|
if err := quick.Check(f, nil); err != nil {
|
|
t.Fatalf("Forbidden triplets appeared in Decode: %v", err)
|
|
}
|
|
}
|
|
|
|
// Optional: basic check for format LLL-NNN-LC
|
|
func TestDecodedFormatLooksCorrect(t *testing.T) {
|
|
f := func(idx uint32) bool {
|
|
if idx >= serial.MaxIndex {
|
|
return true
|
|
}
|
|
|
|
code, err := serial.Decode(idx)
|
|
if err != nil {
|
|
return true
|
|
}
|
|
|
|
if len(code) != 10 {
|
|
return false
|
|
}
|
|
if code[3] != '-' || code[7] != '-' {
|
|
return false
|
|
}
|
|
|
|
// letters at positions 0,1,2,8,9
|
|
for _, p := range []int{0, 1, 2, 8, 9} {
|
|
if code[p] < 'A' || code[p] > 'Z' {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// digits at positions 4,5,6
|
|
for _, p := range []int{4, 5, 6} {
|
|
if code[p] < '0' || code[p] > '9' {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
if err := quick.Check(f, nil); err != nil {
|
|
t.Fatalf("Decoded format property failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEncodeDecodeRoundtripSpecificIndex11223344(t *testing.T) {
|
|
baseIdx := uint32(0x11223344)
|
|
idx := baseIdx % serial.MaxIndex
|
|
|
|
code, err := serial.Decode(idx)
|
|
if err != nil {
|
|
t.Fatalf("Decode failed for idx %d (derived from 0x11223344): %v", idx, err)
|
|
}
|
|
|
|
idx2, err := serial.Encode(code)
|
|
if err != nil {
|
|
t.Fatalf("Encode failed for code %q: %v", code, err)
|
|
}
|
|
|
|
if idx2 != idx {
|
|
t.Fatalf("roundtrip mismatch: got %d, want %d", idx2, idx)
|
|
}
|
|
}
|
|
|
|
func TestDecodeOutOfRangeAtMaxIndex(t *testing.T) {
|
|
_, err := serial.Decode(serial.MaxIndex)
|
|
if !errors.Is(err, serial.ErrIndexOutOfRange) {
|
|
t.Fatalf("expected index out of range error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDecodeOutOfRangeAtMaxIndexPlusHundred(t *testing.T) {
|
|
_, err := serial.Decode(serial.MaxIndex + 100)
|
|
if !errors.Is(err, serial.ErrIndexOutOfRange) {
|
|
t.Fatalf("expected index out of range error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEncodeRejectsOldFormatWithThirdDash(t *testing.T) {
|
|
_, err := serial.Encode("AA-100-AA")
|
|
if !errors.Is(err, serial.ErrInvalidFormat) {
|
|
t.Fatalf("expected invalid format error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEncodeRejectsLowercaseLetters(t *testing.T) {
|
|
_, err := serial.Encode("AAa-100-AA")
|
|
if !errors.Is(err, serial.ErrInvalidLetters) {
|
|
t.Fatalf("expected invalid letters error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEncodeRejectsInvalidChecksum(t *testing.T) {
|
|
_, err := serial.Encode("AAA-100-AB")
|
|
if !errors.Is(err, serial.ErrInvalidChecksum) {
|
|
t.Fatalf("expected invalid checksum error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEncodeRejectsNonDigitInNumber(t *testing.T) {
|
|
_, err := serial.Encode("AAA-1A0-AA")
|
|
if !errors.Is(err, serial.ErrInvalidNumber) {
|
|
t.Fatalf("expected invalid number error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRandomCodeRoundtrip(t *testing.T) {
|
|
for i := 0; i < 100; i++ {
|
|
code, idx, err := serial.RandomCode()
|
|
if err != nil {
|
|
t.Fatalf("RandomCode failed: %v", err)
|
|
}
|
|
|
|
idx2, err := serial.Encode(code)
|
|
if err != nil {
|
|
t.Fatalf("Encode failed for random code %q: %v", code, err)
|
|
}
|
|
|
|
if idx2 != idx {
|
|
t.Fatalf("returned idx mismatch: got %d, want %d", idx, idx2)
|
|
}
|
|
|
|
decoded, err := serial.Decode(idx)
|
|
if err != nil {
|
|
t.Fatalf("Decode failed for idx %d: %v", idx, err)
|
|
}
|
|
|
|
if decoded != code {
|
|
t.Fatalf("random roundtrip mismatch: got %q, want %q", decoded, code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRandomCodeInvokesInUseCallback(t *testing.T) {
|
|
called := false
|
|
|
|
code, idx, err := serial.RandomCode(func(candidate uint32) bool {
|
|
called = true
|
|
return false
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("RandomCode failed: %v", err)
|
|
}
|
|
if !called {
|
|
t.Fatalf("expected in-use callback to be called")
|
|
}
|
|
|
|
idx2, err := serial.Encode(code)
|
|
if err != nil {
|
|
t.Fatalf("Encode failed for random code %q: %v", code, err)
|
|
}
|
|
if idx2 != idx {
|
|
t.Fatalf("returned idx mismatch: got %d, want %d", idx, idx2)
|
|
}
|
|
}
|
|
|
|
func TestRandomCodeFailsWhenAllIndicesInUse(t *testing.T) {
|
|
_, _, err := serial.RandomCode(func(candidate uint32) bool {
|
|
return true
|
|
})
|
|
if !errors.Is(err, serial.ErrNoAvailableIndex) {
|
|
t.Fatalf("expected no available index error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRandomCodeWithOptionsMaxAttempts(t *testing.T) {
|
|
_, _, err := serial.RandomCodeWithOptions(serial.RandomCodeOptions{
|
|
MaxAttempts: 1,
|
|
IsInUse: func(candidate uint32) bool {
|
|
return true
|
|
},
|
|
})
|
|
if !errors.Is(err, serial.ErrNoAvailableIndex) {
|
|
t.Fatalf("expected no available index error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRandomCodeWithOptionsRandomSourceFailure(t *testing.T) {
|
|
_, _, err := serial.RandomCodeWithOptions(serial.RandomCodeOptions{
|
|
RandomIndex: func(max uint32) (uint32, error) {
|
|
return 0, fmt.Errorf("rng down")
|
|
},
|
|
})
|
|
if !errors.Is(err, serial.ErrRandomSourceFailed) {
|
|
t.Fatalf("expected random source failed error, got: %v", err)
|
|
}
|
|
}
|