Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,11 @@ jobs:
name: unit-test-reports-${{ github.run_id }}-${{ github.run_attempt }}
path: target/surefire-reports/
if-no-files-found: error

- name: Upload coverage report
if: ${{ !cancelled() && hashFiles('target/site/jacoco/jacoco.xml') != '' }}
uses: actions/upload-artifact@v7
with:
name: coverage-report-${{ github.run_id }}-${{ github.run_attempt }}
path: target/site/jacoco/
if-no-files-found: error
8 changes: 8 additions & 0 deletions .github/workflows/maven-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ jobs:
echo "jar=target/$final_name.jar" >> "$GITHUB_OUTPUT"
mvn -B --no-transfer-progress clean verify -DskipTests=false -Dmaven.test.skip=false

- name: Upload coverage report
if: ${{ !cancelled() && hashFiles('target/site/jacoco/jacoco.xml') != '' }}
uses: actions/upload-artifact@v7
with:
name: coverage-report-${{ github.run_id }}-${{ github.run_attempt }}
path: target/site/jacoco/
if-no-files-found: error

- name: Stage only the release JAR
env:
ARTIFACT_PATH: ${{ steps.maven.outputs.jar }}
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,12 @@ Copyright (c) 2026 TF-Minecraft contributors.
TF-Minecraft-authored material in this repository is licensed under the
[Artistic License 2.0](LICENSE). Third-party dependencies and bundled material
retain their own licenses.

## Tests and coverage

Run `mvn clean verify` with Java 21. The build runs the unit tests and enforces
100% executable runtime **line coverage** with JaCoCo, without production-class
exclusions. Instruction and branch coverage are reported separately.

The HTML report is `target/site/jacoco/index.html`; the machine-readable report is
`target/site/jacoco/jacoco.xml`. CI uploads these reports alongside test results.
64 changes: 60 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,26 @@
<version>main-SNAPSHOT</version>

<properties>
<argLine></argLine>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.proc>full</maven.compiler.proc>
<tlibs.version>2.0.0</tlibs.version>
</properties>

<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.17</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.11.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
Expand Down Expand Up @@ -45,13 +58,19 @@
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.4</version>
<version>5.14.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.14.2</version>
<version>5.23.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockbukkit.mockbukkit</groupId>
<artifactId>mockbukkit-v1.21</artifactId>
<version>4.95.0</version>
<scope>test</scope>
</dependency>
</dependencies>
Expand Down Expand Up @@ -101,11 +120,48 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
<version>3.5.4</version>
<configuration>
<argLine>-javaagent:${org.mockito:mockito-core:jar} -Xshare:off</argLine>
<argLine>@{argLine} -javaagent:${org.mockito:mockito-core:jar} -Xshare:off</argLine>
<failIfNoTests>true</failIfNoTests>
<systemPropertyVariables>
<!-- The provided Bungee bundle includes a proxy-specific logging provider. -->
<slf4j.provider>org.slf4j.simple.SimpleServiceProvider</slf4j.provider>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.15</version>
<executions>
<execution><goals><goal>prepare-agent</goal></goals></execution>
<execution>
<id>coverage-report</id>
<phase>verify</phase>
<goals><goal>report</goal></goals>
</execution>
<execution>
<id>coverage-check</id>
<phase>verify</phase>
<goals><goal>check</goal></goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>MISSEDCOUNT</value>
<maximum>0</maximum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
2 changes: 1 addition & 1 deletion src/main/java/net/tfminecraft/tfmcweb/TFMCWeb.java
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,6 @@ public static String getRealmId() {
if (realm == null || realm.isBlank()) {
return "main";
}
return realm.trim().toLowerCase();
return realm.trim().toLowerCase(java.util.Locale.ROOT);
}
}
86 changes: 16 additions & 70 deletions src/main/java/net/tfminecraft/tfmcweb/api/ProvinceSystemClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;

import net.tfminecraft.tfmcweb.Cache;

Expand Down Expand Up @@ -277,7 +281,7 @@ public static CodeResult startDiscordLink(String playerUuid, String minecraftNam

public static FeatureCodeResult issueFeatureCode(String playerUuid, String scope) {
String uuid = playerUuid == null ? "" : playerUuid.trim();
String sc = scope == null ? "" : scope.trim().toLowerCase();
String sc = scope == null ? "" : scope.trim().toLowerCase(Locale.ROOT);
if (uuid.isEmpty()) {
return FeatureCodeResult.fail("player_uuid is required");
}
Expand All @@ -289,7 +293,7 @@ public static FeatureCodeResult issueFeatureCode(String playerUuid, String scope
}
String realm = Cache.realmId == null || Cache.realmId.isBlank()
? "main"
: Cache.realmId.trim().toLowerCase();
: Cache.realmId.trim().toLowerCase(Locale.ROOT);
String body = "{"
+ "\"player_uuid\":\"" + escapeJson(uuid) + "\","
+ "\"scope\":\"" + escapeJson(sc) + "\","
Expand Down Expand Up @@ -410,7 +414,7 @@ public static MirrorResult postBanEvent(
String duration,
String staffName
) {
String etype = event == null ? "" : event.trim().toLowerCase();
String etype = event == null ? "" : event.trim().toLowerCase(Locale.ROOT);
if (!"ban".equals(etype) && !"unban".equals(etype)) {
return MirrorResult.fail("event must be ban or unban");
}
Expand Down Expand Up @@ -940,79 +944,21 @@ static String jsonString(String json, String key) {
if (json == null || key == null) {
return null;
}
String needle = "\"" + key + "\"";
int keyIdx = json.indexOf(needle);
if (keyIdx < 0) {
return null;
}
int colon = json.indexOf(':', keyIdx + needle.length());
if (colon < 0) {
return null;
}
int i = colon + 1;
while (i < json.length() && Character.isWhitespace(json.charAt(i))) {
i++;
}
if (i >= json.length()) {
return null;
}
char c = json.charAt(i);
if (c == '"') {
StringBuilder out = new StringBuilder();
i++;
while (i < json.length()) {
char ch = json.charAt(i++);
if (ch == '\\' && i < json.length()) {
out.append(json.charAt(i++));
continue;
}
if (ch == '"') {
break;
}
out.append(ch);
try {
JsonElement root = JsonParser.parseString(json);
if (!root.isJsonObject()) {
return null;
}
return out.toString();
}
if (c == 'n' && json.startsWith("null", i)) {
JsonElement value = root.getAsJsonObject().get(key);
return value != null && value.isJsonPrimitive() ? value.getAsString() : null;
} catch (com.google.gson.JsonParseException e) {
return null;
}
int start = i;
while (i < json.length()) {
char ch = json.charAt(i);
if (ch == ',' || ch == '}' || ch == ']') {
break;
}
i++;
}
return json.substring(start, i).trim();
}

static String escapeJson(String raw) {
if (raw == null) {
return "";
}
StringBuilder sb = new StringBuilder(raw.length() + 8);
for (int i = 0; i < raw.length(); i++) {
char ch = raw.charAt(i);
switch (ch) {
case '\\':
case '"':
sb.append('\\').append(ch);
break;
case '\n':
sb.append("\\n");
break;
case '\r':
sb.append("\\r");
break;
case '\t':
sb.append("\\t");
break;
default:
sb.append(ch);
}
}
return sb.toString();
String encoded = new Gson().toJson(raw == null ? "" : raw);
return encoded.substring(1, encoded.length() - 1);
}

private static String readBody(InputStream stream) throws Exception {
Expand Down
Loading