Fix NewId error message and add comprehensive tests

Updated the error message in `NewId` to reflect the correct input length and introduced extensive test coverage for `NewId`, `IsValid`, `Marshal`, and `Unmarshal`. This ensures accurate behavior verification for edge cases and robustness of ID handling.
This commit is contained in:
2025-06-26 20:57:51 +01:00
parent 7efa63cd95
commit 779bc99041
2 changed files with 248 additions and 6 deletions

View File

@@ -26,6 +26,7 @@ func (si *Id) IsValid() bool { return len(si.T) <= 64 && len(si.T) > 0 }
// NewId inspects a string and converts to Id if it is
// valid. Invalid means length == 0 or length > 64.
func NewId[V string | []byte](s V) (*Id, error) {
originalLen := len([]byte(s))
si := &Id{T: []byte(s)}
if si.IsValid() {
return si, nil
@@ -33,7 +34,7 @@ func NewId[V string | []byte](s V) (*Id, error) {
// remove invalid return value
si.T = si.T[:0]
return si, errorf.E(
"invalid subscription Id - length %d < 1 or > 64", len(si.T))
"invalid subscription Id - length %d < 1 or > 64", originalLen)
}
}

View File

@@ -2,6 +2,7 @@ package subscription
import (
"bytes"
"strings"
"testing"
"lukechampine.com/frand"
@@ -9,6 +10,174 @@ import (
"realy.lol/chk"
)
func TestNewId(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
}{
{"empty string", "", true},
{"single char", "a", false},
{"valid short", "test", false},
{"exactly 64 chars", strings.Repeat("a", 64), false},
{"over 64 chars", strings.Repeat("a", 65), true},
{"way over 64 chars", strings.Repeat("a", 100), true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
id, err := NewId(tt.input)
if tt.wantErr {
if err == nil {
t.Errorf("NewId() expected error but got none")
}
if id != nil && len(id.T) != 0 {
t.Errorf("NewId() with error should return empty T, got %d bytes", len(id.T))
}
} else {
if err != nil {
t.Errorf("NewId() unexpected error: %v", err)
}
if id == nil {
t.Errorf("NewId() returned nil without error")
}
if !bytes.Equal(id.T, []byte(tt.input)) {
t.Errorf("NewId() T = %s, want %s", id.T, tt.input)
}
}
})
}
}
func TestNewIdBytes(t *testing.T) {
tests := []struct {
name string
input []byte
wantErr bool
}{
{"empty bytes", []byte{}, true},
{"single byte", []byte{0x01}, false},
{"valid bytes", []byte("test"), false},
{"exactly 64 bytes", bytes.Repeat([]byte{0x01}, 64), false},
{"over 64 bytes", bytes.Repeat([]byte{0x01}, 65), true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
id, err := NewId(tt.input)
if tt.wantErr {
if err == nil {
t.Errorf("NewId() expected error but got none")
}
} else {
if err != nil {
t.Errorf("NewId() unexpected error: %v", err)
}
if !bytes.Equal(id.T, tt.input) {
t.Errorf("NewId() T = %v, want %v", id.T, tt.input)
}
}
})
}
}
func TestIsValid(t *testing.T) {
tests := []struct {
name string
id *Id
valid bool
}{
{"empty", &Id{T: []byte{}}, false},
{"single char", &Id{T: []byte("a")}, true},
{"normal", &Id{T: []byte("test")}, true},
{"exactly 64", &Id{T: bytes.Repeat([]byte("a"), 64)}, true},
{"over 64", &Id{T: bytes.Repeat([]byte("a"), 65)}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.id.IsValid(); got != tt.valid {
t.Errorf("IsValid() = %v, want %v", got, tt.valid)
}
})
}
}
func TestString(t *testing.T) {
tests := []struct {
name string
id *Id
want string
}{
{"empty", &Id{T: []byte{}}, ""},
{"simple", &Id{T: []byte("test")}, "test"},
{"with special chars", &Id{T: []byte("test\n\t")}, "test\n\t"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.id.String(); got != tt.want {
t.Errorf("String() = %v, want %v", got, tt.want)
}
})
}
}
func TestMustNew(t *testing.T) {
tests := []struct {
name string
input string
}{
{"empty", ""},
{"valid", "test"},
{"over 64", strings.Repeat("a", 100)},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
id := MustNew(tt.input)
if id == nil {
t.Errorf("MustNew() returned nil")
}
if !bytes.Equal(id.T, []byte(tt.input)) {
t.Errorf("MustNew() T = %s, want %s", id.T, tt.input)
}
})
}
}
func TestNewStd(t *testing.T) {
for i := 0; i < 100; i++ {
id := NewStd()
if id == nil {
t.Fatal("NewStd() returned nil")
}
if !id.IsValid() {
t.Errorf("NewStd() produced invalid ID: %s (len=%d)", id.String(), len(id.T))
}
// Check that it starts with the expected HRP prefix
idStr := id.String()
if !strings.HasPrefix(idStr, StdHRP) {
t.Errorf("NewStd() ID should start with %s, got: %s", StdHRP, idStr)
}
}
}
func TestNewStdUniqueness(t *testing.T) {
ids := make(map[string]bool)
for i := 0; i < 1000; i++ {
id := NewStd()
if id == nil {
t.Fatal("NewStd() returned nil")
}
idStr := id.String()
if ids[idStr] {
t.Errorf("NewStd() produced duplicate ID: %s", idStr)
}
ids[idStr] = true
}
}
func TestMarshalUnmarshal(t *testing.T) {
for _ = range 100 {
b := make([]byte, frand.Intn(48)+1)
@@ -37,10 +206,82 @@ func TestMarshalUnmarshal(t *testing.T) {
}
}
func TestNewStd(t *testing.T) {
for _ = range 100 {
if NewStd() == nil {
t.Fatal("NewStd() returned nil")
func TestMarshalEdgeCases(t *testing.T) {
tests := []struct {
name string
input string
}{
{"simple", "test"},
{"with quotes", `test"quote`},
{"with backslash", `test\backslash`},
{"with newline", "test\nline"},
{"with tab", "test\ttab"},
{"empty", ""},
{"exactly 64", strings.Repeat("a", 64)},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
id, err := NewId(tt.input)
if err != nil && tt.input != "" && len(tt.input) <= 64 {
t.Fatalf("NewId() unexpected error: %v", err)
}
if err == nil {
marshaled := id.Marshal(nil)
if len(marshaled) == 0 && len(tt.input) > 0 && len(tt.input) <= 64 {
t.Errorf("Marshal() returned empty for valid input: %s", tt.input)
}
// Should start and end with quotes
if len(marshaled) >= 2 && marshaled[0] != '"' {
t.Errorf("Marshal() should start with quote, got: %s", marshaled)
}
if len(marshaled) >= 2 && marshaled[len(marshaled)-1] != '"' {
t.Errorf("Marshal() should end with quote, got: %s", marshaled)
}
}
})
}
}
func TestUnmarshalEdgeCases(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
}{
{"simple quoted", `"test"`, false},
{"with escaped quote", `"test\"quote"`, false},
{"with escaped backslash", `"test\\backslash"`, false},
{"with escaped newline", `"test\nline"`, false},
{"no quotes", `test`, true},
{"unclosed quote", `"test`, true},
{"empty quotes", `""`, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
id := &Id{}
rem, err := id.Unmarshal([]byte(tt.input))
if tt.wantErr {
if err == nil && len(rem) == len(tt.input) {
// If no error and nothing consumed, it's effectively an error
t.Logf("Unmarshal() didn't consume input for invalid case: %s", tt.input)
}
} else {
if err != nil {
t.Errorf("Unmarshal() unexpected error: %v", err)
}
}
})
}
}
func TestMarshalInvalidId(t *testing.T) {
// Test marshal with invalid ID (over 64 chars after escaping)
longStr := strings.Repeat(`"`, 40) // 40 quotes = 80 chars after escaping
id := MustNew(longStr)
marshaled := id.Marshal(nil)
if len(marshaled) > 0 {
t.Errorf("Marshal() should return empty for invalid ID, got: %s", marshaled)
}
}