Skip to content

Commit f218098

Browse files
committed
Add unit and integration tests for Python ecosystem packages
- Created unit tests for `rag-pipeline` and `inference-orchestrator` covering various utility functions and classes. - Implemented integration tests for both packages, ensuring full coverage of API endpoints and service interactions. - Added scripts for running tests and checking coverage with customizable thresholds and HTML report generation. - Introduced `.gitignore` files to exclude unnecessary files from version control. - Updated README files to provide clear instructions on running tests and understanding the test structure.
1 parent b2d969c commit f218098

137 files changed

Lines changed: 24382 additions & 608 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package org.rostilos.codecrow.pipelineagent;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import io.restassured.RestAssured;
5+
import io.restassured.specification.RequestSpecification;
6+
import org.junit.jupiter.api.BeforeAll;
7+
import org.junit.jupiter.api.BeforeEach;
8+
import org.rostilos.codecrow.core.model.project.Project;
9+
import org.rostilos.codecrow.core.model.workspace.Workspace;
10+
import org.rostilos.codecrow.core.persistence.repository.project.ProjectRepository;
11+
import org.rostilos.codecrow.security.jwt.utils.JwtUtils;
12+
import org.rostilos.codecrow.testsupport.base.IntegrationTest;
13+
import org.rostilos.codecrow.testsupport.cleanup.DatabaseCleaner;
14+
import org.springframework.beans.factory.annotation.Autowired;
15+
import org.springframework.boot.test.context.SpringBootTest;
16+
import org.springframework.boot.test.web.server.LocalServerPort;
17+
18+
import jakarta.persistence.EntityManager;
19+
import jakarta.persistence.PersistenceContext;
20+
import jakarta.transaction.Transactional;
21+
22+
import static io.restassured.RestAssured.given;
23+
24+
/**
25+
* Base class for pipeline-agent integration tests.
26+
* <p>
27+
* Sets up Testcontainers PostgreSQL, REST Assured,
28+
* and provides helpers for project-level JWT authentication.
29+
* </p>
30+
* <p>
31+
* Authentication: Pipeline-agent uses project-level JWTs where
32+
* the JWT subject is the project ID (numeric string). The
33+
* {@link org.rostilos.codecrow.security.pipelineagent.jwt.ProjectInternalJwtFilter}
34+
* validates these tokens and loads the Project from the database.
35+
* </p>
36+
*/
37+
@IntegrationTest
38+
@SpringBootTest(
39+
classes = ProcessingApplication.class,
40+
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
41+
)
42+
abstract class BasePipelineAgentIT {
43+
44+
@LocalServerPort
45+
protected int port;
46+
47+
@Autowired
48+
protected ObjectMapper objectMapper;
49+
50+
@Autowired
51+
protected ProjectRepository projectRepository;
52+
53+
@Autowired
54+
protected JwtUtils jwtUtils;
55+
56+
@Autowired
57+
protected DatabaseCleaner databaseCleaner;
58+
59+
@PersistenceContext
60+
protected EntityManager entityManager;
61+
62+
@BeforeAll
63+
void setupRestAssured() {
64+
RestAssured.port = port;
65+
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
66+
}
67+
68+
@BeforeEach
69+
void cleanDatabase() {
70+
databaseCleaner.cleanAll();
71+
}
72+
73+
// ─── Authentication helpers ──────────────────────────
74+
75+
/**
76+
* Create a request with a valid project JWT in the Authorization header.
77+
*
78+
* @param projectId the project ID to encode in the JWT subject
79+
*/
80+
protected RequestSpecification projectAuthRequest(Long projectId) {
81+
String token = jwtUtils.generateJwtTokenForUser(projectId, String.valueOf(projectId));
82+
return given()
83+
.header("Authorization", "Bearer " + token)
84+
.contentType("application/json");
85+
}
86+
87+
/**
88+
* Create an unauthenticated request (no Authorization header).
89+
*/
90+
protected RequestSpecification unauthenticatedRequest() {
91+
return given()
92+
.contentType("application/json");
93+
}
94+
95+
// ─── Test data helpers ───────────────────────────────
96+
97+
/**
98+
* Create a workspace and project in the database for testing.
99+
* Returns the project ID.
100+
*/
101+
@Transactional
102+
protected Long createTestProject(String namespace, String projectName) {
103+
// Create workspace first
104+
Workspace workspace = new Workspace("test-ws", "Test Workspace", "Integration test workspace");
105+
entityManager.persist(workspace);
106+
entityManager.flush();
107+
108+
// Create project
109+
Project project = new Project();
110+
project.setWorkspace(workspace);
111+
project.setNamespace(namespace);
112+
project.setName(projectName);
113+
entityManager.persist(project);
114+
entityManager.flush();
115+
116+
return project.getId();
117+
}
118+
119+
/**
120+
* Create a workspace and project with a specific workspace slug.
121+
* Returns the project ID.
122+
*/
123+
@Transactional
124+
protected Long createTestProject(String wsSlug, String namespace, String projectName) {
125+
Workspace workspace = new Workspace(wsSlug, "Workspace " + wsSlug, "Test workspace");
126+
entityManager.persist(workspace);
127+
entityManager.flush();
128+
129+
Project project = new Project();
130+
project.setWorkspace(workspace);
131+
project.setNamespace(namespace);
132+
project.setName(projectName);
133+
entityManager.persist(project);
134+
entityManager.flush();
135+
136+
return project.getId();
137+
}
138+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package org.rostilos.codecrow.pipelineagent;
2+
3+
import org.junit.jupiter.api.DisplayName;
4+
import org.junit.jupiter.api.Test;
5+
6+
import static io.restassured.RestAssured.given;
7+
import static org.hamcrest.Matchers.*;
8+
9+
/**
10+
* Integration tests for pipeline-agent HealthCheckController.
11+
* Health endpoint is public — no authentication required.
12+
*/
13+
class HealthCheckControllerIT extends BasePipelineAgentIT {
14+
15+
@Test
16+
@DisplayName("Health endpoint returns OK")
17+
void healthEndpoint_returnsOk() {
18+
given()
19+
.when()
20+
.get("/actuator/health")
21+
.then()
22+
.statusCode(200)
23+
.body(equalTo("OK"));
24+
}
25+
26+
@Test
27+
@DisplayName("Health endpoint is accessible without auth")
28+
void healthEndpoint_noAuth_accessible() {
29+
unauthenticatedRequest()
30+
.when()
31+
.get("/actuator/health")
32+
.then()
33+
.statusCode(200);
34+
}
35+
36+
@Test
37+
@DisplayName("Health endpoint accepts any request method context")
38+
void healthEndpoint_withInvalidToken_stillAccessible() {
39+
given()
40+
.header("Authorization", "Bearer invalid-token")
41+
.when()
42+
.get("/actuator/health")
43+
.then()
44+
.statusCode(200)
45+
.body(equalTo("OK"));
46+
}
47+
}
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
package org.rostilos.codecrow.pipelineagent;
2+
3+
import org.junit.jupiter.api.DisplayName;
4+
import org.junit.jupiter.api.Nested;
5+
import org.junit.jupiter.api.Test;
6+
7+
import static io.restassured.RestAssured.given;
8+
import static org.hamcrest.Matchers.*;
9+
10+
/**
11+
* Integration tests for ProviderPipelineActionController.
12+
* Processing endpoints require project-level JWT authentication.
13+
* The JWT subject must be the project ID, and the request body's
14+
* projectId must match the JWT's subject.
15+
*/
16+
class PipelineActionControllerIT extends BasePipelineAgentIT {
17+
18+
@Nested
19+
@DisplayName("POST /api/processing/webhook/pr")
20+
class PrProcessing {
21+
22+
@Test
23+
@DisplayName("PR processing — unauthenticated — 401")
24+
void prProcessing_noAuth_returns401() {
25+
unauthenticatedRequest()
26+
.body("""
27+
{
28+
"projectId": 1,
29+
"pullRequestId": 42,
30+
"sourceBranch": "feature/test",
31+
"targetBranch": "main",
32+
"commitHash": "abc123",
33+
"rawDiff": "diff content",
34+
"changedFiles": []
35+
}
36+
""")
37+
.when()
38+
.post("/api/processing/webhook/pr")
39+
.then()
40+
.statusCode(401);
41+
}
42+
43+
@Test
44+
@DisplayName("PR processing — invalid JWT — 401")
45+
void prProcessing_invalidToken_returns401() {
46+
given()
47+
.header("Authorization", "Bearer invalid.jwt.token")
48+
.contentType("application/json")
49+
.body("""
50+
{
51+
"projectId": 1,
52+
"pullRequestId": 42,
53+
"sourceBranch": "feature/test",
54+
"targetBranch": "main"
55+
}
56+
""")
57+
.when()
58+
.post("/api/processing/webhook/pr")
59+
.then()
60+
.statusCode(401);
61+
}
62+
63+
@Test
64+
@DisplayName("PR processing — JWT for non-existent project — 404")
65+
void prProcessing_nonExistentProject_returns404() {
66+
projectAuthRequest(99999L)
67+
.body("""
68+
{
69+
"projectId": 99999,
70+
"pullRequestId": 42,
71+
"sourceBranch": "feature/test",
72+
"targetBranch": "main"
73+
}
74+
""")
75+
.when()
76+
.post("/api/processing/webhook/pr")
77+
.then()
78+
.statusCode(404);
79+
}
80+
81+
@Test
82+
@DisplayName("PR processing — project ID mismatch — 401")
83+
void prProcessing_projectIdMismatch_returns401() {
84+
Long projectId = createTestProject("pr-mismatch-ns", "Mismatch Project");
85+
86+
// JWT subject is projectId but body says different projectId
87+
projectAuthRequest(projectId)
88+
.body("""
89+
{
90+
"projectId": 99999,
91+
"pullRequestId": 42,
92+
"sourceBranch": "feature/test",
93+
"targetBranch": "main"
94+
}
95+
""")
96+
.when()
97+
.post("/api/processing/webhook/pr")
98+
.then()
99+
.statusCode(401);
100+
}
101+
102+
@Test
103+
@DisplayName("PR processing — valid auth, valid project — accepted or processes")
104+
void prProcessing_validAuth_accepted() {
105+
Long projectId = createTestProject("pr-valid-ns", "Valid PR Project");
106+
107+
projectAuthRequest(projectId)
108+
.body("""
109+
{
110+
"projectId": %d,
111+
"pullRequestId": 42,
112+
"sourceBranch": "feature/test",
113+
"targetBranch": "main",
114+
"commitHash": "abc123def456",
115+
"rawDiff": "diff --git a/file.txt b/file.txt",
116+
"changedFiles": [{"path": "file.txt", "status": "modified"}]
117+
}
118+
""".formatted(projectId))
119+
.when()
120+
.post("/api/processing/webhook/pr")
121+
.then()
122+
// The endpoint returns streaming NDJSON; may succeed or fail
123+
// depending on external service availability, but auth should pass
124+
.statusCode(anyOf(is(200), is(400), is(500)));
125+
}
126+
}
127+
128+
@Nested
129+
@DisplayName("POST /api/processing/webhook/branch")
130+
class BranchProcessing {
131+
132+
@Test
133+
@DisplayName("Branch processing — unauthenticated — 401")
134+
void branchProcessing_noAuth_returns401() {
135+
unauthenticatedRequest()
136+
.body("""
137+
{
138+
"projectId": 1,
139+
"branchName": "main",
140+
"commitHash": "abc123"
141+
}
142+
""")
143+
.when()
144+
.post("/api/processing/webhook/branch")
145+
.then()
146+
.statusCode(401);
147+
}
148+
149+
@Test
150+
@DisplayName("Branch processing — invalid JWT — 401")
151+
void branchProcessing_invalidToken_returns401() {
152+
given()
153+
.header("Authorization", "Bearer not-a-valid-jwt")
154+
.contentType("application/json")
155+
.body("""
156+
{
157+
"projectId": 1,
158+
"branchName": "main"
159+
}
160+
""")
161+
.when()
162+
.post("/api/processing/webhook/branch")
163+
.then()
164+
.statusCode(401);
165+
}
166+
167+
@Test
168+
@DisplayName("Branch processing — JWT for non-existent project — 404")
169+
void branchProcessing_nonExistentProject_returns404() {
170+
projectAuthRequest(88888L)
171+
.body("""
172+
{
173+
"projectId": 88888,
174+
"branchName": "main"
175+
}
176+
""")
177+
.when()
178+
.post("/api/processing/webhook/branch")
179+
.then()
180+
.statusCode(404);
181+
}
182+
183+
@Test
184+
@DisplayName("Branch processing — valid auth — accepted or processes")
185+
void branchProcessing_validAuth_accepted() {
186+
Long projectId = createTestProject("branch-valid-ns", "Valid Branch Project");
187+
188+
projectAuthRequest(projectId)
189+
.body("""
190+
{
191+
"projectId": %d,
192+
"branchName": "main",
193+
"commitHash": "def456abc789"
194+
}
195+
""".formatted(projectId))
196+
.when()
197+
.post("/api/processing/webhook/branch")
198+
.then()
199+
.statusCode(anyOf(is(200), is(400), is(500)));
200+
}
201+
}
202+
}

0 commit comments

Comments
 (0)