Skip to content

Commit 5dddd3a

Browse files
Copilotphrocker
andcommitted
Implement JiraVerbService with @verb methods and comprehensive tests
Co-authored-by: phrocker <[email protected]>
1 parent 6379a2f commit 5dddd3a

File tree

3 files changed

+446
-0
lines changed

3 files changed

+446
-0
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package io.sentrius.sso.controllers.api;
2+
3+
import io.sentrius.sso.core.dto.capabilities.EndpointDescriptor;
4+
import io.sentrius.sso.core.services.capabilities.EndpointScanningService;
5+
import org.junit.jupiter.api.Test;
6+
import org.springframework.beans.factory.annotation.Autowired;
7+
import org.springframework.boot.test.context.SpringBootTest;
8+
import org.springframework.test.context.TestPropertySource;
9+
10+
import java.util.List;
11+
12+
import static org.junit.jupiter.api.Assertions.*;
13+
14+
/**
15+
* Integration test to verify that JIRA verbs are properly discovered by the capabilities endpoint.
16+
*/
17+
@SpringBootTest
18+
@TestPropertySource(properties = {
19+
"spring.datasource.url=jdbc:h2:mem:testdb",
20+
"spring.jpa.hibernate.ddl-auto=create-drop"
21+
})
22+
public class CapabilitiesApiControllerJiraIntegrationTest {
23+
24+
@Autowired
25+
private EndpointScanningService endpointScanningService;
26+
27+
@Test
28+
public void testJiraVerbsAreDiscovered() {
29+
// Force refresh to ensure we get latest endpoints
30+
endpointScanningService.refreshEndpoints();
31+
32+
List<EndpointDescriptor> allEndpoints = endpointScanningService.getAllEndpoints();
33+
34+
// Verify we found some endpoints
35+
assertTrue(allEndpoints.size() > 0, "Should have found some endpoints");
36+
37+
// Look for JIRA-related verbs
38+
List<EndpointDescriptor> jiraVerbs = allEndpoints.stream()
39+
.filter(endpoint -> "VERB".equals(endpoint.getType()))
40+
.filter(endpoint -> endpoint.getClassName().contains("JiraVerbService"))
41+
.toList();
42+
43+
// Verify we found JIRA verbs
44+
assertTrue(jiraVerbs.size() > 0, "Should have found JiraVerbService endpoints");
45+
46+
// Check for specific JIRA verbs
47+
boolean foundSearchForTickets = jiraVerbs.stream()
48+
.anyMatch(verb -> "searchForTickets".equals(verb.getName()));
49+
assertTrue(foundSearchForTickets, "Should have found searchForTickets verb");
50+
51+
boolean foundAssignTicket = jiraVerbs.stream()
52+
.anyMatch(verb -> "assignTicket".equals(verb.getName()));
53+
assertTrue(foundAssignTicket, "Should have found assignTicket verb");
54+
55+
boolean foundIsJiraAvailable = jiraVerbs.stream()
56+
.anyMatch(verb -> "isJiraAvailable".equals(verb.getName()));
57+
assertTrue(foundIsJiraAvailable, "Should have found isJiraAvailable verb");
58+
59+
boolean foundUpdateTicket = jiraVerbs.stream()
60+
.anyMatch(verb -> "updateTicket".equals(verb.getName()));
61+
assertTrue(foundUpdateTicket, "Should have found updateTicket verb");
62+
63+
// Verify the verbs are marked as AI callable
64+
for (EndpointDescriptor jiraVerb : jiraVerbs) {
65+
Object isAiCallable = jiraVerb.getMetadata().get("isAiCallable");
66+
assertTrue(isAiCallable instanceof Boolean && (Boolean) isAiCallable,
67+
"JIRA verb " + jiraVerb.getName() + " should be AI callable");
68+
}
69+
70+
// Log found verbs for debugging
71+
System.out.println("Found JIRA verbs:");
72+
jiraVerbs.forEach(verb -> {
73+
System.out.println(" - " + verb.getName() + ": " + verb.getDescription());
74+
});
75+
}
76+
77+
@Test
78+
public void testVerbEndpointFilterReturnsJiraVerbs() {
79+
// Force refresh to ensure we get latest endpoints
80+
endpointScanningService.refreshEndpoints();
81+
82+
List<EndpointDescriptor> verbEndpoints = endpointScanningService.getAllEndpoints()
83+
.stream()
84+
.filter(endpoint -> "VERB".equals(endpoint.getType()))
85+
.toList();
86+
87+
// Verify we have some verb endpoints
88+
assertTrue(verbEndpoints.size() > 0, "Should have found verb endpoints");
89+
90+
// Look for JIRA verbs specifically
91+
List<EndpointDescriptor> jiraVerbs = verbEndpoints.stream()
92+
.filter(endpoint -> endpoint.getClassName().contains("JiraVerbService"))
93+
.toList();
94+
95+
assertTrue(jiraVerbs.size() >= 4, "Should have found at least 4 JIRA verbs (searchForTickets, assignTicket, updateTicket, isJiraAvailable)");
96+
}
97+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package io.sentrius.sso.core.integrations.ticketing;
2+
3+
import java.util.List;
4+
import java.util.Optional;
5+
6+
import io.sentrius.sso.core.dto.TicketDTO;
7+
import io.sentrius.sso.core.model.security.IntegrationSecurityToken;
8+
import io.sentrius.sso.core.model.users.User;
9+
import io.sentrius.sso.core.model.verbs.Verb;
10+
import io.sentrius.sso.core.services.security.IntegrationSecurityTokenService;
11+
import lombok.extern.slf4j.Slf4j;
12+
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
13+
import org.springframework.boot.web.client.RestTemplateBuilder;
14+
import org.springframework.stereotype.Service;
15+
16+
/**
17+
* Service that exposes JIRA operations as AI-callable verbs.
18+
* This allows AI agents to discover and call JIRA functionality through the capabilities API.
19+
*/
20+
@Slf4j
21+
@Service
22+
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
23+
public class JiraVerbService {
24+
25+
private final TicketService ticketService;
26+
private final IntegrationSecurityTokenService integrationService;
27+
28+
public JiraVerbService(TicketService ticketService, IntegrationSecurityTokenService integrationService) {
29+
this.ticketService = ticketService;
30+
this.integrationService = integrationService;
31+
}
32+
33+
/**
34+
* Searches for JIRA tickets based on a query string.
35+
* This method is exposed as a Verb so AI agents can discover and call it.
36+
*
37+
* @param query The search query (can be JQL or simple text)
38+
* @return List of tickets matching the query
39+
*/
40+
@Verb(
41+
name = "searchForTickets",
42+
description = "Search for JIRA tickets using a query string. Can use JQL or simple text search.",
43+
returnType = List.class,
44+
isAiCallable = true,
45+
paramDescriptions = {"Search query string (JQL or simple text)"}
46+
)
47+
public List<TicketDTO> searchForTickets(String query) {
48+
log.info("Searching for tickets with query: {}", query);
49+
50+
// Check if JIRA integration is available
51+
if (!isJiraIntegrationAvailable()) {
52+
log.warn("JIRA integration not available, returning empty results");
53+
return List.of();
54+
}
55+
56+
return ticketService.searchForIncidents(query);
57+
}
58+
59+
/**
60+
* Assigns a JIRA ticket to a user.
61+
* This method is exposed as a Verb so AI agents can discover and call it.
62+
*
63+
* @param ticketKey The JIRA ticket key (e.g., "PROJ-123")
64+
* @param user The user to assign the ticket to
65+
* @return true if assignment was successful, false otherwise
66+
*/
67+
@Verb(
68+
name = "assignTicket",
69+
description = "Assign a JIRA ticket to a user",
70+
returnType = Boolean.class,
71+
isAiCallable = true,
72+
paramDescriptions = {"JIRA ticket key (e.g., PROJ-123)", "User to assign the ticket to"}
73+
)
74+
public Boolean assignTicket(String ticketKey, User user) {
75+
log.info("Assigning ticket {} to user {}", ticketKey, user.getEmailAddress());
76+
77+
// Check if JIRA integration is available
78+
if (!isJiraIntegrationAvailable()) {
79+
log.warn("JIRA integration not available, cannot assign ticket");
80+
return false;
81+
}
82+
83+
return ticketService.assignJira(ticketKey, user);
84+
}
85+
86+
/**
87+
* Updates a JIRA ticket with a comment.
88+
* This method is exposed as a Verb so AI agents can discover and call it.
89+
*
90+
* @param ticketKey The JIRA ticket key (e.g., "PROJ-123")
91+
* @param user The user adding the comment
92+
* @param message The comment message
93+
* @return true if update was successful, false otherwise
94+
*/
95+
@Verb(
96+
name = "updateTicket",
97+
description = "Add a comment to a JIRA ticket",
98+
returnType = Boolean.class,
99+
isAiCallable = true,
100+
paramDescriptions = {"JIRA ticket key (e.g., PROJ-123)", "User adding the comment", "Comment message"}
101+
)
102+
public Boolean updateTicket(String ticketKey, User user, String message) {
103+
log.info("Updating ticket {} with comment from user {}", ticketKey, user.getEmailAddress());
104+
105+
// Check if JIRA integration is available
106+
if (!isJiraIntegrationAvailable()) {
107+
log.warn("JIRA integration not available, cannot update ticket");
108+
return false;
109+
}
110+
111+
return ticketService.updateJira(ticketKey, user, message);
112+
}
113+
114+
/**
115+
* Checks if at least one JIRA integration is configured and available.
116+
* This method is exposed as a Verb so AI agents can check JIRA availability.
117+
*
118+
* @return true if JIRA integration is available, false otherwise
119+
*/
120+
@Verb(
121+
name = "isJiraAvailable",
122+
description = "Check if JIRA integration is configured and available",
123+
returnType = Boolean.class,
124+
isAiCallable = true,
125+
paramDescriptions = {}
126+
)
127+
public Boolean isJiraAvailable() {
128+
boolean available = isJiraIntegrationAvailable();
129+
log.info("JIRA integration availability check: {}", available);
130+
return available;
131+
}
132+
133+
/**
134+
* Helper method to check if JIRA integration is available.
135+
*
136+
* @return true if at least one JIRA integration is configured
137+
*/
138+
private boolean isJiraIntegrationAvailable() {
139+
try {
140+
List<IntegrationSecurityToken> jiraIntegrations = integrationService.findByConnectionType("jira");
141+
return !jiraIntegrations.isEmpty();
142+
} catch (Exception e) {
143+
log.error("Error checking JIRA integration availability", e);
144+
return false;
145+
}
146+
}
147+
}

0 commit comments

Comments
 (0)