Add namespace-scoped event stores for storage isolation
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>
This commit was merged in pull request #48.
This commit is contained in:
124
store/namespace_test.go
Normal file
124
store/namespace_test.go
Normal file
@@ -0,0 +1,124 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user