Add support for optional namespace prefixes on JetStreamEventStore streams to enable complete namespace isolation at the storage level: - Add Namespace field to JetStreamConfig - Add NewJetStreamEventStoreWithNamespace convenience constructor - Prefix stream names with sanitized namespace when configured - Add GetNamespace and GetStreamName accessor methods - Add unit tests for namespace functionality - Document namespace-scoped stores in CLAUDE.md The namespace prefix is sanitized (spaces, dots, wildcards converted to underscores) and prepended to the stream name, ensuring events from one namespace cannot be read from another namespace's store while maintaining full backward compatibility for non-namespaced stores. Closes #19 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
125 lines
3.2 KiB
Go
125 lines
3.2 KiB
Go
package store
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestJetStreamConfigNamespace(t *testing.T) {
|
|
t.Run("default config has empty namespace", func(t *testing.T) {
|
|
config := DefaultJetStreamConfig()
|
|
if config.Namespace != "" {
|
|
t.Errorf("expected empty namespace in default config, got %q", config.Namespace)
|
|
}
|
|
})
|
|
|
|
t.Run("namespace can be set in config", func(t *testing.T) {
|
|
config := JetStreamConfig{
|
|
Namespace: "tenant-abc",
|
|
}
|
|
if config.Namespace != "tenant-abc" {
|
|
t.Errorf("expected namespace tenant-abc, got %q", config.Namespace)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestNamespacedStreamName(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
baseStreamName string
|
|
namespace string
|
|
expectedStreamName string
|
|
}{
|
|
{
|
|
name: "no namespace - stream name unchanged",
|
|
baseStreamName: "events",
|
|
namespace: "",
|
|
expectedStreamName: "events",
|
|
},
|
|
{
|
|
name: "with namespace - prefixed stream name",
|
|
baseStreamName: "events",
|
|
namespace: "tenant-abc",
|
|
expectedStreamName: "tenant-abc_events",
|
|
},
|
|
{
|
|
name: "namespace with dots - sanitized",
|
|
baseStreamName: "events",
|
|
namespace: "tenant.abc",
|
|
expectedStreamName: "tenant_abc_events",
|
|
},
|
|
{
|
|
name: "namespace with spaces - sanitized",
|
|
baseStreamName: "events",
|
|
namespace: "tenant abc",
|
|
expectedStreamName: "tenant_abc_events",
|
|
},
|
|
{
|
|
name: "namespace with special chars - sanitized",
|
|
baseStreamName: "events",
|
|
namespace: "tenant*abc>def",
|
|
expectedStreamName: "tenant_abc_def_events",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// We can't create a real JetStreamEventStore without NATS,
|
|
// but we can test the stream name logic by examining the expected format
|
|
effectiveStreamName := tt.baseStreamName
|
|
if tt.namespace != "" {
|
|
effectiveStreamName = sanitizeSubject(tt.namespace) + "_" + tt.baseStreamName
|
|
}
|
|
|
|
if effectiveStreamName != tt.expectedStreamName {
|
|
t.Errorf("expected stream name %q, got %q", tt.expectedStreamName, effectiveStreamName)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSanitizeSubject(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected string
|
|
}{
|
|
{"simple", "simple"},
|
|
{"with spaces", "with_spaces"},
|
|
{"with.dots", "with_dots"},
|
|
{"with*stars", "with_stars"},
|
|
{"with>greater", "with_greater"},
|
|
{"complex.name with*special>chars", "complex_name_with_special_chars"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
result := sanitizeSubject(tt.input)
|
|
if result != tt.expected {
|
|
t.Errorf("sanitizeSubject(%q) = %q, want %q", tt.input, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExtractActorType(t *testing.T) {
|
|
tests := []struct {
|
|
actorID string
|
|
expectedType string
|
|
}{
|
|
{"order-123", "order"},
|
|
{"user-abc-def", "user"},
|
|
{"nodelimiter", "unknown"},
|
|
{"", "unknown"},
|
|
{"-leadingdash", "unknown"},
|
|
{"a-b", "a"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.actorID, func(t *testing.T) {
|
|
result := extractActorType(tt.actorID)
|
|
if result != tt.expectedType {
|
|
t.Errorf("extractActorType(%q) = %q, want %q", tt.actorID, result, tt.expectedType)
|
|
}
|
|
})
|
|
}
|
|
}
|