Skip to content

Commit fbbb3b3

Browse files
coopernetesclaude
andcommitted
feat: add tests, CI workflow, and upgrade to Java 21
- Add 103 unit and integration tests across jgit-proxy-core and jgit-proxy-server covering pack parsing, filter chain behaviour (proxy mode), and pre-receive hook chain behaviour (store-and-forward mode); use real captured push debug files as test fixtures - Add GitHub Actions CI workflow that builds and runs tests on every push and PR, uploading HTML test reports as an artifact - Upgrade Java toolchain from 17 to 21 (Temurin) across both build.gradle files, mise.toml, and the CI workflow - Extend .gitignore to exclude runtime debug output files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a4142e1 commit fbbb3b3

19 files changed

Lines changed: 2507 additions & 7 deletions

.github/workflows/ci.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: ["**"]
6+
pull_request:
7+
8+
jobs:
9+
build-and-test:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- uses: actions/setup-java@v4
16+
with:
17+
distribution: temurin
18+
java-version: 21
19+
cache: gradle
20+
21+
- name: Build and test
22+
run: ./gradlew build test
23+
24+
- name: Publish test results
25+
if: always()
26+
uses: actions/upload-artifact@v4
27+
with:
28+
name: test-reports
29+
path: "**/build/reports/tests/test/"
30+
retention-days: 14

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,7 @@ out/
3939
### Logs ###
4040
logs/
4141
*.log
42+
43+
### Runtime debug output ###
44+
body-*.txt
45+
packetline-*.txt

jgit-proxy-core/build.gradle

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ group = 'org.finos.gitproxy'
66
version = '0.0.1-SNAPSHOT'
77

88
java {
9-
sourceCompatibility = JavaVersion.VERSION_17
10-
targetCompatibility = JavaVersion.VERSION_17
9+
sourceCompatibility = JavaVersion.VERSION_21
10+
targetCompatibility = JavaVersion.VERSION_21
1111
toolchain {
12-
languageVersion = JavaLanguageVersion.of(17)
12+
languageVersion = JavaLanguageVersion.of(21)
1313
vendor = JvmVendorSpec.ADOPTIUM
1414
}
1515
}
@@ -96,6 +96,11 @@ dependencies {
9696
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.14.3'
9797
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.14.3'
9898
testRuntimeOnly "com.h2database:h2:${h2Version}"
99+
testImplementation 'org.mockito:mockito-core:5.17.0'
100+
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.14.3'
101+
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.14.3'
102+
testCompileOnly 'org.projectlombok:lombok:1.18.44'
103+
testAnnotationProcessor 'org.projectlombok:lombok:1.18.44'
99104
}
100105

