Skip to content
19 changes: 16 additions & 3 deletions gateway/gateway-controller/pkg/api/handlers/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,19 @@ func (m *MockStorage) GetAllAPIKeys() ([]*models.APIKey, error) {
return result, nil
}

func (m *MockStorage) GetAPIKeysByApplicationUUID(applicationUUID string) ([]*models.APIKey, error) {
if m.getErr != nil {
return nil, m.getErr
}
result := make([]*models.APIKey, 0)
for _, key := range m.apiKeys {
if key.ApplicationID == applicationUUID && key.Status == models.APIKeyStatusActive {
result = append(result, cloneAPIKey(key))
}
}
return result, nil
}

func (m *MockStorage) GetAPIKeysByAPIAndName(apiId, name string) (*models.APIKey, error) {
if m.getErr != nil {
return nil, m.getErr
Expand Down Expand Up @@ -709,11 +722,11 @@ func (m *MockStorage) DeleteSubscriptionsForAPINotIn(apiID string, ids []string)
return nil
}

func (m *MockStorage) ReplaceApplicationAPIKeyMappings(application *models.StoredApplication, mappings []*models.ApplicationAPIKeyMapping) error {
func (m *MockStorage) ReplaceApplicationAPIKeyMappings(application *models.StoredApplication, mappings []*models.ApplicationAPIKeyMapping) ([]string, error) {
if m.updateErr != nil {
return m.updateErr
return nil, m.updateErr
}
return nil
return nil, nil
}

func (m *MockStorage) SaveCertificate(cert *models.StoredCertificate) error {
Expand Down
88 changes: 85 additions & 3 deletions gateway/gateway-controller/pkg/controlplane/api_deleted_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,31 @@ type mockStorageForDeletion struct {
upsertCallCount int
}

type recordingControlPlaneXDSManager struct {
storeCallCount int
revokeCallCount int
removeCallCount int
}

func (m *recordingControlPlaneXDSManager) StoreAPIKey(string, string, string, *models.APIKey, string) error {
m.storeCallCount++
return nil
}

func (m *recordingControlPlaneXDSManager) RevokeAPIKey(string, string, string, string, string) error {
m.revokeCallCount++
return nil
}

func (m *recordingControlPlaneXDSManager) RemoveAPIKeysByAPI(string, string, string, string) error {
m.removeCallCount++
return nil
}

func (m *recordingControlPlaneXDSManager) RefreshSnapshot() error {
return nil
}
Comment on lines +61 to +84
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Track RefreshSnapshot() in this recorder too.

The new test is meant to catch any inline xDS work during handleApplicationUpdatedEvent, but this double only records store/revoke/remove calls. If the client starts calling RefreshSnapshot() inline again, TestClient_handleApplicationUpdatedEvent_DoesNotRefreshXDSInline still passes.

Suggested test fix
 type recordingControlPlaneXDSManager struct {
 	storeCallCount  int
 	revokeCallCount int
 	removeCallCount int
+	refreshCallCount int
 }
@@
 func (m *recordingControlPlaneXDSManager) RefreshSnapshot() error {
+	m.refreshCallCount++
 	return nil
 }
 	if xdsManager.removeCallCount != 0 {
 		t.Fatalf("expected no inline xDS remove calls, got %d", xdsManager.removeCallCount)
 	}
+	if xdsManager.refreshCallCount != 0 {
+		t.Fatalf("expected no inline xDS refresh calls, got %d", xdsManager.refreshCallCount)
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@gateway/gateway-controller/pkg/controlplane/api_deleted_test.go` around lines
61 - 84, The recorder struct recordingControlPlaneXDSManager should track
RefreshSnapshot() calls so the test can detect inline refreshes: add a
refreshCallCount field to the struct and increment it in the RefreshSnapshot
method (similar to storeCallCount/revokeCallCount/removeCallCount), and update
any assertions in
TestClient_handleApplicationUpdatedEvent_DoesNotRefreshXDSInline to check
refreshCallCount instead of relying only on store/revoke/remove counts.


func newMockStorageForDeletion() *mockStorageForDeletion {
return &mockStorageForDeletion{
configs: make(map[string]*models.StoredConfig),
Expand Down Expand Up @@ -351,6 +376,10 @@ func (m *mockStorageForDeletion) GetAllAPIKeys() ([]*models.APIKey, error) {
return nil, nil
}

func (m *mockStorageForDeletion) GetAPIKeysByApplicationUUID(applicationUUID string) ([]*models.APIKey, error) {
return nil, nil
}

func (m *mockStorageForDeletion) GetAPIKeysByAPIAndName(apiID, name string) (*models.APIKey, error) {
return nil, storage.ErrNotFound
}
Expand All @@ -371,9 +400,9 @@ func (m *mockStorageForDeletion) CountActiveAPIKeysByUserAndAPI(userID, apiID st
return 0, nil
}

func (m *mockStorageForDeletion) ReplaceApplicationAPIKeyMappings(application *models.StoredApplication, mappings []*models.ApplicationAPIKeyMapping) error {
func (m *mockStorageForDeletion) ReplaceApplicationAPIKeyMappings(application *models.StoredApplication, mappings []*models.ApplicationAPIKeyMapping) ([]string, error) {
if m.replaceErr != nil {
return m.replaceErr
return nil, m.replaceErr
}
if application != nil {
m.replacedAppID = application.ApplicationID
Expand All @@ -382,7 +411,7 @@ func (m *mockStorageForDeletion) ReplaceApplicationAPIKeyMappings(application *m
m.replacedAppType = application.ApplicationType
}
m.replacedMappings = append([]*models.ApplicationAPIKeyMapping(nil), mappings...)
return nil
return nil, nil
}

// Certificate methods (not used in deletion tests but required by interface)
Expand Down Expand Up @@ -1031,6 +1060,59 @@ func TestClient_handleApplicationUpdatedEvent_ContinuesOnInvalidMappingEntries(t
}
}

func TestClient_handleApplicationUpdatedEvent_DoesNotRefreshXDSInline(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelError}))
db := newMockStorageForDeletion()
hub := &mockControlPlaneEventHub{}
xdsManager := &recordingControlPlaneXDSManager{}

db.apiKeysByUUID["key-uuid-found"] = &models.APIKey{
UUID: "key-uuid-found",
ArtifactUUID: "api-uuid-1",
}

client := &Client{
logger: logger,
db: db,
eventHub: hub,
apiKeyXDSManager: xdsManager,
gatewayID: "test-gateway",
}

event := map[string]interface{}{
"type": "application.updated",
"payload": map[string]interface{}{
"applicationId": "app-789",
"applicationUuid": "app-uuid-789",
"applicationName": "Inventory App",
"applicationType": "genai",
"mappings": []map[string]interface{}{
{"apiKeyUuid": "key-uuid-found"},
},
},
"timestamp": time.Now().Format(time.RFC3339),
"correlationId": "corr-app-update-no-inline-xds",
}

client.handleApplicationUpdatedEvent(event)

if db.replacedAppUUID != "app-uuid-789" {
t.Fatalf("expected mappings to be replaced for app-uuid-789, got %q", db.replacedAppUUID)
}
if len(hub.publishedEvents) != 1 {
t.Fatalf("expected one application event, got %d", len(hub.publishedEvents))
}
if xdsManager.storeCallCount != 0 {
t.Fatalf("expected no inline xDS store calls, got %d", xdsManager.storeCallCount)
}
if xdsManager.revokeCallCount != 0 {
t.Fatalf("expected no inline xDS revoke calls, got %d", xdsManager.revokeCallCount)
}
if xdsManager.removeCallCount != 0 {
t.Fatalf("expected no inline xDS remove calls, got %d", xdsManager.removeCallCount)
}
}

func TestClient_handleSubscriptionCreatedEvent_PublishesReplicaSyncEvent(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelError}))
db := newMockStorageForDeletion()
Expand Down
67 changes: 0 additions & 67 deletions gateway/gateway-controller/pkg/controlplane/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3123,26 +3123,6 @@ func (c *Client) handleApplicationUpdatedEvent(event map[string]interface{}) {
slog.String("application_type", evt.Payload.ApplicationType),
)

affectedAPIKeyUUIDs := make(map[string]struct{})
apiKeysByUUID := make(map[string]*models.APIKey)
if c.apiKeyXDSManager != nil {
apiKeys, err := c.db.GetAllAPIKeys()
if err != nil {
logger.Error("Failed to load API keys for xDS refresh after application mapping update", slog.Any("error", err))
} else {
for _, apiKey := range apiKeys {
if apiKey == nil || apiKey.UUID == "" {
continue
}

apiKeysByUUID[apiKey.UUID] = apiKey
if apiKey.ApplicationID == evt.Payload.ApplicationUuid {
affectedAPIKeyUUIDs[apiKey.UUID] = struct{}{}
}
}
}
}

resolvedMappings := make([]*models.ApplicationAPIKeyMapping, 0, len(evt.Payload.Mappings))

for _, mapping := range evt.Payload.Mappings {
Expand Down Expand Up @@ -3173,7 +3153,6 @@ func (c *Client) handleApplicationUpdatedEvent(event map[string]interface{}) {
ApplicationUUID: evt.Payload.ApplicationUuid,
APIKeyID: apiKey.UUID,
})
affectedAPIKeyUUIDs[apiKey.UUID] = struct{}{}
}

application := &models.StoredApplication{
Expand All @@ -3188,52 +3167,6 @@ func (c *Client) handleApplicationUpdatedEvent(event map[string]interface{}) {
return
}

if c.apiKeyXDSManager != nil {
cfgByArtifactUUID := make(map[string]*models.StoredConfig)
missingCfgArtifactUUIDs := make(map[string]error)

for apiKeyUUID := range affectedAPIKeyUUIDs {
apiKey := apiKeysByUUID[apiKeyUUID]
if apiKey == nil {
continue
}

cfg := cfgByArtifactUUID[apiKey.ArtifactUUID]
if cfg == nil {
if cfgErr, missing := missingCfgArtifactUUIDs[apiKey.ArtifactUUID]; missing {
logger.Debug("Skipping API key xDS refresh due to missing API config",
slog.String("api_key_uuid", apiKey.UUID),
slog.String("artifact_uuid", apiKey.ArtifactUUID),
slog.Any("error", cfgErr),
)
continue
}

cfgLoaded, cfgErr := c.db.GetConfig(apiKey.ArtifactUUID)
if cfgErr != nil {
missingCfgArtifactUUIDs[apiKey.ArtifactUUID] = cfgErr
logger.Debug("Skipping API key xDS refresh due to missing API config",
slog.String("api_key_uuid", apiKey.UUID),
slog.String("artifact_uuid", apiKey.ArtifactUUID),
slog.Any("error", cfgErr),
)
continue
}

cfg = cfgLoaded
cfgByArtifactUUID[apiKey.ArtifactUUID] = cfgLoaded
}

if err := c.apiKeyXDSManager.StoreAPIKey(apiKey.ArtifactUUID, cfg.DisplayName, cfg.Version, apiKey, evt.CorrelationID); err != nil {
logger.Error("Failed to refresh API key xDS state after application mapping update",
slog.String("api_key_uuid", apiKey.UUID),
slog.String("artifact_uuid", apiKey.ArtifactUUID),
slog.Any("error", err),
)
}
}
}

logger.Info("Successfully processed application updated event", slog.Int("mapping_count", len(resolvedMappings)))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,8 @@ func TestHandleEvent_AcceptsKnownTypesAndUnknown(t *testing.T) {
var logBuf bytes.Buffer
listener := &EventListener{
logger: slog.New(slog.NewTextHandler(&logBuf, nil)),
store: storage.NewConfigStore(),
db: setupSQLiteDBForEventListenerTests(t),
}

listener.handleEvent(eventhub.Event{
Expand All @@ -328,6 +330,7 @@ func TestHandleEvent_AcceptsKnownTypesAndUnknown(t *testing.T) {
EventType: eventhub.EventTypeApplication,
Action: "UPDATE",
EntityID: "app-1",
EventID: "corr-app-1",
})
listener.handleEvent(eventhub.Event{
EventType: eventhub.EventType("UNKNOWN"),
Expand All @@ -336,7 +339,7 @@ func TestHandleEvent_AcceptsKnownTypesAndUnknown(t *testing.T) {

logs := logBuf.String()
assert.Contains(t, logs, "Certificate event received")
assert.Contains(t, logs, "Processed application replica sync event")
assert.Contains(t, logs, "Successfully processed application replica sync event")
assert.Contains(t, logs, "Unknown LLM template event action")
assert.Contains(t, logs, "Unknown event type received")
}
Expand Down
Loading
Loading