Add ValidateSessionID
diff --git a/skabandclient/skabandclient.go b/skabandclient/skabandclient.go
index 7295a7b..3452c55 100644
--- a/skabandclient/skabandclient.go
+++ b/skabandclient/skabandclient.go
@@ -20,6 +20,7 @@
"net/url"
"os"
"path/filepath"
+ "regexp"
"strings"
"sync"
"sync/atomic"
@@ -268,6 +269,16 @@
return s[0:4] + "-" + s[4:8] + "-" + s[8:12] + "-" + s[12:16]
}
+// Regex pattern for SessionID format: xxxx-xxxx-xxxx-xxxx
+// Where x is a valid Crockford Base32 character (0-9, A-H, J-N, P-Z)
+// Case-insensitive match
+var sessionIdRegexp = regexp.MustCompile(
+ "^[0-9A-HJ-NP-Za-hj-np-z]{4}-[0-9A-HJ-NP-Za-hj-np-z]{4}-[0-9A-HJ-NP-Za-hj-np-z]{4}-[0-9A-HJ-NP-Za-hj-np-z]{4}")
+
+func ValidateSessionID(sessionID string) bool {
+ return sessionIdRegexp.MatchString(sessionID)
+}
+
// Addr returns the skaband server address
func (c *SkabandClient) Addr() string {
if c == nil {
diff --git a/skabandclient/skabandclient_test.go b/skabandclient/skabandclient_test.go
new file mode 100644
index 0000000..4ed83d4
--- /dev/null
+++ b/skabandclient/skabandclient_test.go
@@ -0,0 +1,118 @@
+package skabandclient
+
+import (
+ "testing"
+)
+
+// TestValidateSessionID_Roundtrip tests that all session IDs generated by NewSessionID
+// pass validation by ValidateSessionID
+func TestValidateSessionID_Roundtrip(t *testing.T) {
+ // Test multiple generated session IDs to ensure consistent behavior
+ for i := range 1000 {
+ sessionID := NewSessionID()
+ if !ValidateSessionID(sessionID) {
+ t.Errorf("Generated session ID %q failed validation (iteration %d)", sessionID, i)
+ }
+ }
+}
+
+// TestValidateSessionID_ValidCases tests various valid session ID formats
+func TestValidateSessionID_ValidCases(t *testing.T) {
+ tests := []struct {
+ name string
+ sessionID string
+ want bool
+ }{
+ {"uppercase letters", "ABCD-EFGH-JKMN-PQRS", true},
+ {"lowercase letters", "abcd-efgh-jkmn-pqrs", true},
+ {"mixed case", "AbCd-EfGh-JkMn-PqRs", true},
+ {"with numbers", "123A-5678-9BCD-EF0H", true},
+ {"all numbers", "1234-5678-9012-3456", true},
+ {"boundary chars lower", "0000-0000-0000-0000", true},
+ {"boundary chars upper A-H", "ABCD-EFGH-ABCD-EFGH", true},
+ {"boundary chars J-N", "JKMN-JKMN-JKMN-JKMN", true},
+ {"boundary chars P-Z", "PQRS-TUVW-XYZ0-1234", true},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := ValidateSessionID(tt.sessionID)
+ if got != tt.want {
+ t.Errorf("ValidateSessionID(%q) = %v, want %v", tt.sessionID, got, tt.want)
+ }
+ })
+ }
+}
+
+// TestValidateSessionID_InvalidCases tests various invalid session ID formats
+func TestValidateSessionID_InvalidCases(t *testing.T) {
+ tests := []struct {
+ name string
+ sessionID string
+ want bool
+ }{
+ {"empty string", "", false},
+ {"too short", "ABC-DEF-GHI-JKL", false},
+ {"too long", "ABCDE-EFGH-JKMN-PQRS", false},
+ {"missing dashes", "ABCDEFGHJKMNPQRS", false},
+ {"wrong dash positions", "AB-CD-EFGH-JKMNPQRS", false},
+ {"invalid chars I", "ABCI-EFGH-JKMN-PQRS", false},
+ {"invalid chars O", "ABCO-EFGH-JKMN-PQRS", false},
+ {"invalid chars lowercase i", "ABCi-EFGH-JKMN-PQRS", false},
+ {"invalid chars lowercase o", "ABCo-EFGH-JKMN-PQRS", false},
+ {"special characters", "ABC!-EFGH-JKMN-PQRS", false},
+ {"spaces", "ABC -EFGH-JKMN-PQRS", false},
+ {"extra dashes", "ABCD--EFGH-JKMN-PQRS", false},
+ {"underscore instead of dash", "ABCD_EFGH_JKMN_PQRS", false},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := ValidateSessionID(tt.sessionID)
+ if got != tt.want {
+ t.Errorf("ValidateSessionID(%q) = %v, want %v", tt.sessionID, got, tt.want)
+ }
+ })
+ }
+}
+
+// TestNewSessionID_Format tests that NewSessionID generates properly formatted IDs
+func TestNewSessionID_Format(t *testing.T) {
+ // Generate multiple session IDs and verify they all have the correct format
+ for range 100 {
+ sessionID := NewSessionID()
+
+ // Check length
+ if len(sessionID) != 19 { // xxxx-xxxx-xxxx-xxxx = 19 characters
+ t.Errorf("Generated session ID %q has wrong length %d, expected 19", sessionID, len(sessionID))
+ }
+
+ // Check dash positions
+ if sessionID[4] != '-' || sessionID[9] != '-' || sessionID[14] != '-' {
+ t.Errorf("Generated session ID %q has wrong dash positions", sessionID)
+ }
+
+ // Should pass validation
+ if !ValidateSessionID(sessionID) {
+ t.Errorf("Generated session ID %q failed validation", sessionID)
+ }
+ }
+}
+
+// TestNewSessionID_Uniqueness tests that NewSessionID generates unique IDs
+func TestNewSessionID_Uniqueness(t *testing.T) {
+ seen := make(map[string]bool)
+ count := 10000
+
+ for range count {
+ sessionID := NewSessionID()
+ if seen[sessionID] {
+ t.Errorf("Duplicate session ID generated: %q", sessionID)
+ }
+ seen[sessionID] = true
+ }
+
+ if len(seen) != count {
+ t.Errorf("Expected %d unique session IDs, got %d", count, len(seen))
+ }
+}