101106
tasks.named('test') {
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
package org.finos.gitproxy.git;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import java.io.File;
6+
import java.nio.file.Files;
7+
import java.nio.file.Path;
8+
import java.util.List;
9+
import java.util.UUID;
10+
import java.util.regex.Pattern;
11+
import org.eclipse.jgit.api.Git;
12+
import org.eclipse.jgit.lib.ObjectId;
13+
import org.eclipse.jgit.lib.PersonIdent;
14+
import org.eclipse.jgit.lib.Repository;
15+
import org.eclipse.jgit.revwalk.RevCommit;
16+
import org.eclipse.jgit.transport.ReceiveCommand;
17+
import org.eclipse.jgit.transport.ReceivePack;
18+
import org.finos.gitproxy.config.CommitConfig;
19+
import org.finos.gitproxy.db.model.StepStatus;
20+
import org.junit.jupiter.api.BeforeEach;
21+
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.api.io.TempDir;
23+
24+
class AuthorEmailValidationHookTest {
25+
26+
@TempDir
27+
Path tempDir;
28+
29+
Repository repo;
30+
ObjectId commitId1;
31+
ObjectId commitId2;
32+
33+
@BeforeEach
34+
void setUp() throws Exception {
35+
Git git = Git.init().setDirectory(tempDir.toFile()).call();
36+
repo = git.getRepository();
37+
// Disable GPG signing in the test repo to avoid UnsupportedSigningFormatException
38+
// when the user's global git config has commit.gpgsign=true
39+
repo.getConfig().setBoolean("commit", null, "gpgsign", false);
40+
repo.getConfig().save();
41+
42+
commitId1 = createCommit(git, "First commit", "Dev User", "dev@example.com");
43+
commitId2 = createCommit(git, "Second commit", "Dev User", "dev@example.com");
44+
}
45+
46+
private ObjectId createCommit(Git git, String message, String name, String email) throws Exception {
47+
File f = new File(tempDir.toFile(), UUID.randomUUID() + ".txt");
48+
f.createNewFile();
49+
Files.writeString(f.toPath(), message);
50+
git.add().addFilepattern(".").call();
51+
RevCommit c = git.commit()
52+
.setAuthor(new PersonIdent(name, email))
53+
.setCommitter(new PersonIdent(name, email))
54+
.setMessage(message)
55+
.call();
56+
return c.getId();
57+
}
58+
59+
private CommitConfig allowExampleCom() {
60+
return CommitConfig.builder()
61+
.author(CommitConfig.AuthorConfig.builder()
62+
.email(CommitConfig.EmailConfig.builder()
63+
.domain(CommitConfig.DomainConfig.builder()
64+
.allow(Pattern.compile("example\\.com$"))
65+
.build())
66+
.local(CommitConfig.LocalConfig.builder()
67+
.block(Pattern.compile("^(noreply|bot)$"))
68+
.build())
69+
.build())
70+
.build())
71+
.build();
72+
}
73+
74+
private ReceivePack makeReceivePack() {
75+
return new ReceivePack(repo);
76+
}
77+
78+
private ReceiveCommand newBranchCommand(ObjectId newCommit) {
79+
return new ReceiveCommand(ObjectId.zeroId(), newCommit, "refs/heads/test");
80+
}
81+
82+
private ReceiveCommand updateCommand(ObjectId oldCommit, ObjectId newCommit) {
83+
return new ReceiveCommand(oldCommit, newCommit, "refs/heads/main");
84+
}
85+
86+
// ---- tests ----
87+
88+
@Test
89+
void validEmail_noIssues() throws Exception {
90+
ValidationContext ctx = new ValidationContext();
91+
PushContext pushCtx = new PushContext();
92+
AuthorEmailValidationHook hook = new AuthorEmailValidationHook(allowExampleCom(), ctx, pushCtx);
93+
ReceivePack rp = makeReceivePack();
94+
ReceiveCommand cmd = newBranchCommand(commitId1);
95+
96+
hook.onPreReceive(rp, List.of(cmd));
97+
98+
assertFalse(ctx.hasIssues(), "Valid email must not produce issues");
99+
}
100+
101+
@Test
102+
void invalidDomain_addsIssue() throws Exception {
103+
// Create a commit with a bad email domain
104+
Git git = Git.open(tempDir.toFile());
105+
ObjectId badCommit = createCommit(git, "Bad domain commit", "Outsider", "dev@gmail.com");
106+
107+
ValidationContext ctx = new ValidationContext();
108+
PushContext pushCtx = new PushContext();
109+
AuthorEmailValidationHook hook = new AuthorEmailValidationHook(allowExampleCom(), ctx, pushCtx);
110+
ReceivePack rp = makeReceivePack();
111+
ReceiveCommand cmd = newBranchCommand(badCommit);
112+
113+
hook.onPreReceive(rp, List.of(cmd));
114+
115+
assertTrue(ctx.hasIssues(), "Invalid domain must produce an issue");
116+
}
117+
118+
@Test
119+
void blockedLocalPart_addsIssue() throws Exception {
120+
Git git = Git.open(tempDir.toFile());
121+
ObjectId noReplyCommit = createCommit(git, "noreply commit", "Bot", "noreply@example.com");
122+
123+
ValidationContext ctx = new ValidationContext();
124+
PushContext pushCtx = new PushContext();
125+
AuthorEmailValidationHook hook = new AuthorEmailValidationHook(allowExampleCom(), ctx, pushCtx);
126+
ReceivePack rp = makeReceivePack();
127+
ReceiveCommand cmd = newBranchCommand(noReplyCommit);
128+
129+
hook.onPreReceive(rp, List.of(cmd));
130+
131+
assertTrue(ctx.hasIssues(), "Blocked local part must produce an issue");
132+
}
133+
134+
@Test
135+
void validEmail_recordsPassStep() throws Exception {
136+
ValidationContext ctx = new ValidationContext();
137+
PushContext pushCtx = new PushContext();
138+
AuthorEmailValidationHook hook = new AuthorEmailValidationHook(allowExampleCom(), ctx, pushCtx);
139+
ReceivePack rp = makeReceivePack();
140+
141+
hook.onPreReceive(rp, List.of(newBranchCommand(commitId1)));
142+
143+
assertFalse(pushCtx.getSteps().isEmpty());
144+
assertEquals(StepStatus.PASS, pushCtx.getSteps().get(0).getStatus(), "Valid email must record PASS step");
145+
}
146+
147+
@Test
148+
void invalidEmail_recordsFailStep() throws Exception {
149+
Git git = Git.open(tempDir.toFile());
150+
ObjectId badCommit = createCommit(git, "Bad commit", "Outsider", "dev@badomain.io");
151+
152+
ValidationContext ctx = new ValidationContext();
153+
PushContext pushCtx = new PushContext();
154+
AuthorEmailValidationHook hook = new AuthorEmailValidationHook(allowExampleCom(), ctx, pushCtx);
155+
ReceivePack rp = makeReceivePack();
156+
157+
hook.onPreReceive(rp, List.of(newBranchCommand(badCommit)));
158+
159+
assertFalse(pushCtx.getSteps().isEmpty());
160+
assertEquals(StepStatus.FAIL, pushCtx.getSteps().get(0).getStatus(), "Invalid email must record FAIL step");
161+
}
162+
163+
@Test
164+
void deleteCommand_skipped() throws Exception {
165+
ValidationContext ctx = new ValidationContext();
166+
PushContext pushCtx = new PushContext();
167+
AuthorEmailValidationHook hook = new AuthorEmailValidationHook(allowExampleCom(), ctx, pushCtx);
168+
ReceivePack rp = makeReceivePack();
169+
170+
// DELETE type: oldId is a real commit, newId is zero
171+
ReceiveCommand deleteCmd =
172+
new ReceiveCommand(commitId1, ObjectId.zeroId(), "refs/heads/test", ReceiveCommand.Type.DELETE);
173+
174+
hook.onPreReceive(rp, List.of(deleteCmd));
175+
176+
// Hook should skip DELETE commands — no issues from a deletion
177+
assertFalse(ctx.hasIssues(), "DELETE command must not trigger email validation");
178+
}
179+
180+
@Test
181+
void updateCommand_validEmailRange_noIssues() throws Exception {
182+
ValidationContext ctx = new ValidationContext();
183+
PushContext pushCtx = new PushContext();
184+
AuthorEmailValidationHook hook = new AuthorEmailValidationHook(allowExampleCom(), ctx, pushCtx);
185+
ReceivePack rp = makeReceivePack();
186+
// Both commits are in the repo with "dev@example.com"
187+
ReceiveCommand cmd = updateCommand(commitId1, commitId2);
188+
189+
hook.onPreReceive(rp, List.of(cmd));
190+
191+
assertFalse(ctx.hasIssues());
192+
}
193+
194+
@Test
195+
void noConfig_anyEmailAllowed() throws Exception {
196+
Git git = Git.open(tempDir.toFile());
197+
// Create commit with an unusual email — should pass with default config
198+
ObjectId anyCommit = createCommit(git, "Free email commit", "User", "dev@unknown.io");
199+
200+
ValidationContext ctx = new ValidationContext();
201+
PushContext pushCtx = new PushContext();
202+
AuthorEmailValidationHook hook = new AuthorEmailValidationHook(CommitConfig.defaultConfig(), ctx, pushCtx);
203+
ReceivePack rp = makeReceivePack();
204+
205+
hook.onPreReceive(rp, List.of(newBranchCommand(anyCommit)));
206+
207+
assertFalse(ctx.hasIssues(), "Default config must not reject any valid email");
208+
}
209+
}

0 commit comments

Comments
 (0)