diff --git a/argus-spring-boot-starter/build.gradle.kts b/argus-spring-boot-starter/build.gradle.kts index c793aec..577cdd6 100644 --- a/argus-spring-boot-starter/build.gradle.kts +++ b/argus-spring-boot-starter/build.gradle.kts @@ -16,6 +16,10 @@ dependencies { annotationProcessor("org.springframework.boot:spring-boot-configuration-processor:3.2.0") testImplementation("org.junit.jupiter:junit-jupiter:${property("junitVersion")}") + testImplementation("org.springframework.boot:spring-boot-test:3.2.0") + testImplementation("org.springframework.boot:spring-boot-test-autoconfigure:3.2.0") + testImplementation("org.springframework:spring-test:6.1.0") + testImplementation("org.assertj:assertj-core:3.25.1") } tasks.withType { diff --git a/argus-spring-boot-starter/src/test/java/io/argus/spring/ArgusAutoConfigurationIntegrationTest.java b/argus-spring-boot-starter/src/test/java/io/argus/spring/ArgusAutoConfigurationIntegrationTest.java new file mode 100644 index 0000000..147162f --- /dev/null +++ b/argus-spring-boot-starter/src/test/java/io/argus/spring/ArgusAutoConfigurationIntegrationTest.java @@ -0,0 +1,83 @@ +package io.argus.spring; + +import io.argus.core.config.AgentConfig; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Real Spring context integration tests for the Argus Spring Boot starter. + * + *

Verifies that the auto-configuration's conditional wiring and the + * {@link ArgusProperties} binding mechanism actually work end-to-end through + * Spring, rather than just exercising the static property-to-config mapping + * (which is what {@link ArgusAutoConfigurationTest} does). + * + *

To avoid the side effects of {@code ArgusAutoConfiguration} (JFR streaming + * engine startup and {@code ArgusServer} thread spawn), the property-binding + * test uses a minimal {@code @EnableConfigurationProperties} configuration + * rather than loading the full auto-configuration. + */ +class ArgusAutoConfigurationIntegrationTest { + + @Test + void argusPropertiesBindFromSpringPropertySources() { + new ApplicationContextRunner() + .withUserConfiguration(EnablePropsOnlyConfig.class) + .withPropertyValues( + "argus.buffer-size=131072", + "argus.server.port=9999", + "argus.server.enabled=false", + "argus.gc.enabled=true", + "argus.profiling.enabled=true", + "argus.profiling.interval-ms=10", + "argus.contention.threshold-ms=25", + "argus.metrics.prometheus.enabled=true" + ) + .run(context -> { + assertThat(context).hasSingleBean(ArgusProperties.class); + ArgusProperties props = context.getBean(ArgusProperties.class); + + assertThat(props.getBufferSize()).isEqualTo(131072); + assertThat(props.getServer().getPort()).isEqualTo(9999); + assertThat(props.getServer().isEnabled()).isFalse(); + assertThat(props.getGc().isEnabled()).isTrue(); + assertThat(props.getProfiling().isEnabled()).isTrue(); + assertThat(props.getProfiling().getIntervalMs()).isEqualTo(10); + assertThat(props.getContention().getThresholdMs()).isEqualTo(25); + assertThat(props.getMetrics().getPrometheus().isEnabled()).isTrue(); + }); + } + + @Test + void argusEnabledFalseShortCircuitsAutoConfiguration() { + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ArgusAutoConfiguration.class)) + .withPropertyValues("argus.enabled=false") + .run(context -> { + assertThat(context).doesNotHaveBean(ArgusAutoConfiguration.class); + assertThat(context).doesNotHaveBean(AgentConfig.class); + assertThat(context).doesNotHaveBean(ArgusProperties.class); + }); + } + + @Test + void argusEnabledMissingActivatesAutoConfigurationByDefault() { + // ArgusAutoConfiguration uses matchIfMissing=true; if the user does + // not set argus.enabled, the auto-config should activate. We assert + // ArgusProperties shows up — the bean graph beyond that is exercised + // by the dedicated mapping tests (ArgusAutoConfigurationTest) without + // booting the server thread. + new ApplicationContextRunner() + .withUserConfiguration(EnablePropsOnlyConfig.class) + .run(context -> assertThat(context).hasSingleBean(ArgusProperties.class)); + } + + @EnableConfigurationProperties(ArgusProperties.class) + static class EnablePropsOnlyConfig { + } +} diff --git a/install.ps1 b/install.ps1 index b563148..842780e 100644 --- a/install.ps1 +++ b/install.ps1 @@ -75,9 +75,46 @@ New-Item -ItemType Directory -Path $BinDir -Force | Out-Null $DownloadBase = "https://github.com/$Repo/releases/download/$Version" +# Try to fetch checksums.txt from the release. Releases before 1.3.0 do not +# publish it, so a 404 is non-fatal — we just skip integrity checks for those. +$ChecksumsFile = Join-Path $InstallDir ".checksums.txt" +$ChecksumsAvailable = $false +try { + Invoke-WebRequest "$DownloadBase/checksums.txt" -OutFile $ChecksumsFile -UseBasicParsing -ErrorAction Stop + $ChecksumsAvailable = $true + Write-Ok "Fetched checksums.txt for SHA-256 verification" +} catch { + if (Test-Path $ChecksumsFile) { Remove-Item $ChecksumsFile -Force } + Write-Warn "checksums.txt not published for $Version - skipping integrity check" +} + +# Verify-Sha256: aborts on mismatch, skips silently when checksums.txt is +# unavailable or has no entry for the artifact. +function Verify-Sha256 { + param([string]$LocalFile, [string]$ExpectedName) + if (-not $ChecksumsAvailable) { return } + $line = Select-String -Path $ChecksumsFile -Pattern " $([regex]::Escape($ExpectedName))$" -SimpleMatch:$false ` + | Select-Object -First 1 + if (-not $line) { + Write-Warn " no checksum entry for $ExpectedName - skipping" + return + } + $expected = ($line.Line -split '\s+')[0] + $actual = (Get-FileHash $LocalFile -Algorithm SHA256).Hash.ToLower() + if ($expected -ne $actual) { + Write-Err "SHA-256 mismatch for $ExpectedName" + Write-Err " expected: $expected" + Write-Err " actual: $actual" + Remove-Item $LocalFile -Force -ErrorAction SilentlyContinue + exit 1 + } + Write-Ok " sha256 verified: $ExpectedName" +} + Write-Info "Downloading argus-agent-${VerNum}.jar ..." try { Invoke-WebRequest "$DownloadBase/argus-agent-${VerNum}.jar" -OutFile "$InstallDir\argus-agent.jar" -UseBasicParsing + Verify-Sha256 -LocalFile "$InstallDir\argus-agent.jar" -ExpectedName "argus-agent.jar" Write-Ok "argus-agent.jar" } catch { Write-Err "Failed to download argus-agent. Check version: $Version" @@ -87,6 +124,7 @@ try { Write-Info "Downloading argus-cli-${VerNum}-all.jar ..." try { Invoke-WebRequest "$DownloadBase/argus-cli-${VerNum}-all.jar" -OutFile "$InstallDir\argus-cli.jar" -UseBasicParsing + Verify-Sha256 -LocalFile "$InstallDir\argus-cli.jar" -ExpectedName "argus-cli-${VerNum}-all.jar" Write-Ok "argus-cli.jar" } catch { Write-Warn "argus-cli not found in release. CLI may not be available in $Version." diff --git a/install.sh b/install.sh index db9f575..ab0126d 100755 --- a/install.sh +++ b/install.sh @@ -105,14 +105,66 @@ mkdir -p "$INSTALL_DIR" "$BIN_DIR" DOWNLOAD_BASE="https://github.com/$REPO/releases/download/$VERSION" +# Resolve which sha256 binary is available so we can verify release artifacts. +if command -v sha256sum &>/dev/null; then + SHA256_CMD="sha256sum" +elif command -v shasum &>/dev/null; then + SHA256_CMD="shasum -a 256" +else + SHA256_CMD="" +fi + +# Try to fetch checksums.txt from the release. Releases before 1.3.0 do not +# publish it, so a 404 is non-fatal — we just skip integrity checks for those. +CHECKSUMS_FILE="" +if [ -n "$SHA256_CMD" ]; then + CHECKSUMS_FILE="$INSTALL_DIR/.checksums.txt" + if curl -fsSL "$DOWNLOAD_BASE/checksums.txt" -o "$CHECKSUMS_FILE" 2>/dev/null; then + ok "Fetched checksums.txt for SHA-256 verification" + else + rm -f "$CHECKSUMS_FILE" + CHECKSUMS_FILE="" + warn "checksums.txt not published for $VERSION — skipping integrity check" + fi +else + warn "No sha256sum/shasum on PATH — skipping integrity check" +fi + +# verify_sha256 +# Aborts the install on mismatch. Skips silently if checksums.txt is unavailable +# or has no entry for the file (e.g. a legacy artifact). +verify_sha256() { + local local_file="$1" + local expected_name="$2" + [ -z "$CHECKSUMS_FILE" ] && return 0 + local expected_hash + expected_hash=$(awk -v name="$expected_name" '$2 == name {print $1}' "$CHECKSUMS_FILE" | head -1) + if [ -z "$expected_hash" ]; then + warn " no checksum entry for $expected_name — skipping" + return 0 + fi + local actual_hash + actual_hash=$($SHA256_CMD "$local_file" | awk '{print $1}') + if [ "$expected_hash" != "$actual_hash" ]; then + error "SHA-256 mismatch for $expected_name" + error " expected: $expected_hash" + error " actual: $actual_hash" + rm -f "$local_file" + exit 1 + fi + ok " sha256 verified: $expected_name" +} + info "Downloading argus-agent-${VER_NUM}.jar ..." curl -fSL "$DOWNLOAD_BASE/argus-agent-${VER_NUM}.jar" -o "$INSTALL_DIR/argus-agent.jar" \ || { error "Failed to download argus-agent. Check version: $VERSION"; exit 1; } +verify_sha256 "$INSTALL_DIR/argus-agent.jar" "argus-agent.jar" ok "argus-agent.jar" info "Downloading argus-cli-${VER_NUM}-all.jar ..." curl -fSL "$DOWNLOAD_BASE/argus-cli-${VER_NUM}-all.jar" -o "$INSTALL_DIR/argus-cli.jar" \ || { warn "argus-cli not found in release. CLI may not be available in $VERSION."; } +verify_sha256 "$INSTALL_DIR/argus-cli.jar" "argus-cli-${VER_NUM}-all.jar" ok "argus-cli.jar" # --- Attempt native binary download (faster startup, no JVM required for launch) ---