Skip to content

Commit 03593d5

Browse files
Copilotphrocker
andauthored
Fix self-healing GitHub integration detection to check database tokens (#95)
* Initial plan * Fix GitHub integration detection for self-healing Co-authored-by: phrocker <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: phrocker <[email protected]>
1 parent c8fe562 commit 03593d5

File tree

2 files changed

+257
-5
lines changed

2 files changed

+257
-5
lines changed

dataplane/src/main/java/io/sentrius/sso/core/services/selfhealing/SelfHealingOrchestrator.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,9 @@ public class SelfHealingOrchestrator {
5656
/**
5757
* Check if GitHub integration is available
5858
* Self-healing requires GitHub integration to submit PRs
59+
* GitHub integration is considered available if integration tokens exist in the database
5960
*/
6061
private boolean isGitHubIntegrationAvailable() {
61-
if (!githubConfigured) {
62-
log.debug("GitHub integration is disabled in configuration");
63-
return false;
64-
}
65-
6662
if (integrationTokenService == null) {
6763
log.warn("IntegrationSecurityTokenService not available");
6864
return false;
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
package io.sentrius.sso.core.services.selfhealing;
2+
3+
import io.sentrius.sso.core.model.ErrorOutput;
4+
import io.sentrius.sso.core.model.security.IntegrationSecurityToken;
5+
import io.sentrius.sso.core.model.selfhealing.SelfHealingConfig.PatchingPolicy;
6+
import io.sentrius.sso.core.model.selfhealing.SelfHealingSession;
7+
import io.sentrius.sso.core.services.ErrorOutputService;
8+
import io.sentrius.sso.core.services.security.IntegrationSecurityTokenService;
9+
import org.junit.jupiter.api.BeforeEach;
10+
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.api.extension.ExtendWith;
12+
import org.mockito.InjectMocks;
13+
import org.mockito.Mock;
14+
import org.mockito.junit.jupiter.MockitoExtension;
15+
import org.springframework.test.util.ReflectionTestUtils;
16+
17+
import java.util.ArrayList;
18+
import java.util.Collections;
19+
import java.util.List;
20+
21+
import static org.mockito.ArgumentMatchers.*;
22+
import static org.mockito.Mockito.*;
23+
24+
@ExtendWith(MockitoExtension.class)
25+
class SelfHealingOrchestratorTest {
26+
27+
@Mock
28+
private ErrorOutputService errorOutputService;
29+
30+
@Mock
31+
private ErrorAnalysisService errorAnalysisService;
32+
33+
@Mock
34+
private SelfHealingConfigService configService;
35+
36+
@Mock
37+
private SelfHealingSessionService sessionService;
38+
39+
@Mock
40+
private HealingWorkflowCoordinator workflowCoordinator;
41+
42+
@Mock
43+
private IntegrationSecurityTokenService integrationTokenService;
44+
45+
@InjectMocks
46+
private SelfHealingOrchestrator orchestrator;
47+
48+
private ErrorOutput testError;
49+
private IntegrationSecurityToken githubToken;
50+
51+
@BeforeEach
52+
void setUp() {
53+
testError = ErrorOutput.builder()
54+
.id(1L)
55+
.errorType("NullPointerException")
56+
.errorLocation("api-service")
57+
.errorLogs("Error in pod api-service: NullPointerException at line 42")
58+
.build();
59+
60+
githubToken = IntegrationSecurityToken.builder()
61+
.id(1L)
62+
.name("GitHub Token")
63+
.connectionType("github")
64+
.connectionInfo("{\"token\":\"ghp_test123\"}")
65+
.build();
66+
67+
// Set default values for the orchestrator
68+
ReflectionTestUtils.setField(orchestrator, "selfHealingEnabled", true);
69+
ReflectionTestUtils.setField(orchestrator, "githubConfigured", false);
70+
ReflectionTestUtils.setField(orchestrator, "offHoursStart", 22);
71+
ReflectionTestUtils.setField(orchestrator, "offHoursEnd", 6);
72+
}
73+
74+
@Test
75+
void testScanForHealableErrors_DisabledWhenSelfHealingDisabled() {
76+
ReflectionTestUtils.setField(orchestrator, "selfHealingEnabled", false);
77+
78+
orchestrator.scanForHealableErrors();
79+
80+
verify(errorOutputService, never()).getErrorOutputs(anyInt(), anyInt());
81+
}
82+
83+
@Test
84+
void testScanForHealableErrors_WorksWithGitHubTokensPresent() {
85+
// Setup: GitHub tokens exist in database, config flag is false (default)
86+
List<IntegrationSecurityToken> tokens = Collections.singletonList(githubToken);
87+
when(integrationTokenService.findByConnectionType("github")).thenReturn(tokens);
88+
89+
List<ErrorOutput> errors = Collections.singletonList(testError);
90+
when(errorOutputService.getErrorOutputs(0, 50)).thenReturn(errors);
91+
when(errorAnalysisService.shouldTriggerHealing(testError)).thenReturn(true);
92+
when(errorAnalysisService.extractPodName(testError)).thenReturn("api-service");
93+
when(configService.getPatchingPolicyForPod("api-service")).thenReturn(PatchingPolicy.IMMEDIATE);
94+
when(errorAnalysisService.isLikelySecurityConcern(testError)).thenReturn(false);
95+
96+
SelfHealingSession session = SelfHealingSession.builder()
97+
.id(1L)
98+
.errorOutput(testError)
99+
.build();
100+
when(errorAnalysisService.initiateHealing(testError)).thenReturn(session);
101+
102+
orchestrator.scanForHealableErrors();
103+
104+
// Verify the scan happened and healing was initiated
105+
verify(errorOutputService).getErrorOutputs(0, 50);
106+
verify(errorAnalysisService).shouldTriggerHealing(testError);
107+
verify(errorAnalysisService).initiateHealing(testError);
108+
verify(workflowCoordinator).executeHealingWorkflow(session);
109+
}
110+
111+
@Test
112+
void testScanForHealableErrors_DisabledWhenNoGitHubTokens() {
113+
// Setup: No GitHub tokens in database
114+
when(integrationTokenService.findByConnectionType("github")).thenReturn(Collections.emptyList());
115+
116+
orchestrator.scanForHealableErrors();
117+
118+
// Verify scan was aborted due to no tokens
119+
verify(integrationTokenService).findByConnectionType("github");
120+
verify(errorOutputService, never()).getErrorOutputs(anyInt(), anyInt());
121+
}
122+
123+
@Test
124+
void testScanForHealableErrors_WorksWithTokensEvenWhenConfigIsFalse() {
125+
// Setup: GitHub tokens exist but config flag is false (default)
126+
// The fix ensures tokens in DB take precedence over config flag
127+
ReflectionTestUtils.setField(orchestrator, "githubConfigured", false);
128+
List<IntegrationSecurityToken> tokens = Collections.singletonList(githubToken);
129+
when(integrationTokenService.findByConnectionType("github")).thenReturn(tokens);
130+
131+
List<ErrorOutput> errors = Collections.singletonList(testError);
132+
when(errorOutputService.getErrorOutputs(0, 50)).thenReturn(errors);
133+
when(errorAnalysisService.shouldTriggerHealing(testError)).thenReturn(true);
134+
when(errorAnalysisService.extractPodName(testError)).thenReturn("api-service");
135+
when(configService.getPatchingPolicyForPod("api-service")).thenReturn(PatchingPolicy.IMMEDIATE);
136+
when(errorAnalysisService.isLikelySecurityConcern(testError)).thenReturn(false);
137+
138+
SelfHealingSession session = SelfHealingSession.builder()
139+
.id(1L)
140+
.errorOutput(testError)
141+
.build();
142+
when(errorAnalysisService.initiateHealing(testError)).thenReturn(session);
143+
144+
orchestrator.scanForHealableErrors();
145+
146+
// With the fix, tokens in database enable GitHub integration regardless of config flag
147+
verify(errorOutputService).getErrorOutputs(0, 50);
148+
verify(errorAnalysisService).initiateHealing(testError);
149+
}
150+
151+
@Test
152+
void testScanForHealableErrors_DisabledWhenIntegrationServiceNull() {
153+
// Setup: IntegrationSecurityTokenService is null
154+
ReflectionTestUtils.setField(orchestrator, "integrationTokenService", null);
155+
156+
orchestrator.scanForHealableErrors();
157+
158+
// Verify scan was aborted
159+
verify(errorOutputService, never()).getErrorOutputs(anyInt(), anyInt());
160+
}
161+
162+
@Test
163+
void testProcessErrorForHealing_ImmediatePolicy() {
164+
when(errorAnalysisService.extractPodName(testError)).thenReturn("api-service");
165+
when(configService.getPatchingPolicyForPod("api-service")).thenReturn(PatchingPolicy.IMMEDIATE);
166+
when(errorAnalysisService.isLikelySecurityConcern(testError)).thenReturn(false);
167+
168+
SelfHealingSession session = SelfHealingSession.builder()
169+
.id(1L)
170+
.errorOutput(testError)
171+
.build();
172+
when(errorAnalysisService.initiateHealing(testError)).thenReturn(session);
173+
174+
orchestrator.processErrorForHealing(testError);
175+
176+
verify(errorAnalysisService).initiateHealing(testError);
177+
verify(sessionService).updateSessionStatus(eq(1L), any());
178+
verify(workflowCoordinator).executeHealingWorkflow(session);
179+
}
180+
181+
@Test
182+
void testProcessErrorForHealing_NeverPolicy() {
183+
when(errorAnalysisService.extractPodName(testError)).thenReturn("api-service");
184+
when(configService.getPatchingPolicyForPod("api-service")).thenReturn(PatchingPolicy.NEVER);
185+
186+
orchestrator.processErrorForHealing(testError);
187+
188+
verify(errorAnalysisService, never()).initiateHealing(any());
189+
verify(workflowCoordinator, never()).executeHealingWorkflow(any());
190+
}
191+
192+
@Test
193+
void testProcessErrorForHealing_SecurityConcern() {
194+
when(errorAnalysisService.extractPodName(testError)).thenReturn("api-service");
195+
when(configService.getPatchingPolicyForPod("api-service")).thenReturn(PatchingPolicy.IMMEDIATE);
196+
when(errorAnalysisService.isLikelySecurityConcern(testError)).thenReturn(true);
197+
198+
SelfHealingSession session = SelfHealingSession.builder()
199+
.id(1L)
200+
.errorOutput(testError)
201+
.build();
202+
when(errorAnalysisService.initiateHealing(testError)).thenReturn(session);
203+
204+
orchestrator.processErrorForHealing(testError);
205+
206+
verify(errorAnalysisService).initiateHealing(testError);
207+
verify(sessionService).recordSecurityAnalysis(eq(1L), eq(true), anyString());
208+
verify(workflowCoordinator, never()).executeHealingWorkflow(any());
209+
}
210+
211+
@Test
212+
void testProcessQueuedErrors_WorksWithGitHubTokens() {
213+
// Setup: GitHub tokens exist, it's off-hours
214+
ReflectionTestUtils.setField(orchestrator, "offHoursStart", 0);
215+
ReflectionTestUtils.setField(orchestrator, "offHoursEnd", 23);
216+
217+
List<IntegrationSecurityToken> tokens = Collections.singletonList(githubToken);
218+
when(integrationTokenService.findByConnectionType("github")).thenReturn(tokens);
219+
220+
ErrorOutput queuedError = ErrorOutput.builder()
221+
.id(2L)
222+
.errorType("DatabaseException")
223+
.errorLocation("data-service")
224+
.healingStatus("QUEUED")
225+
.build();
226+
227+
when(errorOutputService.getAllErrorOutputs()).thenReturn(Collections.singletonList(queuedError));
228+
when(errorAnalysisService.extractPodName(queuedError)).thenReturn("data-service");
229+
when(configService.getPatchingPolicyForPod("data-service")).thenReturn(PatchingPolicy.OFF_HOURS);
230+
when(errorAnalysisService.isLikelySecurityConcern(queuedError)).thenReturn(false);
231+
232+
SelfHealingSession session = SelfHealingSession.builder()
233+
.id(2L)
234+
.errorOutput(queuedError)
235+
.build();
236+
when(errorAnalysisService.initiateHealing(queuedError)).thenReturn(session);
237+
238+
orchestrator.processQueuedErrors();
239+
240+
verify(errorAnalysisService).initiateHealing(queuedError);
241+
}
242+
243+
@Test
244+
void testProcessQueuedErrors_DisabledWhenNoGitHubTokens() {
245+
// Setup: It's off-hours but no GitHub tokens
246+
ReflectionTestUtils.setField(orchestrator, "offHoursStart", 0);
247+
ReflectionTestUtils.setField(orchestrator, "offHoursEnd", 23);
248+
249+
when(integrationTokenService.findByConnectionType("github")).thenReturn(Collections.emptyList());
250+
251+
orchestrator.processQueuedErrors();
252+
253+
verify(integrationTokenService).findByConnectionType("github");
254+
verify(errorOutputService, never()).getAllErrorOutputs();
255+
}
256+
}

0 commit comments

Comments
 (0)