// 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 TestCompleteCodeRoundtrip(t *testing.T) { original, err := serial.Decode(123456) if err != nil { t.Fatalf("Decode failed: %v", err) } withoutChecksum := original[:len(original)-1] full, idx, err := serial.CompleteCode(withoutChecksum) if err != nil { t.Fatalf("CompleteCode failed: %v", err) } if full != original { t.Fatalf("completed code mismatch: got %q, want %q", full, original) } idx2, err := serial.Encode(full) if err != nil { t.Fatalf("Encode failed for completed code %q: %v", full, err) } if idx2 != idx { t.Fatalf("idx mismatch: got %d, want %d", idx, idx2) } } func TestCompleteCodeRejectsInvalidFormat(t *testing.T) { _, _, err := serial.CompleteCode("AAA-10-A") if !errors.Is(err, serial.ErrInvalidFormatNoChecksum) { t.Fatalf("expected invalid no-checksum format 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) } }