Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,10 @@ synchronization:
host: "redis.demo.svc:6379"
password: ""
database: 0
## channel: "optimizely-sync" # Base channel name (NOT currently parsed - uses hardcoded default)
## Agent publishes to channels: "optimizely-sync-{sdk_key}"
## For external Redis clients: Subscribe "optimizely-sync-{sdk_key}" or PSubscribe "optimizely-sync-*"
## Note: Channel configuration parsing is a known bug - planned for future release
## if notification synchronization is enabled, then the active notification event-stream API
## will get the notifications from available replicas
notification:
Expand Down
4 changes: 4 additions & 0 deletions pkg/handlers/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ func NotificationEventStreamHandler(notificationReceiverFn NotificationReceiverF
notify := r.Context().Done()

sdkKey := r.Header.Get(middleware.OptlySDKHeader)
// Parse out the SDK key if it includes a secure token (format: sdkKey:apiKey)
if idx := strings.Index(sdkKey, ":"); idx != -1 {
sdkKey = sdkKey[:idx]
}
ctx := context.WithValue(r.Context(), SDKKey, sdkKey)

dataChan, err := notificationReceiverFn(context.WithValue(ctx, LoggerKey, middleware.GetLogger(r)))
Expand Down
121 changes: 121 additions & 0 deletions pkg/handlers/notification_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -503,3 +503,124 @@
return dataChan, nil
}
}

func (suite *NotificationTestSuite) TestSecureTokenParsing() {
conf := config.NewDefaultConfig()

Check failure on line 508 in pkg/handlers/notification_test.go

View workflow job for this annotation

GitHub Actions / tests_coveralls

declared and not used: conf

testCases := []struct {
name string
sdkKeyHeader string
expectedSDKKey string
description string
}{
{
name: "StandardSDKKey",
sdkKeyHeader: "normal_sdk_key_123",
expectedSDKKey: "normal_sdk_key_123",
description: "Standard SDK key without secure token should remain unchanged",
},
{
name: "SecureTokenFormat",
sdkKeyHeader: "sdk_key_123:api_key_456",
expectedSDKKey: "sdk_key_123",
description: "SDK key with secure token should extract only the SDK key portion",
},
{
name: "MultipleColons",
sdkKeyHeader: "sdk_key:api_key:extra_part",
expectedSDKKey: "sdk_key",
description: "Multiple colons should split at first colon only",
},
{
name: "EmptySDKKey",
sdkKeyHeader: ":api_key_456",
expectedSDKKey: "",
description: "Empty SDK key portion should result in empty string",
},
{
name: "EmptyAPIKey",
sdkKeyHeader: "sdk_key_123:",
expectedSDKKey: "sdk_key_123",
description: "Empty API key portion should extract SDK key",
},
{
name: "ColonOnly",
sdkKeyHeader: ":",
expectedSDKKey: "",
description: "Colon only should result in empty SDK key",
},
{
name: "EmptyHeader",
sdkKeyHeader: "",
expectedSDKKey: "",
description: "Empty header should remain empty",
},
}

for _, tc := range testCases {
suite.Run(tc.name, func() {
// Create a mock notification receiver that captures the SDK key
var capturedSDKKey string
mockReceiver := func(ctx context.Context) (<-chan syncer.Event, error) {
capturedSDKKey = ctx.Value(SDKKey).(string)
dataChan := make(chan syncer.Event)
close(dataChan) // Close immediately as we don't need events
return dataChan, nil
}

// Setup handler
suite.mux.Get("/test-notifications", NotificationEventStreamHandler(mockReceiver))

// Create request with SDK key header
req := httptest.NewRequest("GET", "/test-notifications", nil)
if tc.sdkKeyHeader != "" {
req.Header.Set(middleware.OptlySDKHeader, tc.sdkKeyHeader)
}
rec := httptest.NewRecorder()

// Execute request
suite.mux.ServeHTTP(rec, req)

// Verify SDK key was parsed correctly
suite.Equal(tc.expectedSDKKey, capturedSDKKey, tc.description)
})
}
}

func (suite *NotificationTestSuite) TestSecureTokenParsingIntegration() {
// Test that secure token parsing works end-to-end with actual notification flow
conf := config.NewDefaultConfig()

Check failure on line 592 in pkg/handlers/notification_test.go

View workflow job for this annotation

GitHub Actions / tests_coveralls

declared and not used: conf

// Test with secure token format
req := httptest.NewRequest("GET", "/notifications/event-stream", nil)
req.Header.Set(middleware.OptlySDKHeader, "test_sdk_key:test_api_key")
rec := httptest.NewRecorder()

// Create a mock receiver that verifies the SDK key context
mockReceiver := func(ctx context.Context) (<-chan syncer.Event, error) {
sdkKey := ctx.Value(SDKKey).(string)
suite.Equal("test_sdk_key", sdkKey, "SDK key should be extracted from secure token format")

dataChan := make(chan syncer.Event, 1)
// Send a test event
dataChan <- syncer.Event{
Type: notification.Decision,
Message: map[string]string{"test": "event"},
}
close(dataChan)
return dataChan, nil
}

suite.mux.Get("/test-secure-notifications", NotificationEventStreamHandler(mockReceiver))

// Create cancelable context for SSE
ctx, cancel := context.WithTimeout(req.Context(), 1*time.Second)
defer cancel()

suite.mux.ServeHTTP(rec, req.WithContext(ctx))

// Verify response
suite.Equal(http.StatusOK, rec.Code)
response := rec.Body.String()
suite.Contains(response, `data: {"test":"event"}`, "Should receive the test event")
}
Loading