diff --git a/.github/workflows/build-external.yml b/.github/workflows/build-external.yml index 54e27864..ef480f90 100644 --- a/.github/workflows/build-external.yml +++ b/.github/workflows/build-external.yml @@ -26,3 +26,7 @@ jobs: ${{ runner.os }}- - name: Run maven build run: mvn install -PprettierCheck -Dprettier.nodePath=node -Dprettier.npmPath=npm + - name: codecov + uses: codecov/codecov-action@v5 + with: + files: ./**/target/site/jacoco/jacoco.xml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 06207efb..f5de419f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,7 +31,6 @@ jobs: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} ${{ runner.os }}-maven- ${{ runner.os }}- - - name: Run maven build run: mvn verify -s .github/workflows/settings.xml -PprettierCheck -Dprettier.nodePath=node -Dprettier.npmPath=npm - name: Sonar Scan @@ -47,6 +46,10 @@ jobs: -Dsonar.projectName=${SONAR_PROJECT_NAME} \ -Dsonar.host.url=https://sonarcloud.io \ -Dsonar.token=${SONAR_TOKEN} + - name: codecov + uses: codecov/codecov-action@v5 + with: + files: ./**/target/site/jacoco/jacoco.xml - name: Upload artifact uses: actions/upload-artifact@v4.6.2 with: diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 808241ab..fb3d6922 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -56,6 +56,10 @@ jobs: -Dsonar.projectName=${SONAR_PROJECT_NAME} \ -Dsonar.host.url=https://sonarcloud.io \ -Dsonar.token=${SONAR_TOKEN} + - name: codecov + uses: codecov/codecov-action@v5 + with: + files: ./**/target/site/jacoco/jacoco.xml - name: Upload artifact uses: actions/upload-artifact@v4.6.2 with: diff --git a/gbfs-validator-java-cli/pom.xml b/gbfs-validator-java-cli/pom.xml new file mode 100644 index 00000000..915be9d9 --- /dev/null +++ b/gbfs-validator-java-cli/pom.xml @@ -0,0 +1,187 @@ + + + 4.0.0 + + org.entur.gbfs + gbfs-validator-java-parent + 2.0.57-SNAPSHOT + + + gbfs-validator-java-cli + jar + GBFS Validator CLI + Command-line interface for GBFS validation + https://github.com/entur/gbfs-validator-java + + + 17 + 17 + UTF-8 + 4.7.7 + 2.15.2 + 5.10.2 + 0.8.13 + + + + + + + + org.entur.gbfs + gbfs-validator-java + ${project.version} + + + org.entur.gbfs + gbfs-validator-java-loader + ${project.version} + + + + + info.picocli + picocli + ${picocli.version} + + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + + + org.slf4j + slf4j-simple + 2.0.17 + + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + org.mockito + mockito-junit-jupiter + 5.11.0 + test + + + com.github.tomakehurst + wiremock-jre8 + 2.35.1 + test + + + + + + + src/main/resources + true + + + + + + maven-compiler-plugin + 3.14.1 + + 17 + 17 + UTF-8 + + + info.picocli + picocli-codegen + ${picocli.version} + + + + -Aproject=${project.groupId}/${project.artifactId} + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.1 + + + package + + shade + + + + + org.entur.gbfs.validator.cli.GbfsValidatorCli + + + gbfs-validator-cli + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + + default-prepare-agent + + prepare-agent + + + + default-report + prepare-package + + report + + + + default-check + + check + + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0 + + + + diff --git a/gbfs-validator-java-cli/src/main/java/org/entur/gbfs/validator/cli/AuthOptions.java b/gbfs-validator-java-cli/src/main/java/org/entur/gbfs/validator/cli/AuthOptions.java new file mode 100644 index 00000000..0d52137d --- /dev/null +++ b/gbfs-validator-java-cli/src/main/java/org/entur/gbfs/validator/cli/AuthOptions.java @@ -0,0 +1,73 @@ +package org.entur.gbfs.validator.cli; + +import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Option; + +public class AuthOptions { + + @Option( + names = { "--auth-type" }, + description = "Authentication type: basic, bearer, or oauth" + ) + AuthType authType; + + @ArgGroup(exclusive = false) + BasicAuthOptions basicAuth; + + @ArgGroup(exclusive = false) + BearerAuthOptions bearerAuth; + + @ArgGroup(exclusive = false) + OAuthOptions oauthOptions; + + public static class BasicAuthOptions { + + @Option( + names = { "--username" }, + description = "Username for Basic Authentication", + required = true + ) + String username; + + @Option( + names = { "--password" }, + description = "Password for Basic Authentication", + required = true + ) + String password; + } + + public static class BearerAuthOptions { + + @Option( + names = { "--token" }, + description = "Bearer token", + required = true + ) + String token; + } + + public static class OAuthOptions { + + @Option( + names = { "--client-id" }, + description = "OAuth client ID", + required = true + ) + String clientId; + + @Option( + names = { "--client-secret" }, + description = "OAuth client secret", + required = true + ) + String clientSecret; + + @Option( + names = { "--token-url" }, + description = "OAuth token endpoint URL", + required = true + ) + String tokenUrl; + } +} diff --git a/gbfs-validator-java-cli/src/main/java/org/entur/gbfs/validator/cli/AuthType.java b/gbfs-validator-java-cli/src/main/java/org/entur/gbfs/validator/cli/AuthType.java new file mode 100644 index 00000000..d514728d --- /dev/null +++ b/gbfs-validator-java-cli/src/main/java/org/entur/gbfs/validator/cli/AuthType.java @@ -0,0 +1,7 @@ +package org.entur.gbfs.validator.cli; + +public enum AuthType { + BASIC, + BEARER, + OAUTH, +} diff --git a/gbfs-validator-java-cli/src/main/java/org/entur/gbfs/validator/cli/AuthenticationHandler.java b/gbfs-validator-java-cli/src/main/java/org/entur/gbfs/validator/cli/AuthenticationHandler.java new file mode 100644 index 00000000..88611011 --- /dev/null +++ b/gbfs-validator-java-cli/src/main/java/org/entur/gbfs/validator/cli/AuthenticationHandler.java @@ -0,0 +1,49 @@ +package org.entur.gbfs.validator.cli; + +import org.entur.gbfs.validator.loader.auth.Authentication; +import org.entur.gbfs.validator.loader.auth.BasicAuth; +import org.entur.gbfs.validator.loader.auth.BearerTokenAuth; +import org.entur.gbfs.validator.loader.auth.OAuthClientCredentialsGrantAuth; + +public class AuthenticationHandler { + + public static Authentication buildAuthentication(AuthOptions authOptions) { + if (authOptions == null || authOptions.authType == null) { + return null; + } + + return switch (authOptions.authType) { + case BASIC -> { + if (authOptions.basicAuth == null) { + throw new IllegalArgumentException( + "Basic auth selected but --username and --password not provided" + ); + } + yield new BasicAuth( + authOptions.basicAuth.username, + authOptions.basicAuth.password + ); + } + case BEARER -> { + if (authOptions.bearerAuth == null) { + throw new IllegalArgumentException( + "Bearer auth selected but --token not provided" + ); + } + yield new BearerTokenAuth(authOptions.bearerAuth.token); + } + case OAUTH -> { + if (authOptions.oauthOptions == null) { + throw new IllegalArgumentException( + "OAuth auth selected but credentials not provided" + ); + } + yield new OAuthClientCredentialsGrantAuth( + authOptions.oauthOptions.clientId, + authOptions.oauthOptions.clientSecret, + authOptions.oauthOptions.tokenUrl + ); + } + }; + } +} diff --git a/gbfs-validator-java-cli/src/main/java/org/entur/gbfs/validator/cli/GbfsValidatorCli.java b/gbfs-validator-java-cli/src/main/java/org/entur/gbfs/validator/cli/GbfsValidatorCli.java new file mode 100644 index 00000000..9acf89c5 --- /dev/null +++ b/gbfs-validator-java-cli/src/main/java/org/entur/gbfs/validator/cli/GbfsValidatorCli.java @@ -0,0 +1,180 @@ +package org.entur.gbfs.validator.cli; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import org.entur.gbfs.validation.GbfsValidator; +import org.entur.gbfs.validation.GbfsValidatorFactory; +import org.entur.gbfs.validation.model.ValidationResult; +import org.entur.gbfs.validator.cli.formatter.ConsoleReportFormatter; +import org.entur.gbfs.validator.cli.formatter.JsonReportFormatter; +import org.entur.gbfs.validator.cli.formatter.ReportFormatter; +import org.entur.gbfs.validator.loader.LoadedFile; +import org.entur.gbfs.validator.loader.Loader; +import org.entur.gbfs.validator.loader.auth.Authentication; +import picocli.CommandLine; +import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + +@Command( + name = "gbfs-validator", + mixinStandardHelpOptions = true, + versionProvider = VersionProvider.class, + description = "Validate GBFS feeds against JSON schemas", + headerHeading = "Usage:%n%n", + synopsisHeading = "%n", + descriptionHeading = "%nDescription:%n%n", + parameterListHeading = "%nParameters:%n", + optionListHeading = "%nOptions:%n" +) +public class GbfsValidatorCli implements Callable { + + @Option( + names = { "-u", "--url" }, + description = "URL of the GBFS feed (discovery endpoint)", + required = true + ) + private String feedUrl; + + @Option( + names = { "-s", "--save-report" }, + description = "Local path to output report file" + ) + private File reportFile; + + @Option( + names = { "-pr", "--print-report" }, + description = "Print report to standard output (yes/no, default: yes)", + defaultValue = "yes" + ) + private String printReport; + + @Option( + names = { "-vb", "--verbose" }, + description = "Verbose mode - show detailed error information" + ) + private boolean verbose; + + @Option( + names = { "--format" }, + description = "Output format: text or json (default: text)", + defaultValue = "text" + ) + private String format; + + @ArgGroup(exclusive = false, heading = "%nAuthentication Options:%n") + private AuthOptions authOptions; + + @Override + public Integer call() throws Exception { + Loader loader = null; + + try { + Authentication auth = AuthenticationHandler.buildAuthentication( + authOptions + ); + + loader = new Loader(); + List loadedFiles = loader.load(feedUrl, auth); + + boolean hasFatalLoaderErrors = hasFatalLoaderErrors(loadedFiles); + if (hasFatalLoaderErrors && hasNoValidContent(loadedFiles)) { + System.err.println("ERROR: Failed to load any feeds from " + feedUrl); + return 2; + } + + Map fileMap = buildFileMap(loadedFiles); + + GbfsValidator validator = GbfsValidatorFactory.getGbfsJsonValidator(); + ValidationResult result = validator.validate(fileMap); + + String report = formatReport(result, loadedFiles); + outputReport(report); + + return determineExitCode(hasFatalLoaderErrors, result); + } catch (Exception e) { + System.err.println("ERROR: " + e.getMessage()); + if (verbose) { + e.printStackTrace(System.err); + } + return 2; + } finally { + if (loader != null) { + loader.close(); + } + } + } + + private boolean hasFatalLoaderErrors(List loadedFiles) { + return loadedFiles + .stream() + .anyMatch(file -> + file.loaderErrors() != null && !file.loaderErrors().isEmpty() + ); + } + + private boolean hasNoValidContent(List loadedFiles) { + return loadedFiles.stream().noneMatch(file -> file.fileContents() != null); + } + + private Map buildFileMap(List loadedFiles) { + Map fileMap = new HashMap<>(); + for (LoadedFile file : loadedFiles) { + if (file.fileContents() != null) { + fileMap.put(file.fileName(), file.fileContents()); + } + } + return fileMap; + } + + private String formatReport( + ValidationResult result, + List loadedFiles + ) { + ReportFormatter formatter = createFormatter(format); + return formatter.format(result, loadedFiles, verbose); + } + + private void outputReport(String report) throws IOException { + if ("yes".equalsIgnoreCase(printReport)) { + System.out.println(report); + } + + if (reportFile != null) { + ReportWriter.writeReport(reportFile, report); + if (!"yes".equalsIgnoreCase(printReport)) { + System.out.println("Report saved to: " + reportFile.getAbsolutePath()); + } + } + } + + private int determineExitCode( + boolean hasFatalLoaderErrors, + ValidationResult result + ) { + if (hasFatalLoaderErrors) { + return 2; + } else if (result.summary().errorsCount() > 0) { + return 1; + } else { + return 0; + } + } + + private ReportFormatter createFormatter(String format) { + return switch (format.toLowerCase()) { + case "json" -> new JsonReportFormatter(); + default -> new ConsoleReportFormatter(); + }; + } + + public static void main(String[] args) { + int exitCode = new CommandLine(new GbfsValidatorCli()).execute(args); + System.exit(exitCode); + } +} diff --git a/gbfs-validator-java-cli/src/main/java/org/entur/gbfs/validator/cli/ReportWriter.java b/gbfs-validator-java-cli/src/main/java/org/entur/gbfs/validator/cli/ReportWriter.java new file mode 100644 index 00000000..83cda9a2 --- /dev/null +++ b/gbfs-validator-java-cli/src/main/java/org/entur/gbfs/validator/cli/ReportWriter.java @@ -0,0 +1,18 @@ +package org.entur.gbfs.validator.cli; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; + +public class ReportWriter { + + public static void writeReport(File file, String content) throws IOException { + Files.writeString( + file.toPath(), + content, + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING + ); + } +} diff --git a/gbfs-validator-java-cli/src/main/java/org/entur/gbfs/validator/cli/VersionProvider.java b/gbfs-validator-java-cli/src/main/java/org/entur/gbfs/validator/cli/VersionProvider.java new file mode 100644 index 00000000..8f7505f9 --- /dev/null +++ b/gbfs-validator-java-cli/src/main/java/org/entur/gbfs/validator/cli/VersionProvider.java @@ -0,0 +1,28 @@ +package org.entur.gbfs.validator.cli; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; +import picocli.CommandLine; + +public class VersionProvider implements CommandLine.IVersionProvider { + + @Override + public String[] getVersion() throws Exception { + Properties properties = new Properties(); + try ( + InputStream input = getClass() + .getClassLoader() + .getResourceAsStream("version.properties") + ) { + if (input == null) { + return new String[] { "Unknown version" }; + } + properties.load(input); + String version = properties.getProperty("version", "Unknown version"); + return new String[] { version }; + } catch (IOException e) { + return new String[] { "Unknown version" }; + } + } +} diff --git a/gbfs-validator-java-cli/src/main/java/org/entur/gbfs/validator/cli/formatter/ConsoleReportFormatter.java b/gbfs-validator-java-cli/src/main/java/org/entur/gbfs/validator/cli/formatter/ConsoleReportFormatter.java new file mode 100644 index 00000000..d8315887 --- /dev/null +++ b/gbfs-validator-java-cli/src/main/java/org/entur/gbfs/validator/cli/formatter/ConsoleReportFormatter.java @@ -0,0 +1,147 @@ +package org.entur.gbfs.validator.cli.formatter; + +import java.util.List; +import org.entur.gbfs.validation.model.FileValidationError; +import org.entur.gbfs.validation.model.FileValidationResult; +import org.entur.gbfs.validation.model.ValidationResult; +import org.entur.gbfs.validation.model.ValidatorError; +import org.entur.gbfs.validator.loader.LoadedFile; +import org.entur.gbfs.validator.loader.LoaderError; + +public class ConsoleReportFormatter implements ReportFormatter { + + private static final String HEADER = "=" + "=".repeat(79); + private static final String SUBHEADER = "-" + "-".repeat(79); + + @Override + public String format( + ValidationResult result, + List loadedFiles, + boolean verbose + ) { + StringBuilder sb = new StringBuilder(); + + // Header + sb.append(HEADER).append("\n"); + sb.append("GBFS Validation Report\n"); + sb.append(HEADER).append("\n\n"); + + // Summary + sb.append("Version: ").append(result.summary().version()).append("\n"); + sb.append("Timestamp: ").append(result.summary().timestamp()).append("\n"); + sb + .append("Total Errors: ") + .append(result.summary().errorsCount()) + .append("\n"); + sb.append("Files Validated: ").append(result.files().size()).append("\n\n"); + + // Loader Errors (if any) + boolean hasLoaderErrors = loadedFiles + .stream() + .anyMatch(file -> + file.loaderErrors() != null && !file.loaderErrors().isEmpty() + ); + + if (hasLoaderErrors) { + sb.append(SUBHEADER).append("\n"); + sb.append("LOADER ERRORS\n"); + sb.append(SUBHEADER).append("\n\n"); + + for (LoadedFile file : loadedFiles) { + if (file.loaderErrors() != null && !file.loaderErrors().isEmpty()) { + sb.append("File: ").append(file.fileName()).append("\n"); + for (LoaderError error : file.loaderErrors()) { + sb + .append(" ✗ ") + .append(error.error()) + .append(": ") + .append(error.message()) + .append("\n"); + } + sb.append("\n"); + } + } + } + + // File Results + sb.append(SUBHEADER).append("\n"); + sb.append("VALIDATION RESULTS\n"); + sb.append(SUBHEADER).append("\n\n"); + + for (var entry : result.files().entrySet()) { + String fileName = entry.getKey(); + FileValidationResult fileResult = entry.getValue(); + + if (fileResult == null) { + continue; + } + + // File status + String status = getFileStatus(fileResult); + sb.append(status).append(" ").append(fileName); + + if (fileResult.required()) { + sb.append(" [REQUIRED]"); + } + + if (!fileResult.exists()) { + sb.append(" - NOT FOUND"); + } else if (fileResult.errorsCount() > 0) { + sb.append(" - ").append(fileResult.errorsCount()).append(" error(s)"); + } + + sb.append("\n"); + + // Validator errors (system errors) + if ( + fileResult.validatorErrors() != null && + !fileResult.validatorErrors().isEmpty() + ) { + for (ValidatorError error : fileResult.validatorErrors()) { + sb.append(" ✗ SYSTEM ERROR: ").append(error.message()).append("\n"); + } + } + + // Validation errors (schema violations) + if ( + verbose && fileResult.errors() != null && !fileResult.errors().isEmpty() + ) { + for (FileValidationError error : fileResult.errors()) { + sb.append(" ✗ ").append(error.message()).append("\n"); + sb.append(" Path: ").append(error.violationPath()).append("\n"); + sb.append(" Schema: ").append(error.schemaPath()).append("\n"); + } + } else if (!verbose && fileResult.errorsCount() > 0) { + sb.append(" (Use --verbose to see detailed errors)\n"); + } + + sb.append("\n"); + } + + // Footer + sb.append(HEADER).append("\n"); + String resultText = result.summary().errorsCount() == 0 + ? "VALID" + : "INVALID"; + sb.append("Result: ").append(resultText).append("\n"); + sb.append(HEADER).append("\n"); + + return sb.toString(); + } + + private String getFileStatus(FileValidationResult fileResult) { + if (!fileResult.exists()) { + return "⚠"; + } else if ( + fileResult.errorsCount() == 0 && + ( + fileResult.validatorErrors() == null || + fileResult.validatorErrors().isEmpty() + ) + ) { + return "✓"; + } else { + return "✗"; + } + } +} diff --git a/gbfs-validator-java-cli/src/main/java/org/entur/gbfs/validator/cli/formatter/JsonReportFormatter.java b/gbfs-validator-java-cli/src/main/java/org/entur/gbfs/validator/cli/formatter/JsonReportFormatter.java new file mode 100644 index 00000000..f959cf15 --- /dev/null +++ b/gbfs-validator-java-cli/src/main/java/org/entur/gbfs/validator/cli/formatter/JsonReportFormatter.java @@ -0,0 +1,66 @@ +package org.entur.gbfs.validator.cli.formatter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.entur.gbfs.validation.model.ValidationResult; +import org.entur.gbfs.validator.loader.LoadedFile; + +public class JsonReportFormatter implements ReportFormatter { + + private final ObjectMapper objectMapper; + + public JsonReportFormatter() { + this.objectMapper = new ObjectMapper(); + this.objectMapper.enable(SerializationFeature.INDENT_OUTPUT); + } + + @Override + public String format( + ValidationResult result, + List loadedFiles, + boolean verbose + ) { + try { + Map report = new HashMap<>(); + + // Validation result + report.put("summary", result.summary()); + report.put("files", result.files()); + + // Loader errors + List> loaderErrors = loadedFiles + .stream() + .filter(file -> + file.loaderErrors() != null && !file.loaderErrors().isEmpty() + ) + .flatMap(file -> + file + .loaderErrors() + .stream() + .map(error -> { + Map errorMap = new HashMap<>(); + errorMap.put("fileName", file.fileName()); + errorMap.put("error", error.error()); + errorMap.put("message", error.message()); + return errorMap; + }) + ) + .collect(Collectors.toList()); + + if (!loaderErrors.isEmpty()) { + report.put("loaderErrors", loaderErrors); + } + + return objectMapper.writeValueAsString(report); + } catch (Exception e) { + throw new RuntimeException( + "Failed to serialize validation result to JSON", + e + ); + } + } +} diff --git a/gbfs-validator-java-cli/src/main/java/org/entur/gbfs/validator/cli/formatter/ReportFormatter.java b/gbfs-validator-java-cli/src/main/java/org/entur/gbfs/validator/cli/formatter/ReportFormatter.java new file mode 100644 index 00000000..eab7ff5b --- /dev/null +++ b/gbfs-validator-java-cli/src/main/java/org/entur/gbfs/validator/cli/formatter/ReportFormatter.java @@ -0,0 +1,20 @@ +package org.entur.gbfs.validator.cli.formatter; + +import java.util.List; +import org.entur.gbfs.validation.model.ValidationResult; +import org.entur.gbfs.validator.loader.LoadedFile; + +public interface ReportFormatter { + /** + * Format validation results into a report string + * @param result Validation result from GbfsValidator + * @param loadedFiles List of loaded files (to report loader errors) + * @param verbose Whether to include detailed error information + * @return Formatted report string + */ + String format( + ValidationResult result, + List loadedFiles, + boolean verbose + ); +} diff --git a/gbfs-validator-java-cli/src/main/resources/simplelogger.properties b/gbfs-validator-java-cli/src/main/resources/simplelogger.properties new file mode 100644 index 00000000..3f399f18 --- /dev/null +++ b/gbfs-validator-java-cli/src/main/resources/simplelogger.properties @@ -0,0 +1,7 @@ +# SLF4J SimpleLogger configuration for CLI +org.slf4j.simpleLogger.defaultLogLevel=warn +org.slf4j.simpleLogger.log.org.entur.gbfs=info +org.slf4j.simpleLogger.showDateTime=false +org.slf4j.simpleLogger.showThreadName=false +org.slf4j.simpleLogger.showLogName=false +org.slf4j.simpleLogger.levelInBrackets=true diff --git a/gbfs-validator-java-cli/src/main/resources/version.properties b/gbfs-validator-java-cli/src/main/resources/version.properties new file mode 100644 index 00000000..defbd482 --- /dev/null +++ b/gbfs-validator-java-cli/src/main/resources/version.properties @@ -0,0 +1 @@ +version=${project.version} diff --git a/gbfs-validator-java-cli/src/test/java/org/entur/gbfs/validator/cli/GbfsValidatorCliIntegrationTest.java b/gbfs-validator-java-cli/src/test/java/org/entur/gbfs/validator/cli/GbfsValidatorCliIntegrationTest.java new file mode 100644 index 00000000..38750353 --- /dev/null +++ b/gbfs-validator-java-cli/src/test/java/org/entur/gbfs/validator/cli/GbfsValidatorCliIntegrationTest.java @@ -0,0 +1,227 @@ +package org.entur.gbfs.validator.cli; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import picocli.CommandLine; + +public class GbfsValidatorCliIntegrationTest { + + private static Path testFeedDir; + + /** + * Helper class to capture System.out/err and execute CLI commands + */ + static class CliResult { + + final int exitCode; + final String output; + + CliResult(int exitCode, String output) { + this.exitCode = exitCode; + this.output = output; + } + } + + /** + * Execute CLI command and capture output + */ + static CliResult execute(String... args) { + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + ByteArrayOutputStream errContent = new ByteArrayOutputStream(); + PrintStream originalOut = System.out; + PrintStream originalErr = System.err; + + try { + System.setOut(new PrintStream(outContent)); + System.setErr(new PrintStream(errContent)); + + CommandLine cmd = new CommandLine(new GbfsValidatorCli()); + int exitCode = cmd.execute(args); + String output = outContent.toString() + errContent.toString(); + + return new CliResult(exitCode, output); + } finally { + System.setOut(originalOut); + System.setErr(originalErr); + } + } + + @BeforeAll + static void setup(@TempDir Path tempDir) throws Exception { + // Create test feed files with correct file:// URLs + testFeedDir = tempDir.resolve("test-feeds"); + Files.createDirectories(testFeedDir); + + // Create system_information.json + String systemInfo = + """ + { + "last_updated": 1609459200, + "ttl": 0, + "version": "2.2", + "data": { + "system_id": "test_system", + "language": "en", + "name": "Test Bike Share", + "timezone": "America/New_York" + } + } + """; + Files.writeString( + testFeedDir.resolve("system_information.json"), + systemInfo + ); + + // Create gbfs.json with file:// URL pointing to system_information.json + String gbfsJson = String.format( + """ + { + "last_updated": 1609459200, + "ttl": 0, + "version": "2.2", + "data": { + "en": { + "feeds": [ + { + "name": "system_information", + "url": "file://%s" + } + ] + } + } + } + """, + testFeedDir.resolve("system_information.json").toAbsolutePath() + ); + Files.writeString(testFeedDir.resolve("gbfs.json"), gbfsJson); + } + + @Test + void testCli_ValidLocalFeed_Success() { + CliResult result = execute( + "-u", + "file://" + testFeedDir.resolve("gbfs.json").toAbsolutePath() + ); + + assertTrue( + result.exitCode == 0 || result.exitCode == 1, + "Expected success or validation error, got: " + + result.exitCode + + "\nOutput: " + + result.output + ); + assertTrue(result.output.contains("GBFS Validation Report")); + } + + @Test + void testCli_ValidLocalFeed_JsonFormat() { + CliResult result = execute( + "-u", + "file://" + testFeedDir.resolve("gbfs.json").toAbsolutePath(), + "--format", + "json" + ); + + assertTrue( + result.exitCode == 0 || result.exitCode == 1, + "Expected success or validation error, got: " + result.exitCode + ); + assertTrue(result.output.contains("\"summary\"")); + assertTrue(result.output.contains("\"files\"")); + } + + @Test + void testCli_SaveReport_CreatesFile(@TempDir Path tempDir) { + Path reportFile = tempDir.resolve("report.txt"); + + CliResult result = execute( + "-u", + "file://" + testFeedDir.resolve("gbfs.json").toAbsolutePath(), + "-s", + reportFile.toString(), + "-pr", + "no" + ); + + assertTrue( + result.exitCode == 0 || result.exitCode == 1, + "Expected success or validation error, got: " + result.exitCode + ); + assertTrue(Files.exists(reportFile), "Report file should exist"); + assertTrue( + reportFile.toFile().length() > 0, + "Report file should not be empty" + ); + assertTrue(result.output.contains("Report saved to:")); + } + + @Test + void testCli_VerboseFlag() { + CliResult result = execute( + "-u", + "file://" + testFeedDir.resolve("gbfs.json").toAbsolutePath(), + "--verbose" + ); + + assertTrue( + result.exitCode == 0 || result.exitCode == 1, + "Expected success or validation error, got: " + result.exitCode + ); + } + + @Test + void testCli_MissingUrl_ShowsError() { + CliResult result = execute(); + + assertNotEquals(0, result.exitCode, "Should fail when URL is missing"); + assertTrue( + result.output.contains("Missing required option") || + result.output.contains("--url"), + "Should mention missing URL option" + ); + } + + @Test + void testCli_InvalidUrl_SystemError() { + CliResult result = execute( + "-u", + "https://invalid.example.com/nonexistent.json" + ); + + assertEquals(2, result.exitCode, "Should return system error code"); + assertTrue( + result.output.contains("ERROR") || + result.output.contains("Failed to load"), + "Should show error message" + ); + } + + @Test + void testCli_HelpOption_Success() { + CliResult result = execute("--help"); + + assertEquals(0, result.exitCode, "Help should return success"); + assertTrue(result.output.contains("Usage:")); + assertTrue(result.output.contains("--url")); + } + + @Test + void testCli_VersionOption_Success() { + CliResult result = execute("--version"); + + assertEquals(0, result.exitCode, "Version should return success"); + // Just verify it outputs something (version format may vary) + assertFalse(result.output.trim().isEmpty(), "Should output version info"); + assertTrue( + result.output.contains(".") || result.output.matches(".*\\d+.*"), + "Should contain version-like content" + ); + } +} diff --git a/gbfs-validator-java-cli/src/test/resources/feeds/invalid/gbfs.json b/gbfs-validator-java-cli/src/test/resources/feeds/invalid/gbfs.json new file mode 100644 index 00000000..c63050e3 --- /dev/null +++ b/gbfs-validator-java-cli/src/test/resources/feeds/invalid/gbfs.json @@ -0,0 +1,9 @@ +{ + "last_updated": 1609459200, + "ttl": 0, + "data": { + "en": { + "feeds": [] + } + } +} diff --git a/gbfs-validator-java-cli/src/test/resources/feeds/valid/gbfs.json b/gbfs-validator-java-cli/src/test/resources/feeds/valid/gbfs.json new file mode 100644 index 00000000..ef11796d --- /dev/null +++ b/gbfs-validator-java-cli/src/test/resources/feeds/valid/gbfs.json @@ -0,0 +1,15 @@ +{ + "last_updated": 1609459200, + "ttl": 0, + "version": "2.2", + "data": { + "en": { + "feeds": [ + { + "name": "system_information", + "url": "file:///${project.basedir}/src/test/resources/feeds/valid/system_information.json" + } + ] + } + } +} diff --git a/gbfs-validator-java-cli/src/test/resources/feeds/valid/system_information.json b/gbfs-validator-java-cli/src/test/resources/feeds/valid/system_information.json new file mode 100644 index 00000000..775d0222 --- /dev/null +++ b/gbfs-validator-java-cli/src/test/resources/feeds/valid/system_information.json @@ -0,0 +1,11 @@ +{ + "last_updated": 1609459200, + "ttl": 0, + "version": "2.2", + "data": { + "system_id": "test_system", + "language": "en", + "name": "Test Bike Share", + "timezone": "America/New_York" + } +} diff --git a/pom.xml b/pom.xml index 58d0d443..01055070 100644 --- a/pom.xml +++ b/pom.xml @@ -26,6 +26,7 @@ gbfs-validator-java gbfs-validator-java-loader gbfs-validator-java-api + gbfs-validator-java-cli