Files
aether/store/namespace_test.go
Hugo Nijhuis f62964bf3b
All checks were successful
CI / build (pull_request) Successful in 15s
CI / build (push) Successful in 16s
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>
2026-01-10 19:01:03 +01:00

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)
}
})
}
}