From 73045664ade9078b7b3abe8e0cdab9685b31e295 Mon Sep 17 00:00:00 2001 From: vLuckyyy Date: Tue, 8 Jul 2025 01:42:58 +0200 Subject: [PATCH 1/3] Update GitCheck to include error handling and move to java 17. --- build.gradle.kts | 38 ++--- gradle.properties | 4 + gradle/wrapper/gradle-wrapper.properties | 2 +- .../com/eternalcode/gitcheck/GitCheck.java | 135 +++++++++++++-- .../eternalcode/gitcheck/GitCheckError.java | 67 ++++++++ .../gitcheck/GitCheckErrorHandler.java | 46 ++++++ .../gitcheck/GitCheckErrorType.java | 46 ++++++ .../gitcheck/GitCheckException.java | 17 ++ .../eternalcode/gitcheck/GitCheckResult.java | 72 +++++++- .../gitcheck/git/GitException.java | 1 - .../eternalcode/gitcheck/git/GitRelease.java | 25 +-- .../gitcheck/git/GitReleaseProvider.java | 1 - .../gitcheck/git/GitRepository.java | 40 ++--- .../com/eternalcode/gitcheck/git/GitTag.java | 45 +++-- .../github/GitHubReleaseProvider.java | 84 +++++----- .../eternalcode/gitcheck/github/JSONUtil.java | 36 ++-- .../gitcheck/shared/Preconditions.java | 1 - .../gitcheck/GitCheckErrorHandlerTest.java | 66 ++++++++ .../gitcheck/GitCheckErrorTest.java | 79 +++++++++ .../gitcheck/GitCheckExceptionTest.java | 25 +++ .../gitcheck/GitCheckResultTest.java | 65 ++++++-- .../eternalcode/gitcheck/GitCheckTest.java | 88 +++++++++- .../github/GitHubReleaseProviderTest.java | 156 ++++++++++++++++++ .../github/GitHubVersionProviderTest.java | 47 ------ .../gitcheck/github/JSONUtilTest.java | 2 +- .../mock/MockErrorGitReleaseProvider.java | 14 ++ .../gitcheck/mock/MockGitReleaseProvider.java | 1 - 27 files changed, 975 insertions(+), 228 deletions(-) create mode 100644 gradle.properties create mode 100644 src/main/java/com/eternalcode/gitcheck/GitCheckError.java create mode 100644 src/main/java/com/eternalcode/gitcheck/GitCheckErrorHandler.java create mode 100644 src/main/java/com/eternalcode/gitcheck/GitCheckErrorType.java create mode 100644 src/main/java/com/eternalcode/gitcheck/GitCheckException.java create mode 100644 src/test/java/com/eternalcode/gitcheck/GitCheckErrorHandlerTest.java create mode 100644 src/test/java/com/eternalcode/gitcheck/GitCheckErrorTest.java create mode 100644 src/test/java/com/eternalcode/gitcheck/GitCheckExceptionTest.java create mode 100644 src/test/java/com/eternalcode/gitcheck/github/GitHubReleaseProviderTest.java delete mode 100644 src/test/java/com/eternalcode/gitcheck/github/GitHubVersionProviderTest.java create mode 100644 src/test/java/com/eternalcode/gitcheck/mock/MockErrorGitReleaseProvider.java diff --git a/build.gradle.kts b/build.gradle.kts index e8a0758..607c041 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,6 @@ plugins { `java-library` `maven-publish` - id("com.github.johnrengelman.shadow") version "8.1.0" } group = "com.eternalcode" @@ -11,9 +10,8 @@ val artifactId = "gitcheck" java { withJavadocJar() withSourcesJar() - - sourceCompatibility = JavaVersion.VERSION_1_9 - targetCompatibility = JavaVersion.VERSION_1_9 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } repositories { @@ -21,12 +19,10 @@ repositories { } dependencies { - // https://mvnrepository.com/artifact/com.googlecode.json-simple/json-simple api("com.googlecode.json-simple:json-simple:1.1.1") { exclude(group = "junit") } - - api("org.jetbrains:annotations:24.0.1") + api("org.jetbrains:annotations:26.0.2") testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.2") testImplementation("nl.jqno.equalsverifier:equalsverifier:3.14.1") @@ -34,28 +30,30 @@ dependencies { } publishing { - publications { - create("maven") { - groupId = "$group" - artifactId = artifactId - version = "${project.version}" - - from(components["java"]) - } - } repositories { + mavenLocal() + maven { - name = "eternalcode-repository" url = uri("https://repo.eternalcode.pl/releases") + if (version.toString().endsWith("-SNAPSHOT")) { + url = uri("https://repo.eternalcode.pl/snapshots") + } + credentials { - username = System.getenv("ETERNALCODE_REPO_USERNAME") - password = System.getenv("ETERNALCODE_REPO_PASSWORD") + username = System.getenv("ETERNAL_CODE_MAVEN_USERNAME") + password = System.getenv("ETERNAL_CODE_MAVEN_PASSWORD") } } } + + publications { + create("maven") { + from(components["java"]) + } + } } -tasks.getByName("test") { +tasks.test { useJUnitPlatform() } diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..905a0cd --- /dev/null +++ b/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.configuration-cache=true +org.gradle.caching=true +org.gradle.parallel=true +org.gradle.vfs.watch=false \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index bdc9a83..aa02b02 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/java/com/eternalcode/gitcheck/GitCheck.java b/src/main/java/com/eternalcode/gitcheck/GitCheck.java index baa2d04..6d8d661 100644 --- a/src/main/java/com/eternalcode/gitcheck/GitCheck.java +++ b/src/main/java/com/eternalcode/gitcheck/GitCheck.java @@ -10,14 +10,11 @@ /** * Service for checking if the latest release is up to date. - *

- * This service uses {@link GitReleaseProvider} to get the latest release and compares it with the current tag. - * The current tag is provided by {@link GitTag#of(String)} - *
*/ public class GitCheck { private final GitReleaseProvider versionProvider; + private GitCheckErrorHandler errorHandler; /** * Creates a new instance of {@link GitCheck} with the default {@link GitHubReleaseProvider}. @@ -34,25 +31,92 @@ public GitCheck() { public GitCheck(@NotNull GitReleaseProvider versionProvider) { Preconditions.notNull(versionProvider, "release provider"); this.versionProvider = versionProvider; + this.errorHandler = GitCheckErrorHandler.noOp(); + } + + /** + * Sets the error handler for this GitCheck instance. + * + * @param errorHandler the error handler + * @return this instance for method chaining + */ + @NotNull + public GitCheck onError(@NotNull GitCheckErrorHandler errorHandler) { + Preconditions.notNull(errorHandler, "error handler"); + this.errorHandler = errorHandler; + return this; + } + + /** + * Sets error handler for specific status codes. + * + * @param statusCode the status code to handle + * @param handler the handler for this status code + * @return this instance for method chaining + */ + @NotNull + public GitCheck onStatusCode(int statusCode, @NotNull GitCheckErrorHandler handler) { + Preconditions.notNull(handler, "handler"); + + GitCheckErrorHandler previousHandler = this.errorHandler; + this.errorHandler = error -> { + if (error.hasStatusCode() && error.getStatusCode() == statusCode) { + handler.handle(error); + } + else { + previousHandler.handle(error); + } + }; + return this; + } + + /** + * Sets error handler for specific error types. + * + * @param errorType the error type to handle + * @param handler the handler for this error type + * @return this instance for method chaining + */ + @NotNull + public GitCheck onErrorType(@NotNull GitCheckErrorType errorType, @NotNull GitCheckErrorHandler handler) { + Preconditions.notNull(errorType, "error type"); + Preconditions.notNull(handler, "handler"); + + GitCheckErrorHandler previousHandler = this.errorHandler; + this.errorHandler = error -> { + if (error.getType() == errorType) { + handler.handle(error); + } + else { + previousHandler.handle(error); + } + }; + return this; } /** * Gets the latest release for the given repository. * * @param repository the repository - * @return the latest release + * @return the latest release, or null if error occurred */ @NotNull - public GitRelease getLatestRelease(@NotNull GitRepository repository) { + public GitCheckResult getLatestRelease(@NotNull GitRepository repository) { Preconditions.notNull(repository, "repository"); - return this.versionProvider.getLatestRelease(repository); + try { + GitRelease release = this.versionProvider.getLatestRelease(repository); + return GitCheckResult.success(release, GitTag.of("unknown")); + } + catch (Exception exception) { + GitCheckError error = mapExceptionToError(exception); + this.errorHandler.handle(error); + return GitCheckResult.failure(error); + } } /** * Creates a new instance of {@link GitCheckResult} for the given repository and tag. - * Result contains the latest release and the current tag. - * Use {@link GitCheckResult#isUpToDate()} to check if the latest release is up to date. * * @param repository the repository * @param currentTag the current tag @@ -63,8 +127,55 @@ public GitCheckResult checkRelease(@NotNull GitRepository repository, @NotNull G Preconditions.notNull(repository, "repository"); Preconditions.notNull(currentTag, "current tag"); - GitRelease latestRelease = this.getLatestRelease(repository); - return new GitCheckResult(latestRelease, currentTag); + try { + GitRelease latestRelease = this.versionProvider.getLatestRelease(repository); + return GitCheckResult.success(latestRelease, currentTag); + } + catch (Exception exception) { + GitCheckError error = mapExceptionToError(exception); + this.errorHandler.handle(error); + return GitCheckResult.failure(error); + } } -} + private GitCheckError mapExceptionToError(Exception exception) { + String message = exception.getMessage(); + + if (message.contains("404")) { + if (message.contains("repository")) { + return new GitCheckError(GitCheckErrorType.REPOSITORY_NOT_FOUND, message, 404, exception); + } + else { + return new GitCheckError(GitCheckErrorType.RELEASE_NOT_FOUND, message, 404, exception); + } + } + + if (message.contains("403")) { + return new GitCheckError(GitCheckErrorType.RATE_LIMIT_EXCEEDED, message, 403, exception); + } + + if (message.contains("401")) { + return new GitCheckError(GitCheckErrorType.AUTHENTICATION_REQUIRED, message, 401, exception); + } + + if (message.contains("Unexpected response code")) { + try { + int statusCode = Integer.parseInt(message.replaceAll(".*: (\\d+).*", "$1")); + return new GitCheckError(GitCheckErrorType.HTTP_ERROR, message, statusCode, exception); + } + catch (NumberFormatException e) { + return new GitCheckError(GitCheckErrorType.HTTP_ERROR, message, exception); + } + } + + if (message.contains("Invalid JSON") || message.contains("not a JSON object")) { + return new GitCheckError(GitCheckErrorType.INVALID_RESPONSE, message, exception); + } + + if (exception instanceof java.io.IOException) { + return new GitCheckError(GitCheckErrorType.NETWORK_ERROR, message, exception); + } + + return new GitCheckError(GitCheckErrorType.UNKNOWN, message, exception); + } +} \ No newline at end of file diff --git a/src/main/java/com/eternalcode/gitcheck/GitCheckError.java b/src/main/java/com/eternalcode/gitcheck/GitCheckError.java new file mode 100644 index 0000000..3489ef2 --- /dev/null +++ b/src/main/java/com/eternalcode/gitcheck/GitCheckError.java @@ -0,0 +1,67 @@ +package com.eternalcode.gitcheck; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Represents an error that occurred during git check operation. + */ +public class GitCheckError { + + private final GitCheckErrorType type; + private final String message; + private final int statusCode; + private final Throwable cause; + + public GitCheckError( + @NotNull GitCheckErrorType type, + @NotNull String message, + int statusCode, + @Nullable Throwable cause + ) { + this.type = type; + this.message = message; + this.statusCode = statusCode; + this.cause = cause; + } + + public GitCheckError(@NotNull GitCheckErrorType type, @NotNull String message, @Nullable Throwable cause) { + this(type, message, -1, cause); + } + + public GitCheckError(@NotNull GitCheckErrorType type, @NotNull String message) { + this(type, message, -1, null); + } + + @NotNull + public GitCheckErrorType getType() { + return type; + } + + @NotNull + public String getMessage() { + return message; + } + + public int getStatusCode() { + return statusCode; + } + + @Nullable + public Throwable getCause() { + return cause; + } + + public boolean hasStatusCode() { + return statusCode != -1; + } + + @Override + public String toString() { + return "GitCheckError{" + + "type=" + type + + ", message='" + message + '\'' + + (hasStatusCode() ? ", statusCode=" + statusCode : "") + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/eternalcode/gitcheck/GitCheckErrorHandler.java b/src/main/java/com/eternalcode/gitcheck/GitCheckErrorHandler.java new file mode 100644 index 0000000..2787794 --- /dev/null +++ b/src/main/java/com/eternalcode/gitcheck/GitCheckErrorHandler.java @@ -0,0 +1,46 @@ +package com.eternalcode.gitcheck; + +import org.jetbrains.annotations.NotNull; + +/** + * Interface for handling git check errors. + */ +@FunctionalInterface +public interface GitCheckErrorHandler { + + /** + * Creates a no-op error handler. + * + * @return no-op error handler + */ + @NotNull + static GitCheckErrorHandler noOp() { + return error -> {}; + } + /** + * Creates an error handler that prints to console. + * + * @return console error handler + */ + @NotNull + static GitCheckErrorHandler console() { + return error -> System.err.println("GitCheck Error: " + error); + } + /** + * Creates an error handler that throws exceptions. + * + * @return throwing error handler + */ + @NotNull + static GitCheckErrorHandler throwing() { + return error -> { + throw new GitCheckException(error.getMessage(), error.getCause()); + }; + } + /** + * Handles the error. + * + * @param error the error to handle + */ + void handle(@NotNull GitCheckError error); +} \ No newline at end of file diff --git a/src/main/java/com/eternalcode/gitcheck/GitCheckErrorType.java b/src/main/java/com/eternalcode/gitcheck/GitCheckErrorType.java new file mode 100644 index 0000000..23a59dd --- /dev/null +++ b/src/main/java/com/eternalcode/gitcheck/GitCheckErrorType.java @@ -0,0 +1,46 @@ +package com.eternalcode.gitcheck; + +/** + * Types of errors that can occur during git check operations. + */ +public enum GitCheckErrorType { + /** + * Repository not found (404). + */ + REPOSITORY_NOT_FOUND, + + /** + * Release not found (404). + */ + RELEASE_NOT_FOUND, + + /** + * Rate limit exceeded (403). + */ + RATE_LIMIT_EXCEEDED, + + /** + * Authentication required (401). + */ + AUTHENTICATION_REQUIRED, + + /** + * Network connection error. + */ + NETWORK_ERROR, + + /** + * Invalid JSON response. + */ + INVALID_RESPONSE, + + /** + * HTTP error with specific status code. + */ + HTTP_ERROR, + + /** + * Unknown error. + */ + UNKNOWN +} \ No newline at end of file diff --git a/src/main/java/com/eternalcode/gitcheck/GitCheckException.java b/src/main/java/com/eternalcode/gitcheck/GitCheckException.java new file mode 100644 index 0000000..9e03e38 --- /dev/null +++ b/src/main/java/com/eternalcode/gitcheck/GitCheckException.java @@ -0,0 +1,17 @@ +package com.eternalcode.gitcheck; + +import com.eternalcode.gitcheck.git.GitException; + +/** + * Exception thrown when git check operation fails. + */ +public class GitCheckException extends GitException { + + public GitCheckException(String message) { + super(message); + } + + public GitCheckException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/com/eternalcode/gitcheck/GitCheckResult.java b/src/main/java/com/eternalcode/gitcheck/GitCheckResult.java index 86c19c7..b10d308 100644 --- a/src/main/java/com/eternalcode/gitcheck/GitCheckResult.java +++ b/src/main/java/com/eternalcode/gitcheck/GitCheckResult.java @@ -3,6 +3,7 @@ import com.eternalcode.gitcheck.git.GitRelease; import com.eternalcode.gitcheck.git.GitTag; import com.eternalcode.gitcheck.shared.Preconditions; +import java.util.Optional; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; @@ -14,6 +15,7 @@ public class GitCheckResult { private final GitRelease latestRelease; private final GitTag currentTag; + private final GitCheckError error; @ApiStatus.Internal GitCheckResult(@NotNull GitRelease latestRelease, @NotNull GitTag currentTag) { @@ -22,15 +24,64 @@ public class GitCheckResult { this.latestRelease = latestRelease; this.currentTag = currentTag; + this.error = null; + } + + @ApiStatus.Internal + GitCheckResult(@NotNull GitCheckError error) { + Preconditions.notNull(error, "error"); + + this.latestRelease = null; + this.currentTag = null; + this.error = error; + } + + /** + * Creates a successful result. + * + * @param latestRelease the latest release + * @param currentTag the current tag + * @return successful result + */ + @ApiStatus.Internal + @NotNull + public static GitCheckResult success(@NotNull GitRelease latestRelease, @NotNull GitTag currentTag) { + return new GitCheckResult(latestRelease, currentTag); + } + + /** + * Creates a failed result. + * + * @param error the error + * @return failed result + */ + @ApiStatus.Internal + @NotNull + public static GitCheckResult failure(@NotNull GitCheckError error) { + return new GitCheckResult(error); + } + + /** + * Checks if the operation was successful. + * + * @return {@code true} if successful, {@code false} otherwise + */ + @Contract(pure = true) + public boolean isSuccessful() { + return this.error == null; } /** * Gets the latest release. * * @return the latest release + * @throws IllegalStateException if the operation was not successful */ @NotNull public GitRelease getLatestRelease() { + if (!isSuccessful()) { + throw new IllegalStateException("Cannot get latest release from failed result. Check isSuccessful() first."); + } return this.latestRelease; } @@ -38,20 +89,37 @@ public GitRelease getLatestRelease() { * Gets the current tag. * * @return the current tag + * @throws IllegalStateException if the operation was not successful */ @NotNull public GitTag getCurrentTag() { + if (!isSuccessful()) { + throw new IllegalStateException("Cannot get current tag from failed result. Check isSuccessful() first."); + } return this.currentTag; } + /** + * Gets the error if the operation failed. + * + * @return the error or empty if successful + */ + @NotNull + public Optional getError() { + return Optional.ofNullable(this.error); + } + /** * Checks if the latest release is up to date. * * @return {@code true} if the latest release is up to date, {@code false} otherwise + * @throws IllegalStateException if the operation was not successful */ @Contract(pure = true) public boolean isUpToDate() { + if (!isSuccessful()) { + throw new IllegalStateException("Cannot check if up to date from failed result. Check isSuccessful() first."); + } return this.latestRelease.getTag().equals(this.currentTag); } - -} +} \ No newline at end of file diff --git a/src/main/java/com/eternalcode/gitcheck/git/GitException.java b/src/main/java/com/eternalcode/gitcheck/git/GitException.java index 67a2165..7a3e29b 100644 --- a/src/main/java/com/eternalcode/gitcheck/git/GitException.java +++ b/src/main/java/com/eternalcode/gitcheck/git/GitException.java @@ -9,5 +9,4 @@ public GitException(String message) { public GitException(String message, Throwable cause) { super(message, cause); } - } diff --git a/src/main/java/com/eternalcode/gitcheck/git/GitRelease.java b/src/main/java/com/eternalcode/gitcheck/git/GitRelease.java index 6858706..f84c779 100644 --- a/src/main/java/com/eternalcode/gitcheck/git/GitRelease.java +++ b/src/main/java/com/eternalcode/gitcheck/git/GitRelease.java @@ -1,11 +1,10 @@ package com.eternalcode.gitcheck.git; import com.eternalcode.gitcheck.shared.Preconditions; +import java.time.Instant; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; -import java.time.Instant; - public final class GitRelease { private final String name; @@ -14,7 +13,13 @@ public final class GitRelease { private final String pageUrl; private final Instant publishedAt; - private GitRelease(@NotNull String name, @NotNull String branch, @NotNull GitTag tag, @NotNull String pageUrl, @NotNull Instant publishedAt) { + private GitRelease( + @NotNull String name, + @NotNull String branch, + @NotNull GitTag tag, + @NotNull String pageUrl, + @NotNull Instant publishedAt + ) { this.name = name; this.branch = branch; this.tag = tag; @@ -22,6 +27,12 @@ private GitRelease(@NotNull String name, @NotNull String branch, @NotNull GitTag this.publishedAt = publishedAt; } + @NotNull + @Contract("-> new") + public static Builder builder() { + return new Builder(); + } + @NotNull public String getName() { return this.name; @@ -47,12 +58,6 @@ public Instant getPublishedAt() { return this.publishedAt; } - @NotNull - @Contract("-> new") - public static Builder builder() { - return new Builder(); - } - public static class Builder { private String name; @@ -100,7 +105,5 @@ public GitRelease build() { return new GitRelease(this.name, this.branch, this.tag, this.pageUrl, this.publishedAt); } - } - } diff --git a/src/main/java/com/eternalcode/gitcheck/git/GitReleaseProvider.java b/src/main/java/com/eternalcode/gitcheck/git/GitReleaseProvider.java index 450f757..155f61f 100644 --- a/src/main/java/com/eternalcode/gitcheck/git/GitReleaseProvider.java +++ b/src/main/java/com/eternalcode/gitcheck/git/GitReleaseProvider.java @@ -3,5 +3,4 @@ public interface GitReleaseProvider { GitRelease getLatestRelease(GitRepository repository); - } diff --git a/src/main/java/com/eternalcode/gitcheck/git/GitRepository.java b/src/main/java/com/eternalcode/gitcheck/git/GitRepository.java index 8874462..3b2cf39 100644 --- a/src/main/java/com/eternalcode/gitcheck/git/GitRepository.java +++ b/src/main/java/com/eternalcode/gitcheck/git/GitRepository.java @@ -1,9 +1,8 @@ package com.eternalcode.gitcheck.git; import com.eternalcode.gitcheck.shared.Preconditions; -import org.jetbrains.annotations.NotNull; - import java.util.Objects; +import org.jetbrains.annotations.NotNull; /** * Represents a git repository. @@ -18,9 +17,9 @@ public final class GitRepository { /** * Creates a new instance of {@link GitRepository} with the given owner and name. * - * @see #of(String, String) * @param owner the owner of the repository - * @param name the name of the repository + * @param name the name of the repository + * @see #of(String, String) */ private GitRepository(@NotNull String owner, @NotNull String name) { Preconditions.notNull(owner, "owner"); @@ -32,6 +31,17 @@ private GitRepository(@NotNull String owner, @NotNull String name) { this.name = name; } + /** + * Creates a new instance of {@link GitRepository} with the given owner and name. + * + * @param owner the owner of the repository + * @param name the name of the repository + * @return repository of the given owner and name + */ + public static GitRepository of(@NotNull String owner, @NotNull String name) { + return new GitRepository(owner, name); + } + @NotNull public String getOwner() { return this.owner; @@ -52,29 +62,13 @@ public boolean equals(Object object) { if (this == object) { return true; } - - if (!(object instanceof GitRepository)) { - return false; - } - - GitRepository that = (GitRepository) object; - return this.owner.equals(that.owner) && this.name.equals(that.name); + return object instanceof GitRepository that && + this.owner.equals(that.owner) && + this.name.equals(that.name); } @Override public int hashCode() { return Objects.hash(this.owner, this.name); } - - /** - * Creates a new instance of {@link GitRepository} with the given owner and name. - * - * @param owner the owner of the repository - * @param name the name of the repository - * @return repository of the given owner and name - */ - public static GitRepository of(@NotNull String owner, @NotNull String name) { - return new GitRepository(owner, name); - } - } diff --git a/src/main/java/com/eternalcode/gitcheck/git/GitTag.java b/src/main/java/com/eternalcode/gitcheck/git/GitTag.java index 21bf430..2dd2f9e 100644 --- a/src/main/java/com/eternalcode/gitcheck/git/GitTag.java +++ b/src/main/java/com/eternalcode/gitcheck/git/GitTag.java @@ -1,11 +1,10 @@ package com.eternalcode.gitcheck.git; import com.eternalcode.gitcheck.shared.Preconditions; +import java.util.Objects; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; -import java.util.Objects; - /** * Represents a git tag. */ @@ -16,14 +15,32 @@ public final class GitTag { /** * Creates a new instance of {@link GitTag} with the given tag. * - * @see #of(String) * @param tag the tag + * @see #of(String) */ private GitTag(@NotNull String tag) { Preconditions.notNull(tag, "tag"); this.tag = tag; } + /** + * Creates a new instance of {@link GitTag} with the given tag. + *

+ * Tag should be in the format of {@code v1.0.0}, but it can be anything. + *
+ * This method does not validate the tag. + *
+ *

+ * + * @param tag the tag + * @return the git tag + * @throws IllegalArgumentException if the tag is null + */ + @Contract("_ -> new") + public static GitTag of(@NotNull String tag) { + return new GitTag(tag); + } + @NotNull public String getTag() { return this.tag; @@ -35,11 +52,10 @@ public boolean equals(Object object) { return true; } - if (!(object instanceof GitTag)) { + if (!(object instanceof GitTag gitTag)) { return false; } - GitTag gitTag = (GitTag) object; return this.tag.equals(gitTag.tag); } @@ -52,23 +68,4 @@ public int hashCode() { public String toString() { return this.tag; } - - /** - * Creates a new instance of {@link GitTag} with the given tag. - *

- * Tag should be in the format of {@code v1.0.0}, but it can be anything. - *
- * This method does not validate the tag. - *
- *

- * - * @throws IllegalArgumentException if the tag is null - * @param tag the tag - * @return the git tag - */ - @Contract("_ -> new") - public static GitTag of(@NotNull String tag) { - return new GitTag(tag); - } - } diff --git a/src/main/java/com/eternalcode/gitcheck/github/GitHubReleaseProvider.java b/src/main/java/com/eternalcode/gitcheck/github/GitHubReleaseProvider.java index 0d67221..763ca9a 100644 --- a/src/main/java/com/eternalcode/gitcheck/github/GitHubReleaseProvider.java +++ b/src/main/java/com/eternalcode/gitcheck/github/GitHubReleaseProvider.java @@ -4,22 +4,29 @@ import com.eternalcode.gitcheck.git.GitRelease; import com.eternalcode.gitcheck.git.GitReleaseProvider; import com.eternalcode.gitcheck.git.GitRepository; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; import org.json.simple.JSONObject; import org.json.simple.parser.JSONParser; import org.json.simple.parser.ParseException; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.URL; -import java.net.URLConnection; -import java.util.stream.Collectors; - public class GitHubReleaseProvider implements GitReleaseProvider { private static final String USER_AGENT = "Mozilla/5.0"; - private static final String GET_LATEST_RELEASE = "https://api.github.com/repos/%s/releases/latest"; + private static final String GET_LATEST_RELEASE_URL = "https://api.github.com/repos/%s/releases/latest"; + + private final HttpClient httpClient; + + public GitHubReleaseProvider(HttpClient httpClient) { + this.httpClient = httpClient; + } + + public GitHubReleaseProvider() { + this(HttpClient.newHttpClient()); + } @Override public GitRelease getLatestRelease(GitRepository repository) { @@ -35,51 +42,42 @@ public GitRelease getLatestRelease(GitRepository repository) { } private JSONObject requestLastRelease(GitRepository repository) { - String getUrl = String.format(GET_LATEST_RELEASE, repository.getFullName()); - - try { - URL url = new URL(getUrl); - URLConnection urlConnection = url.openConnection(); - - if (!(urlConnection instanceof HttpURLConnection)) { - throw new GitException("The URL is not an HTTP URL"); - } - - HttpURLConnection httpURLConnection = (HttpURLConnection) urlConnection; - - httpURLConnection.setRequestProperty("User-Agent", USER_AGENT); - httpURLConnection.connect(); + String formattedUrl = String.format(GET_LATEST_RELEASE_URL, repository.getFullName()); - if (httpURLConnection.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) { - throw new GitException("The release of the repository " + repository.getFullName() + " was not found"); - } + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(formattedUrl)) + .header("User-Agent", USER_AGENT) + .GET() + .build(); - if (httpURLConnection.getResponseCode() != HttpURLConnection.HTTP_OK) { - throw new GitException("The response code is not 200"); + try { + HttpResponse response = this.httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + switch (response.statusCode()) { + case 404 -> throw new GitException("Repository or release not found: " + repository.getFullName() + " (404)"); + case 403 -> throw new GitException("Rate limit exceeded or access forbidden (403)"); + case 401 -> throw new GitException("Authentication required (401)"); + case 200 -> { + } + default -> throw new GitException("Unexpected response code: " + response.statusCode()); } - String response = this.readResponse(httpURLConnection); - + String responseBody = response.body(); JSONParser parser = new JSONParser(); - Object parsed = parser.parse(response); + Object parsed = parser.parse(responseBody); if (!(parsed instanceof JSONObject)) { - throw new GitException("The response is not a JSON object"); + throw new GitException("Invalid JSON response: not a JSON object"); } return (JSONObject) parsed; - } catch (IOException exception) { - throw new GitException("Invalid URL", exception); - } catch (ParseException exception) { - throw new GitException("Invalid JSON response", exception); } - } - - private String readResponse(HttpURLConnection connection) throws IOException { - try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { - return reader.lines() - .collect(Collectors.joining()); + catch (IOException | InterruptedException exception) { + Thread.currentThread().interrupt(); + throw new GitException("Network error during HTTP request to GitHub API", exception); + } + catch (ParseException exception) { + throw new GitException("Invalid JSON response: parse error", exception); } } - } diff --git a/src/main/java/com/eternalcode/gitcheck/github/JSONUtil.java b/src/main/java/com/eternalcode/gitcheck/github/JSONUtil.java index c9e89ab..05f45f8 100644 --- a/src/main/java/com/eternalcode/gitcheck/github/JSONUtil.java +++ b/src/main/java/com/eternalcode/gitcheck/github/JSONUtil.java @@ -1,44 +1,44 @@ package com.eternalcode.gitcheck.github; import com.eternalcode.gitcheck.git.GitTag; -import org.json.simple.JSONObject; - import java.time.Instant; -import java.util.NoSuchElementException; +import java.util.Optional; +import org.json.simple.JSONObject; final class JSONUtil { private JSONUtil() { + throw new UnsupportedOperationException("Cannot create instance of utility class JSONUtil"); } static String asString(JSONObject jsonObject, String key) { - return as(jsonObject, key, String.class); + return asOpt(jsonObject, key, String.class) + .orElseThrow(() -> new IllegalArgumentException("No value found for key: " + key)); } static GitTag asGitTag(JSONObject jsonObject, String key) { - String rawTag = as(jsonObject, key, String.class); - - return GitTag.of(rawTag); + return asOpt(jsonObject, key, String.class) + .map(GitTag::of) + .orElseThrow(() -> new IllegalArgumentException("No value found for key: " + key)); } static Instant asInstant(JSONObject jsonObject, String key) { - String rawDateTime = as(jsonObject, key, String.class); - - return Instant.parse(rawDateTime); + return asOpt(jsonObject, key, String.class) + .map(Instant::parse) + .orElseThrow(() -> new IllegalArgumentException("No value found for key: " + key)); } - private static T as(JSONObject jsonObject, String key, Class clazz) { - Object obj = jsonObject.get(key); + private static Optional asOpt(JSONObject jsonObject, String key, Class clazz) { + Object value = jsonObject.get(key); - if (obj == null) { - throw new NoSuchElementException("No value for key " + key); + if (value == null) { + return Optional.empty(); } - if (!clazz.isInstance(obj)) { + if (!clazz.isInstance(value)) { throw new IllegalArgumentException("Value for key " + key + " is not of type " + clazz.getSimpleName()); } - return clazz.cast(obj); + return Optional.of(clazz.cast(value)); } - -} +} \ No newline at end of file diff --git a/src/main/java/com/eternalcode/gitcheck/shared/Preconditions.java b/src/main/java/com/eternalcode/gitcheck/shared/Preconditions.java index b15febf..45b4d29 100644 --- a/src/main/java/com/eternalcode/gitcheck/shared/Preconditions.java +++ b/src/main/java/com/eternalcode/gitcheck/shared/Preconditions.java @@ -17,5 +17,4 @@ public static void notEmpty(String owner, String name) { throw new IllegalArgumentException(name + " cannot be empty"); } } - } diff --git a/src/test/java/com/eternalcode/gitcheck/GitCheckErrorHandlerTest.java b/src/test/java/com/eternalcode/gitcheck/GitCheckErrorHandlerTest.java new file mode 100644 index 0000000..a927c0a --- /dev/null +++ b/src/test/java/com/eternalcode/gitcheck/GitCheckErrorHandlerTest.java @@ -0,0 +1,66 @@ +package com.eternalcode.gitcheck; + +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import static org.junit.jupiter.api.Assertions.*; + +class GitCheckErrorHandlerTest { + + @Test + void testNoOpHandler() { + GitCheckErrorHandler handler = GitCheckErrorHandler.noOp(); + GitCheckError error = new GitCheckError(GitCheckErrorType.UNKNOWN, "Test error"); + + // Should not throw any exception + assertDoesNotThrow(() -> handler.handle(error)); + } + + @Test + void testConsoleHandler() { + GitCheckErrorHandler handler = GitCheckErrorHandler.console(); + GitCheckError error = new GitCheckError(GitCheckErrorType.UNKNOWN, "Test error"); + + // Capture System.err output + ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + PrintStream originalErr = System.err; + System.setErr(new PrintStream(errContent)); + + try { + handler.handle(error); + String output = errContent.toString(); + assertTrue(output.contains("GitCheck Error:")); + assertTrue(output.contains("Test error")); + } finally { + System.setErr(originalErr); + } + } + + @Test + void testThrowingHandler() { + GitCheckErrorHandler handler = GitCheckErrorHandler.throwing(); + GitCheckError error = new GitCheckError(GitCheckErrorType.UNKNOWN, "Test error"); + + GitCheckException exception = assertThrows(GitCheckException.class, () -> + handler.handle(error) + ); + + assertEquals("Test error", exception.getMessage()); + } + + @Test + void testThrowingHandlerWithCause() { + GitCheckErrorHandler handler = GitCheckErrorHandler.throwing(); + RuntimeException cause = new RuntimeException("Original cause"); + GitCheckError error = new GitCheckError(GitCheckErrorType.UNKNOWN, "Test error", cause); + + GitCheckException exception = assertThrows(GitCheckException.class, () -> + handler.handle(error) + ); + + assertEquals("Test error", exception.getMessage()); + assertEquals(cause, exception.getCause()); + } +} \ No newline at end of file diff --git a/src/test/java/com/eternalcode/gitcheck/GitCheckErrorTest.java b/src/test/java/com/eternalcode/gitcheck/GitCheckErrorTest.java new file mode 100644 index 0000000..e0d333c --- /dev/null +++ b/src/test/java/com/eternalcode/gitcheck/GitCheckErrorTest.java @@ -0,0 +1,79 @@ +package com.eternalcode.gitcheck; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class GitCheckErrorTest { + + @Test + void testErrorWithAllParameters() { + Throwable cause = new RuntimeException("Test cause"); + GitCheckError error = new GitCheckError( + GitCheckErrorType.REPOSITORY_NOT_FOUND, + "Test message", + 404, + cause + ); + + assertEquals(GitCheckErrorType.REPOSITORY_NOT_FOUND, error.getType()); + assertEquals("Test message", error.getMessage()); + assertEquals(404, error.getStatusCode()); + assertEquals(cause, error.getCause()); + assertTrue(error.hasStatusCode()); + } + + @Test + void testErrorWithoutStatusCode() { + Throwable cause = new RuntimeException("Test cause"); + GitCheckError error = new GitCheckError( + GitCheckErrorType.NETWORK_ERROR, + "Test message", + cause + ); + + assertEquals(GitCheckErrorType.NETWORK_ERROR, error.getType()); + assertEquals("Test message", error.getMessage()); + assertEquals(-1, error.getStatusCode()); + assertEquals(cause, error.getCause()); + assertFalse(error.hasStatusCode()); + } + + @Test + void testErrorWithoutCause() { + GitCheckError error = new GitCheckError( + GitCheckErrorType.INVALID_RESPONSE, + "Test message" + ); + + assertEquals(GitCheckErrorType.INVALID_RESPONSE, error.getType()); + assertEquals("Test message", error.getMessage()); + assertEquals(-1, error.getStatusCode()); + assertNull(error.getCause()); + assertFalse(error.hasStatusCode()); + } + + @Test + void testToString() { + GitCheckError errorWithStatus = new GitCheckError( + GitCheckErrorType.REPOSITORY_NOT_FOUND, + "Test message", + 404, + null + ); + + GitCheckError errorWithoutStatus = new GitCheckError( + GitCheckErrorType.NETWORK_ERROR, + "Test message" + ); + + String toStringWithStatus = errorWithStatus.toString(); + String toStringWithoutStatus = errorWithoutStatus.toString(); + + assertTrue(toStringWithStatus.contains("statusCode=404")); + assertFalse(toStringWithoutStatus.contains("statusCode")); + + assertTrue(toStringWithStatus.contains("REPOSITORY_NOT_FOUND")); + assertTrue(toStringWithoutStatus.contains("NETWORK_ERROR")); + } +} \ No newline at end of file diff --git a/src/test/java/com/eternalcode/gitcheck/GitCheckExceptionTest.java b/src/test/java/com/eternalcode/gitcheck/GitCheckExceptionTest.java new file mode 100644 index 0000000..f8568e2 --- /dev/null +++ b/src/test/java/com/eternalcode/gitcheck/GitCheckExceptionTest.java @@ -0,0 +1,25 @@ +package com.eternalcode.gitcheck; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class GitCheckExceptionTest { + + @Test + void testMessageConstructor() { + GitCheckException exception = new GitCheckException("Test message"); + + assertEquals("Test message", exception.getMessage()); + assertNull(exception.getCause()); + } + + @Test + void testMessageAndCauseConstructor() { + RuntimeException cause = new RuntimeException("Original cause"); + GitCheckException exception = new GitCheckException("Test message", cause); + + assertEquals("Test message", exception.getMessage()); + assertEquals(cause, exception.getCause()); + } +} \ No newline at end of file diff --git a/src/test/java/com/eternalcode/gitcheck/GitCheckResultTest.java b/src/test/java/com/eternalcode/gitcheck/GitCheckResultTest.java index 873e0ff..c613c36 100644 --- a/src/test/java/com/eternalcode/gitcheck/GitCheckResultTest.java +++ b/src/test/java/com/eternalcode/gitcheck/GitCheckResultTest.java @@ -6,9 +6,7 @@ import java.time.Instant; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; class GitCheckResultTest { @@ -20,25 +18,66 @@ class GitCheckResultTest { .publishedAt(Instant.parse("2020-01-01T00:00:00Z")) .build(); - private final GitCheckResult noActualResult = new GitCheckResult(this.release, GitTag.of("v1.0.0")); - private final GitCheckResult actualResult = new GitCheckResult(this.release, GitTag.of("v2.0.0")); + private final GitCheckResult successResult = GitCheckResult.success(this.release, GitTag.of("v1.0.0")); + private final GitCheckResult upToDateResult = GitCheckResult.success(this.release, GitTag.of("v2.0.0")); + private final GitCheckError error = new GitCheckError(GitCheckErrorType.REPOSITORY_NOT_FOUND, "Test error", 404, null); + private final GitCheckResult failedResult = GitCheckResult.failure(error); + + @Test + void testSuccessfulResult() { + assertTrue(successResult.isSuccessful()); + assertTrue(upToDateResult.isSuccessful()); + assertFalse(failedResult.isSuccessful()); + } @Test void testIsUpToDate() { - assertFalse(this.noActualResult.isUpToDate()); - assertTrue(this.actualResult.isUpToDate()); + assertFalse(successResult.isUpToDate()); + assertTrue(upToDateResult.isUpToDate()); + } + + @Test + void testGetLatestReleaseSuccess() { + assertEquals(release, successResult.getLatestRelease()); + assertEquals(release, upToDateResult.getLatestRelease()); } @Test - void testGetRelease() { - assertEquals(this.release, this.noActualResult.getLatestRelease()); - assertEquals(this.release, this.actualResult.getLatestRelease()); + void testGetLatestReleaseFailure() { + assertThrows(IllegalStateException.class, () -> failedResult.getLatestRelease()); } @Test - void testGetActualTag() { - assertEquals(GitTag.of("v1.0.0"), this.noActualResult.getCurrentTag()); - assertEquals(GitTag.of("v2.0.0"), this.actualResult.getCurrentTag()); + void testGetCurrentTagSuccess() { + assertEquals(GitTag.of("v1.0.0"), successResult.getCurrentTag()); + assertEquals(GitTag.of("v2.0.0"), upToDateResult.getCurrentTag()); } + @Test + void testGetCurrentTagFailure() { + assertThrows(IllegalStateException.class, () -> failedResult.getCurrentTag()); + } + + @Test + void testIsUpToDateFailure() { + assertThrows(IllegalStateException.class, () -> failedResult.isUpToDate()); + } + + @Test + void testGetError() { + assertTrue(successResult.getError().isEmpty()); + assertTrue(upToDateResult.getError().isEmpty()); + + assertTrue(failedResult.getError().isPresent()); + assertEquals(error, failedResult.getError().get()); + } + + @Test + void testStaticFactoryMethods() { + GitCheckResult success = GitCheckResult.success(release, GitTag.of("v1.0.0")); + GitCheckResult failure = GitCheckResult.failure(error); + + assertTrue(success.isSuccessful()); + assertFalse(failure.isSuccessful()); + } } \ No newline at end of file diff --git a/src/test/java/com/eternalcode/gitcheck/GitCheckTest.java b/src/test/java/com/eternalcode/gitcheck/GitCheckTest.java index 88925ee..d362e83 100644 --- a/src/test/java/com/eternalcode/gitcheck/GitCheckTest.java +++ b/src/test/java/com/eternalcode/gitcheck/GitCheckTest.java @@ -4,12 +4,12 @@ import com.eternalcode.gitcheck.git.GitRepository; import com.eternalcode.gitcheck.git.GitTag; import com.eternalcode.gitcheck.mock.MockGitReleaseProvider; +import com.eternalcode.gitcheck.mock.MockErrorGitReleaseProvider; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.*; class GitCheckTest { @@ -19,13 +19,15 @@ void testDefaultConstructor() { } @Test - void testGetLatestRelease() { + void testGetLatestReleaseSuccess() { GitCheck gitCheck = new GitCheck(new MockGitReleaseProvider()); GitRepository repository = GitRepository.of("EternalCodeTeam", "ChatFormatter"); - GitRelease release = gitCheck.getLatestRelease(repository); + GitCheckResult result = gitCheck.getLatestRelease(repository); - assertNotNull(release); + assertTrue(result.isSuccessful()); + assertNotNull(result.getLatestRelease()); + assertTrue(result.getError().isEmpty()); } @Test @@ -36,16 +38,84 @@ void testGetLatestReleaseWithNullRepository() { } @Test - void testCheckRelease() { + void testCheckReleaseSuccess() { GitCheck gitCheck = new GitCheck(new MockGitReleaseProvider()); GitRepository repository = GitRepository.of("EternalCodeTeam", "ChatFormatter"); GitTag tag = GitTag.of("v1.0.0"); GitCheckResult result = gitCheck.checkRelease(repository, tag); - assertNotNull(result); + assertTrue(result.isSuccessful()); assertNotNull(result.getLatestRelease()); assertEquals(tag, result.getCurrentTag()); + assertTrue(result.getError().isEmpty()); + } + + @Test + void testStatusCodeErrorHandler() { + AtomicReference capturedError = new AtomicReference<>(); + + GitCheck gitCheck = new GitCheck(new MockErrorGitReleaseProvider()) + .onStatusCode(404, capturedError::set); + + GitRepository repository = GitRepository.of("EternalCodeTeam", "ChatFormatter"); + GitCheckResult result = gitCheck.checkRelease(repository, GitTag.of("v1.0.0")); + + assertFalse(result.isSuccessful()); + assertNotNull(capturedError.get()); + assertEquals(404, capturedError.get().getStatusCode()); + } + + + @Test + void testBuiltInErrorHandlers() { + assertDoesNotThrow(() -> { + GitCheck gitCheck = new GitCheck(new MockGitReleaseProvider()) + .onError(GitCheckErrorHandler.noOp()) + .onError(GitCheckErrorHandler.console()); + }); + } + + @Test + void testThrowingErrorHandler() { + GitCheck gitCheck = new GitCheck(new MockErrorGitReleaseProvider()) + .onError(GitCheckErrorHandler.throwing()); + + GitRepository repository = GitRepository.of("EternalCodeTeam", "ChatFormatter"); + + assertThrows(GitCheckException.class, () -> + gitCheck.checkRelease(repository, GitTag.of("v1.0.0")) + ); } + @Test + void testNullErrorHandler() { + GitCheck gitCheck = new GitCheck(new MockGitReleaseProvider()); + + assertThrows(IllegalArgumentException.class, () -> + gitCheck.onError(null) + ); + } + + @Test + void testNullErrorTypeHandler() { + GitCheck gitCheck = new GitCheck(new MockGitReleaseProvider()); + + assertThrows(IllegalArgumentException.class, () -> + gitCheck.onErrorType(null, GitCheckErrorHandler.noOp()) + ); + + assertThrows(IllegalArgumentException.class, () -> + gitCheck.onErrorType(GitCheckErrorType.UNKNOWN, null) + ); + } + + @Test + void testNullStatusCodeHandler() { + GitCheck gitCheck = new GitCheck(new MockGitReleaseProvider()); + + assertThrows(IllegalArgumentException.class, () -> + gitCheck.onStatusCode(404, null) + ); + } } \ No newline at end of file diff --git a/src/test/java/com/eternalcode/gitcheck/github/GitHubReleaseProviderTest.java b/src/test/java/com/eternalcode/gitcheck/github/GitHubReleaseProviderTest.java new file mode 100644 index 0000000..90d9044 --- /dev/null +++ b/src/test/java/com/eternalcode/gitcheck/github/GitHubReleaseProviderTest.java @@ -0,0 +1,156 @@ +package com.eternalcode.gitcheck.github; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.eternalcode.gitcheck.git.GitException; +import com.eternalcode.gitcheck.git.GitRelease; +import com.eternalcode.gitcheck.git.GitRepository; +import java.net.Authenticator; +import java.net.CookieHandler; +import java.net.ProxySelector; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Version; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.Collections; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSession; +import org.junit.jupiter.api.Test; + +class GitHubReleaseProviderTest { + + @Test + void testGetLatestReleaseSuccess() { + String json = """ + { + "name": "v1.0.0", + "target_commitish": "main", + "tag_name": "v1.0.0", + "html_url": "https://github.com/test/repo/releases/tag/v1.0.0", + "published_at": "2020-01-01T00:00:00Z" + } + """; + + HttpClient httpClient = createMockClient(200, json); + + GitHubReleaseProvider provider = new GitHubReleaseProvider(httpClient); + GitRepository repository = GitRepository.of("test", "repo"); + + GitRelease release = provider.getLatestRelease(repository); + + assertNotNull(release); + assertEquals("v1.0.0", release.getName()); + assertEquals("main", release.getBranch()); + assertEquals("v1.0.0", release.getTag().getTag()); + assertEquals("https://github.com/test/repo/releases/tag/v1.0.0", release.getPageUrl()); + } + + @Test + void testGetLatestRelease404Error() { + HttpClient httpClient = createMockClient(404, ""); + + GitHubReleaseProvider provider = new GitHubReleaseProvider(httpClient); + GitRepository repository = GitRepository.of("test", "repo"); + + GitException exception = assertThrows( + GitException.class, () -> + provider.getLatestRelease(repository)); + + assertTrue(exception.getMessage().contains("404")); + } + + private HttpClient createMockClient(int statusCode, String body) { + return new HttpClient() { + @Override + public Optional cookieHandler() {return Optional.empty();} + + @Override + public Redirect followRedirects() {return Redirect.NEVER;} + + @Override + public Optional proxy() {return Optional.empty();} + + @Override + public SSLContext sslContext() {return null;} + + @Override + public SSLParameters sslParameters() {return new SSLParameters();} + + @Override + public Optional authenticator() {return Optional.empty();} + + @Override + public Version version() {return Version.HTTP_1_1;} + + @Override + public Optional executor() {return Optional.empty();} + + @Override + public Optional connectTimeout() { + return Optional.of(Duration.ofSeconds(10)); + } + + @Override + public HttpResponse send(HttpRequest request, HttpResponse.BodyHandler responseBodyHandler) { + assertEquals("https://api.github.com/repos/test/repo/releases/latest", request.uri().toString()); + + @SuppressWarnings("unchecked") + T castedBody = (T) body; + + return new MockHttpResponse<>(request, statusCode, castedBody); + } + + @Override + public CompletableFuture> sendAsync( + HttpRequest request, + HttpResponse.BodyHandler responseBodyHandler) { + throw new UnsupportedOperationException("sendAsync not supported in tests"); + } + + @Override + public CompletableFuture> sendAsync( + HttpRequest request, + HttpResponse.BodyHandler responseBodyHandler, + HttpResponse.PushPromiseHandler pushPromiseHandler) { + throw new UnsupportedOperationException("sendAsync with push promises not supported in tests"); + } + }; + } + + private record MockHttpResponse(HttpRequest request, int statusCode, T body) implements HttpResponse { + + @Override + public int statusCode() {return statusCode;} + + @Override + public HttpRequest request() {return request;} + + @Override + public Optional> previousResponse() {return Optional.empty();} + + @Override + public HttpHeaders headers() {return HttpHeaders.of(Collections.emptyMap(), (a, b) -> true);} + + @Override + public T body() {return body;} + + @Override + public URI uri() {return request.uri();} + + @Override + public Version version() {return Version.HTTP_1_1;} + + @Override + public Optional sslSession() {return null;} + } +} diff --git a/src/test/java/com/eternalcode/gitcheck/github/GitHubVersionProviderTest.java b/src/test/java/com/eternalcode/gitcheck/github/GitHubVersionProviderTest.java deleted file mode 100644 index c69ebf1..0000000 --- a/src/test/java/com/eternalcode/gitcheck/github/GitHubVersionProviderTest.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.eternalcode.gitcheck.github; - -import com.eternalcode.gitcheck.git.GitException; -import com.eternalcode.gitcheck.git.GitRelease; -import com.eternalcode.gitcheck.git.GitRepository; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -class GitHubVersionProviderTest { - - @Test - void testGetLatestRelease() { - GitHubReleaseProvider provider = new GitHubReleaseProvider(); - GitRelease release = provider.getLatestRelease(GitRepository.of("EternalCodeTeam", "ChatFormatter")); - - assertNotNull(release); - assertNotNull(release.getName()); - assertNotNull(release.getBranch()); - assertNotNull(release.getTag()); - assertNotNull(release.getPageUrl()); - assertNotNull(release.getPublishedAt()); - } - - @Test - void testGetLatestReleaseWithNonExistingRepository() { - this.assertThrowsReleaseFor(GitRepository.of("EternalCodeTeam", "-")); - } - - @Test - void testGetLatestReleaseWithNotReleasedRepository() { - this.assertThrowsReleaseFor(GitRepository.of("EternalCodeTeam", "EternalCore")); - } - - @Test - void testGetLatestReleaseWithInvalidRepository() { - this.assertThrowsReleaseFor(GitRepository.of(".", ".")); - } - - private void assertThrowsReleaseFor(GitRepository repository) { - GitHubReleaseProvider provider = new GitHubReleaseProvider(); - - assertThrows(GitException.class, () -> provider.getLatestRelease(repository)); - } - -} \ No newline at end of file diff --git a/src/test/java/com/eternalcode/gitcheck/github/JSONUtilTest.java b/src/test/java/com/eternalcode/gitcheck/github/JSONUtilTest.java index 0003f31..744c62d 100644 --- a/src/test/java/com/eternalcode/gitcheck/github/JSONUtilTest.java +++ b/src/test/java/com/eternalcode/gitcheck/github/JSONUtilTest.java @@ -32,7 +32,7 @@ void testAsStringWithInvalidKey() { JSONObject jsonObject = new JSONObject(); jsonObject.put("text", "test"); - assertThrows(NoSuchElementException.class, () -> JSONUtil.asString(jsonObject, "time")); + assertThrows(IllegalArgumentException.class, () -> JSONUtil.asString(jsonObject, "time")); } @Test diff --git a/src/test/java/com/eternalcode/gitcheck/mock/MockErrorGitReleaseProvider.java b/src/test/java/com/eternalcode/gitcheck/mock/MockErrorGitReleaseProvider.java new file mode 100644 index 0000000..a58f0eb --- /dev/null +++ b/src/test/java/com/eternalcode/gitcheck/mock/MockErrorGitReleaseProvider.java @@ -0,0 +1,14 @@ +package com.eternalcode.gitcheck.mock; + +import com.eternalcode.gitcheck.git.GitException; +import com.eternalcode.gitcheck.git.GitRelease; +import com.eternalcode.gitcheck.git.GitReleaseProvider; +import com.eternalcode.gitcheck.git.GitRepository; + +public class MockErrorGitReleaseProvider implements GitReleaseProvider { + + @Override + public GitRelease getLatestRelease(GitRepository repository) { + throw new GitException("Repository or release not found: " + repository.getFullName() + " (404)"); + } +} \ No newline at end of file diff --git a/src/test/java/com/eternalcode/gitcheck/mock/MockGitReleaseProvider.java b/src/test/java/com/eternalcode/gitcheck/mock/MockGitReleaseProvider.java index f411df6..78ff145 100644 --- a/src/test/java/com/eternalcode/gitcheck/mock/MockGitReleaseProvider.java +++ b/src/test/java/com/eternalcode/gitcheck/mock/MockGitReleaseProvider.java @@ -19,5 +19,4 @@ public GitRelease getLatestRelease(GitRepository repository) { .publishedAt(Instant.parse("2020-01-01T00:00:00Z")) .build(); } - } From 7e439ae341bff3731f17a42e2aadecf7674be5d8 Mon Sep 17 00:00:00 2001 From: vLuckyyy Date: Tue, 8 Jul 2025 01:44:38 +0200 Subject: [PATCH 2/3] Update repository credentials environment variable names --- build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 607c041..14ceb7a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -41,8 +41,8 @@ publishing { } credentials { - username = System.getenv("ETERNAL_CODE_MAVEN_USERNAME") - password = System.getenv("ETERNAL_CODE_MAVEN_PASSWORD") + username = System.getenv("ETERNALCODE_REPO_USERNAME") + password = System.getenv("ETERNALCODE_REPO_PASSWORD") } } } From 7aefc474772611c285f4aab25a01ff1d58f5c85c Mon Sep 17 00:00:00 2001 From: vLuckyyy Date: Tue, 8 Jul 2025 01:46:27 +0200 Subject: [PATCH 3/3] Fix workflow. --- .github/workflows/build.yml | 62 +++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0dd1021..c262bf1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,34 +1,36 @@ -name: Java CI and Test with Gradle +name: Java CI with Gradle on: - push: - branches: [ master ] - pull_request: - branches: [ master ] + push: + branches: + - master + pull_request: jobs: - build: - name: "Build with JDK${{ matrix.jdk }}" - runs-on: ubuntu-latest - strategy: - matrix: - jdk: [ 9, 11, 17 ] - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Set up JDK ${{ matrix.jdk }} - uses: actions/setup-java@v3 - with: - java-version: ${{ matrix.jdk }} - distribution: 'adopt' - cache: gradle - - - name: Grant execute permission for gradlew - run: chmod +x ./gradlew - - - name: Build with Gradle - run: ./gradlew clean build - - - name: Test with Gradle - run: ./gradlew clean test + build: + runs-on: ubuntu-latest + strategy: + matrix: + java: + - 17 + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: 'Set up JDK ${{ matrix.java }}' + uses: actions/setup-java@v4 + with: + distribution: adopt + java-version: '${{ matrix.java }}' + - name: Cache Gradle + uses: actions/cache@v4 + with: + path: ~/.gradle/caches + key: >- + ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', + '**/gradle-wrapper.properties') }} + restore-keys: '${{ runner.os }}-gradle-' + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build the Jar + run: './gradlew clean test build' \ No newline at end of file