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:
@@ -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
|
// NewId inspects a string and converts to Id if it is
|
||||||
// valid. Invalid means length == 0 or length > 64.
|
// valid. Invalid means length == 0 or length > 64.
|
||||||
func NewId[V string | []byte](s V) (*Id, error) {
|
func NewId[V string | []byte](s V) (*Id, error) {
|
||||||
|
originalLen := len([]byte(s))
|
||||||
si := &Id{T: []byte(s)}
|
si := &Id{T: []byte(s)}
|
||||||
if si.IsValid() {
|
if si.IsValid() {
|
||||||
return si, nil
|
return si, nil
|
||||||
@@ -33,7 +34,7 @@ func NewId[V string | []byte](s V) (*Id, error) {
|
|||||||
// remove invalid return value
|
// remove invalid return value
|
||||||
si.T = si.T[:0]
|
si.T = si.T[:0]
|
||||||
return si, errorf.E(
|
return si, errorf.E(
|
||||||
"invalid subscription Id - length %d < 1 or > 64", len(si.T))
|
"invalid subscription Id - length %d < 1 or > 64", originalLen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package subscription
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"lukechampine.com/frand"
|
"lukechampine.com/frand"
|
||||||
@@ -9,6 +10,174 @@ import (
|
|||||||
"realy.lol/chk"
|
"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) {
|
func TestMarshalUnmarshal(t *testing.T) {
|
||||||
for _ = range 100 {
|
for _ = range 100 {
|
||||||
b := make([]byte, frand.Intn(48)+1)
|
b := make([]byte, frand.Intn(48)+1)
|
||||||
@@ -37,10 +206,82 @@ func TestMarshalUnmarshal(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewStd(t *testing.T) {
|
func TestMarshalEdgeCases(t *testing.T) {
|
||||||
for _ = range 100 {
|
tests := []struct {
|
||||||
if NewStd() == nil {
|
name string
|
||||||
t.Fatal("NewStd() returned nil")
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user