diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml new file mode 100644 index 0000000..e8f0182 --- /dev/null +++ b/.github/workflows/pr-checks.yml @@ -0,0 +1,149 @@ +name: PR Checks + +on: + pull_request: + branches: [ main ] + push: + branches: [ main ] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + pull-requests: write + issues: write + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +jobs: + # Quick checks that fail fast + format: + name: Format Check + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + + - name: Check formatting + run: cargo fmt --all -- --check + + lint: + name: Clippy Lint + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: clippy + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: ~/.cargo/registry/index + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo-registry- + + - name: Cache cargo build + uses: actions/cache@v4 + with: + path: target + key: ${{ runner.os }}-cargo-build-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo-build- + + - name: Run Clippy + run: make ci-lint + + # Main test suite + test: + name: Test Suite + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + rust: [stable, beta] + exclude: + - os: macos-latest + rust: beta + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust ${{ matrix.rust }} + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: ~/.cargo/registry/index + key: ${{ runner.os }}-${{ matrix.rust }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-${{ matrix.rust }}-cargo-registry- + + - name: Cache cargo build + uses: actions/cache@v4 + with: + path: target + key: ${{ runner.os }}-${{ matrix.rust }}-cargo-build-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-${{ matrix.rust }}-cargo-build- + + - name: Generate test certificates + run: | + mkdir -p certs + openssl req -x509 -newkey rsa:4096 -keyout certs/test_key.pem -out certs/test_cert.pem -days 365 -nodes -subj "/CN=localhost" + + - name: Run tests + run: make ci-test + + - name: Run doctests + run: cargo test --doc + + # Security audit + security: + name: Security Audit + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Install cargo-audit + run: cargo install cargo-audit + + - name: Run security audit + run: make ci-security + + + # Summary job that depends on all checks + pr-checks-complete: + name: All PR Checks Passed + runs-on: ubuntu-latest + needs: [format, lint, test, security] + if: always() + steps: + - name: Check results + run: | + if [[ "${{ needs.format.result }}" == "failure" ]] || \ + [[ "${{ needs.lint.result }}" == "failure" ]] || \ + [[ "${{ needs.test.result }}" == "failure" ]] || \ + [[ "${{ needs.security.result }}" == "failure" ]]; then + echo "❌ One or more checks failed" + exit 1 + fi + echo "✅ All PR checks passed!" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..096f088 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,246 @@ +name: Release + +on: + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+' + - '[0-9]+.[0-9]+.[0-9]+-*' + workflow_dispatch: + inputs: + version: + description: 'Version to publish (e.g., 0.2.0)' + required: true + type: string + dry_run: + description: 'Perform dry run only (do not publish)' + required: false + type: boolean + default: false + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +jobs: + # Pre-release validation + validate: + name: Pre-release Validation + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: ~/.cargo/registry/index + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo-registry- + + - name: Cache cargo build + uses: actions/cache@v4 + with: + path: target + key: ${{ runner.os }}-cargo-release-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo-release- + + - name: Run tests + run: cargo test --all-features + + - name: Run clippy + run: cargo clippy --all-targets --all-features -- -D warnings + + - name: Check formatting + run: cargo fmt --all -- --check + + - name: Build documentation + run: cargo doc --no-deps --all-features + env: + RUSTDOCFLAGS: -D warnings + + - name: Verify package can be built + run: cargo package --allow-dirty + + - name: Verify version matches + if: github.event_name == 'push' + run: | + CARGO_VERSION=$(cargo metadata --format-version 1 --no-deps | jq -r '.packages[0].version') + RELEASE_TAG="${GITHUB_REF#refs/tags/}" + + echo "Cargo.toml version: $CARGO_VERSION" + echo "Git tag: $RELEASE_TAG" + + if [ "$CARGO_VERSION" != "$RELEASE_TAG" ]; then + echo "❌ Error: Version mismatch!" + echo "Cargo.toml version ($CARGO_VERSION) does not match git tag ($RELEASE_TAG)" + echo "" + echo "To fix this:" + echo "1. Update version in Cargo.toml to $RELEASE_TAG" + echo "2. Commit the change" + echo "3. Re-tag with: git tag -f $RELEASE_TAG" + exit 1 + fi + + echo "✅ Version check passed" + + # Publish to crates.io + publish: + name: Publish to crates.io + needs: validate + runs-on: ubuntu-latest + if: | + github.event_name == 'push' || + (github.event_name == 'workflow_dispatch' && !inputs.dry_run) + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: ~/.cargo/registry/index + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo-registry- + + - name: Publish to crates.io + run: cargo publish --token ${{ secrets.CARGO_REGISTRY_TOKEN }} + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + + - name: Create publish summary + run: | + VERSION="${GITHUB_REF#refs/tags/}" + + echo "## 🚀 Published to crates.io" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Version ${VERSION}** has been successfully published!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Installation" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`toml" >> $GITHUB_STEP_SUMMARY + echo "[dependencies]" >> $GITHUB_STEP_SUMMARY + echo "github_app = \"${VERSION}\"" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Links" >> $GITHUB_STEP_SUMMARY + echo "- 📦 [crates.io/crates/github_app](https://crates.io/crates/github_app)" >> $GITHUB_STEP_SUMMARY + echo "- 📚 [docs.rs/github_app/${VERSION}](https://docs.rs/github_app/${VERSION})" >> $GITHUB_STEP_SUMMARY + + # Dry run for testing + dry-run: + name: Publish Dry Run + needs: validate + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' && inputs.dry_run + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: ~/.cargo/registry/index + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo-registry- + + - name: Run dry-run publish + run: cargo publish --dry-run --allow-dirty + + - name: Display package contents + run: | + echo "## 📦 Package Contents" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + cargo package --list --allow-dirty >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + + # Update documentation + update-docs: + name: Update Documentation + needs: publish + runs-on: ubuntu-latest + if: github.event_name == 'push' + steps: + - name: Documentation summary + run: | + VERSION="${GITHUB_REF#refs/tags/}" + + echo "## 📚 Documentation Updated" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Documentation for version ${VERSION} is being built on docs.rs." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "It should be available at https://docs.rs/github_app/${VERSION} within a few minutes." >> $GITHUB_STEP_SUMMARY + + # Post-release checks + verify-published: + name: Verify Published Package + needs: publish + runs-on: ubuntu-latest + if: github.event_name == 'push' + steps: + - name: Wait for crates.io to sync + run: sleep 60 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Verify package is available + run: | + VERSION="${GITHUB_REF#refs/tags/}" + + echo "Checking if github_app ${VERSION} is available on crates.io..." + + # Try to fetch the package info + if cargo search github_app --limit 10 | grep -q "github_app.*${VERSION}"; then + echo "✅ Package successfully published and indexed on crates.io!" + else + echo "⚠️ Package may still be indexing. Please check manually." + fi + + - name: Test installation + run: | + VERSION="${GITHUB_REF#refs/tags/}" + + # Create a test project and try to install the new version + cargo init --lib test-install + cd test-install + cargo add github_app@${VERSION} || echo "Package may still be syncing" + + # Announce release + announce: + name: Announce Release + needs: [publish, verify-published] + runs-on: ubuntu-latest + if: github.event_name == 'push' && success() + steps: + - name: Create release summary + run: | + VERSION="${GITHUB_REF#refs/tags/}" + + echo "## 🎉 Release ${VERSION} Complete!" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ✅ Published Successfully" >> $GITHUB_STEP_SUMMARY + echo "- 📦 [crates.io/crates/github_app](https://crates.io/crates/github_app)" >> $GITHUB_STEP_SUMMARY + echo "- 📚 [docs.rs/github_app/${VERSION}](https://docs.rs/github_app/${VERSION})" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Installation" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`toml" >> $GITHUB_STEP_SUMMARY + echo "[dependencies]" >> $GITHUB_STEP_SUMMARY + echo "github_app = \"${VERSION}\"" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### What's Next?" >> $GITHUB_STEP_SUMMARY + echo "- Documentation will be available on docs.rs within a few minutes" >> $GITHUB_STEP_SUMMARY + echo "- All CI checks passed ✅" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Thank you for using github_app! 🚀" >> $GITHUB_STEP_SUMMARY diff --git a/Cargo.toml b/Cargo.toml index 96ffc36..3793812 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,15 @@ name = "github_app" version = "0.1.0" edition = "2021" +authors = ["InputLayer"] +description = "A Rust library for GitHub App integration with GitOps support - JWT authentication, webhook verification, and Git-based manifest management" +license = "MIT" +repository = "https://github.com/inputlayer/github_app" +homepage = "https://github.com/inputlayer/github_app" +documentation = "https://docs.rs/github_app" +readme = "README.md" +keywords = ["github", "webhook", "gitops", "authentication", "jwt"] +categories = ["api-bindings", "authentication", "web-programming"] [dependencies] serde = { version = "1.0", features = ["derive"] } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ef3d111 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 InputLayer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ee58145 --- /dev/null +++ b/Makefile @@ -0,0 +1,80 @@ +# Makefile for github_app +# Used by CI/CD workflows and local development + +.PHONY: help +help: ## Show this help message + @echo "Available targets:" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}' + +# ============================================================================ +# CI Targets (used by GitHub Actions) +# ============================================================================ + +.PHONY: ci-lint +ci-lint: ## Run clippy linting for CI (strict mode) + cargo clippy --all-targets --all-features -- -D warnings + +.PHONY: ci-test +ci-test: ## Run tests for CI with all features + cargo test --all-features + +.PHONY: ci-security +ci-security: ## Run security audit (requires cargo-audit) + cargo audit + +# ============================================================================ +# Development Targets +# ============================================================================ + +.PHONY: test +test: ## Run tests + cargo test --all-features + +.PHONY: lint +lint: ## Run clippy linting + cargo clippy --all-targets --all-features + +.PHONY: fmt +fmt: ## Format code + cargo fmt --all + +.PHONY: fmt-check +fmt-check: ## Check code formatting + cargo fmt --all -- --check + +.PHONY: check +check: ## Run cargo check + cargo check --all-targets --all-features + +.PHONY: build +build: ## Build the library + cargo build --all-features + +.PHONY: doc +doc: ## Build and open documentation + cargo doc --no-deps --all-features --open + +.PHONY: clean +clean: ## Clean build artifacts + cargo clean + rm -rf certs/ + +# ============================================================================ +# Publishing +# ============================================================================ + +.PHONY: publish-check +publish-check: ## Check if package can be published (dry-run) + cargo publish --dry-run --allow-dirty + +.PHONY: publish +publish: ## Publish to crates.io + cargo publish + +# ============================================================================ +# Convenience Targets +# ============================================================================ + +.PHONY: pre-commit +pre-commit: fmt-check lint test ## Run pre-commit checks (format, lint, test) + @echo "✅ All pre-commit checks passed!" diff --git a/src/app_auth.rs b/src/app_auth.rs index 995ad6b..4ffcb70 100644 --- a/src/app_auth.rs +++ b/src/app_auth.rs @@ -1,6 +1,6 @@ use crate::{GitHubAppConfig, GitHubError}; use chrono::{DateTime, Duration, Utc}; -use jsonwebtoken::{encode, EncodingKey, Header, Algorithm}; +use jsonwebtoken::{encode, Algorithm, EncodingKey, Header}; use serde::{Deserialize, Serialize}; use std::sync::Arc; use tokio::sync::Mutex; @@ -57,13 +57,14 @@ impl GitHubTokenProvider { async fn fetch_installation_token(&self) -> Result { let jwt = self.create_jwt()?; - + let url = format!( "https://api.github.com/app/installations/{}/access_tokens", self.config.installation_id ); - let response = self.client + let response = self + .client .post(&url) .header("Authorization", format!("Bearer {}", jwt)) .header("Accept", "application/vnd.github+json") diff --git a/src/config.rs b/src/config.rs index b078853..3ea8fd1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -13,6 +13,7 @@ pub struct GitHubAppConfig { } impl GitHubAppConfig { + #[allow(clippy::too_many_arguments)] pub fn new( app_id: u64, installation_id: u64, @@ -37,25 +38,39 @@ impl GitHubAppConfig { pub fn validate(&self) -> Result<(), crate::error::GitHubError> { if self.app_id == 0 { - return Err(crate::error::GitHubError::Config("app_id cannot be 0".to_string())); + return Err(crate::error::GitHubError::Config( + "app_id cannot be 0".to_string(), + )); } if self.installation_id == 0 { - return Err(crate::error::GitHubError::Config("installation_id cannot be 0".to_string())); + return Err(crate::error::GitHubError::Config( + "installation_id cannot be 0".to_string(), + )); } if self.private_key_pem.is_empty() { - return Err(crate::error::GitHubError::Config("private_key_pem cannot be empty".to_string())); + return Err(crate::error::GitHubError::Config( + "private_key_pem cannot be empty".to_string(), + )); } if self.webhook_secret.is_empty() { - return Err(crate::error::GitHubError::Config("webhook_secret cannot be empty".to_string())); + return Err(crate::error::GitHubError::Config( + "webhook_secret cannot be empty".to_string(), + )); } if self.repo.is_empty() { - return Err(crate::error::GitHubError::Config("repo cannot be empty".to_string())); + return Err(crate::error::GitHubError::Config( + "repo cannot be empty".to_string(), + )); } if self.branch.is_empty() { - return Err(crate::error::GitHubError::Config("branch cannot be empty".to_string())); + return Err(crate::error::GitHubError::Config( + "branch cannot be empty".to_string(), + )); } if self.manifest_glob.is_empty() { - return Err(crate::error::GitHubError::Config("manifest_glob cannot be empty".to_string())); + return Err(crate::error::GitHubError::Config( + "manifest_glob cannot be empty".to_string(), + )); } Ok(()) } diff --git a/src/gitops.rs b/src/gitops.rs index c7daa4d..afc150a 100644 --- a/src/gitops.rs +++ b/src/gitops.rs @@ -18,9 +18,7 @@ impl GitHubGitOps { fn run_git_command(&self, args: &[&str], cwd: Option<&Path>) -> Result { let mut cmd = Command::new("git"); - cmd.args(args) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()); + cmd.args(args).stdout(Stdio::piped()).stderr(Stdio::piped()); if let Some(dir) = cwd { cmd.current_dir(dir); @@ -109,9 +107,7 @@ impl GitHubGitOps { for entry in glob::glob(&pattern)? { let path = entry?; let content = std::fs::read_to_string(&path)?; - let manifest: T = serde_yaml::from_str(&content).map_err(|e| { - GitHubError::Yaml(e) - })?; + let manifest: T = serde_yaml::from_str(&content).map_err(GitHubError::Yaml)?; manifests.push(manifest); } diff --git a/src/lib.rs b/src/lib.rs index 0075c76..9799761 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,11 @@ +pub mod app_auth; pub mod config; pub mod error; -pub mod app_auth; -pub mod webhook; pub mod gitops; +pub mod webhook; +pub use app_auth::GitHubTokenProvider; pub use config::GitHubAppConfig; pub use error::GitHubError; -pub use app_auth::GitHubTokenProvider; -pub use webhook::{WebhookVerifier, WebhookEvent, PushEvent}; pub use gitops::GitHubGitOps; +pub use webhook::{PushEvent, WebhookEvent, WebhookVerifier}; diff --git a/src/webhook.rs b/src/webhook.rs index 88bb020..32e902a 100644 --- a/src/webhook.rs +++ b/src/webhook.rs @@ -26,11 +26,9 @@ pub struct Commit { impl PushEvent { pub fn branch(&self) -> Option { - if let Some(branch) = self.git_ref.strip_prefix("refs/heads/") { - Some(branch.to_string()) - } else { - None - } + self.git_ref + .strip_prefix("refs/heads/") + .map(|branch| branch.to_string()) } pub fn commit_sha(&self) -> Option { @@ -57,17 +55,18 @@ impl WebhookVerifier { } pub fn verify_signature(&self, signature: &str, body: &[u8]) -> Result<(), GitHubError> { - let signature = signature.strip_prefix("sha256=") + let signature = signature + .strip_prefix("sha256=") .ok_or(GitHubError::InvalidSignature)?; - let expected_signature = hex::decode(signature) - .map_err(|_| GitHubError::InvalidSignature)?; + let expected_signature = + hex::decode(signature).map_err(|_| GitHubError::InvalidSignature)?; let mut mac = HmacSha256::new_from_slice(self.secret.as_bytes()) .map_err(|e| GitHubError::Other(format!("Failed to create HMAC: {}", e)))?; - + mac.update(body); - + mac.verify_slice(&expected_signature) .map_err(|_| GitHubError::InvalidSignature)?; diff --git a/tests/integration_test.rs b/tests/integration_test.rs index ba6f718..4124a37 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -1,4 +1,4 @@ -use github_app::{GitHubAppConfig, WebhookVerifier, WebhookEvent}; +use github_app::{GitHubAppConfig, WebhookEvent, WebhookVerifier}; use std::path::PathBuf; #[test] @@ -13,9 +13,9 @@ fn test_config_validation() { PathBuf::from("/tmp/test"), "**/*.yaml".to_string(), ); - + assert!(valid_config.validate().is_ok()); - + let invalid_config = GitHubAppConfig::new( 0, 456, @@ -26,7 +26,7 @@ fn test_config_validation() { PathBuf::from("/tmp/test"), "**/*.yaml".to_string(), ); - + assert!(invalid_config.validate().is_err()); } @@ -42,15 +42,16 @@ fn test_webhook_signature_verification() { PathBuf::from("/tmp/test"), "**/*.yaml".to_string(), ); - + let verifier = WebhookVerifier::new(&config); - + let body = b"{\"ref\":\"refs/heads/main\"}"; - let valid_signature = "sha256=8c9b5f8f7f9d4e6c5d4b3a2f1e0d9c8b7a6f5e4d3c2b1a0f9e8d7c6b5a4f3e2d1"; - + let valid_signature = + "sha256=8c9b5f8f7f9d4e6c5d4b3a2f1e0d9c8b7a6f5e4d3c2b1a0f9e8d7c6b5a4f3e2d1"; + let result = verifier.verify_signature(valid_signature, body); assert!(result.is_err()); - + let invalid_signature = "sha256=invalid"; let result = verifier.verify_signature(invalid_signature, body); assert!(result.is_err()); @@ -68,9 +69,9 @@ fn test_push_event_parsing() { PathBuf::from("/tmp/test"), "**/*.yaml".to_string(), ); - + let verifier = WebhookVerifier::new(&config); - + let body = br#"{ "ref": "refs/heads/main", "repository": { @@ -81,18 +82,18 @@ fn test_push_event_parsing() { "id": "abc123" } }"#; - + use hmac::{Hmac, Mac}; use sha2::Sha256; type HmacSha256 = Hmac; - + let mut mac = HmacSha256::new_from_slice(b"test_secret").unwrap(); mac.update(body); let result = mac.finalize(); let signature = format!("sha256={}", hex::encode(result.into_bytes())); - + let event = verifier.parse_event("push", &signature, body).unwrap(); - + match event { WebhookEvent::Push(push_event) => { assert_eq!(push_event.git_ref, "refs/heads/main"); @@ -116,24 +117,24 @@ fn test_ping_event_parsing() { PathBuf::from("/tmp/test"), "**/*.yaml".to_string(), ); - + let verifier = WebhookVerifier::new(&config); - + let body = b"{}"; - + use hmac::{Hmac, Mac}; use sha2::Sha256; type HmacSha256 = Hmac; - + let mut mac = HmacSha256::new_from_slice(b"test_secret").unwrap(); mac.update(body); let result = mac.finalize(); let signature = format!("sha256={}", hex::encode(result.into_bytes())); - + let event = verifier.parse_event("ping", &signature, body).unwrap(); - + match event { - WebhookEvent::Ping => {}, + WebhookEvent::Ping => {} _ => panic!("Expected Ping event"), } } @@ -141,8 +142,8 @@ fn test_ping_event_parsing() { #[tokio::test] async fn test_token_provider_concurrent_access() { use github_app::GitHubTokenProvider; - use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::Arc; let config = GitHubAppConfig::new( 123, @@ -180,7 +181,10 @@ async fn test_token_provider_concurrent_access() { // Wait for all tasks to complete for handle in handles { // Should not panic or timeout - assert!(handle.await.is_ok(), "Task should complete without panicking"); + assert!( + handle.await.is_ok(), + "Task should complete without panicking" + ); } // All 10 should have failed gracefully (no GitHub creds)