diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml new file mode 100644 index 0000000..7260d1c --- /dev/null +++ b/.github/workflows/integration-tests.yml @@ -0,0 +1,230 @@ +--- +name: Integration Tests + +on: + # Manual trigger for on-demand runs. + # NOTE: we always test mindersec/minder@main to prevent + # impersonation attacks where a fork's malicious Makefile/compose + # code could run with access to this repo's secrets. + workflow_dispatch: + inputs: + test_tags: + description: 'Robot Framework tag filter (e.g. "core", "smoke")' + required: false + default: '' + + # Weekly scheduled run + schedule: + - cron: "0 6 * * 1" # Every Monday at 6:00 AM UTC + + # Run on PRs to validate test infrastructure changes + pull_request: + paths: + - "scripts/**" + - "minder-tests/**" + - "resources/**" + - "smoke-test-config*.yaml" + - ".github/workflows/integration-tests.yml" + - "requirements.txt" + +concurrency: + group: integration-tests-${{ github.ref }} + cancel-in-progress: true + +jobs: + integration-test: + name: Run Integration Tests + runs-on: ubuntu-latest + timeout-minutes: 30 + + permissions: + contents: read + + steps: + # ------------------------------------------------------------------ + # Checkout smoke-tests. + # We test against mindersec/minder@main using pre-built images — + # we do NOT run any Makefile from the minder repo. + # ------------------------------------------------------------------ + - name: Checkout smoke-tests + uses: actions/checkout@v4 + + # ------------------------------------------------------------------ + # Install test dependencies directly on the runner. + # No container image needed — the runner already has Python. + # ------------------------------------------------------------------ + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: pip + + - name: Install Robot Framework and dependencies + run: pip install -r requirements.txt + + # ------------------------------------------------------------------ + # Install minder CLI from the latest published GitHub release. + # Avoids building from source (~10 min saved). + # ------------------------------------------------------------------ + - name: Install minder CLI + run: | + # Fetch the download URL for the linux amd64 tarball directly from + # the releases API — avoids guessing the exact filename format + # (goreleaser strips the 'v' prefix: v0.1.2 -> minder_0.1.2_linux_amd64.tar.gz) + ASSET_URL=$(curl -sf https://api.github.com/repos/mindersec/minder/releases/latest \ + | jq -r '.assets[] + | select(.name | test("linux_amd64\\.tar\\.gz$")) + | .browser_download_url') + + if [ -z "${ASSET_URL}" ]; then + echo "ERROR: could not find a linux_amd64 tarball in the latest release assets." + echo "Available assets:" + curl -sf https://api.github.com/repos/mindersec/minder/releases/latest \ + | jq -r '.assets[].name' + exit 1 + fi + + echo "Downloading minder CLI from: ${ASSET_URL}" + curl -fsSL "${ASSET_URL}" | tar -xz -C /usr/local/bin minder + minder version + + # ------------------------------------------------------------------ + # Fetch the minder docker-compose.yaml from main and patch it to + # use pre-built ghcr.io images instead of local build directives. + # The minder compose file has `build: context: .` for the server + # and migrate services — we replace these with the published image. + # ------------------------------------------------------------------ + - name: Fetch and patch Minder docker-compose + run: | + curl -sLo docker-compose.minder.yaml \ + https://raw.githubusercontent.com/mindersec/minder/main/docker-compose.yaml + + # Install yq to patch the compose file + sudo wget -qO /usr/local/bin/yq \ + https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 + sudo chmod +x /usr/local/bin/yq + + # Replace build directives with ghcr.io pre-built images + yq e ' + del(.services.minder.build) | + .services.minder.image = "ghcr.io/mindersec/minder:latest" | + del(.services.migrate.build) | + .services.migrate.image = "ghcr.io/mindersec/minder:latest" + ' -i docker-compose.minder.yaml + + # ------------------------------------------------------------------ + # minder needs a few config files and SSH keys that make bootstrap + # normally generates. Recreate the minimal set here. + # ------------------------------------------------------------------ + - name: Generate minder runtime config + run: | + # Fetch config templates from minder main + curl -sLo server-config.yaml \ + https://raw.githubusercontent.com/mindersec/minder/main/config/server-config.yaml.example + curl -sLo flags-config.yaml \ + https://raw.githubusercontent.com/mindersec/minder/main/flags-config.yaml || echo "{}" > flags-config.yaml + + # Generate the SSH keys minder expects + mkdir -p .ssh .secrets + openssl genrsa -out .ssh/access_token_rsa 2048 2>/dev/null + openssl rsa -in .ssh/access_token_rsa -pubout -out .ssh/access_token_rsa.pub 2>/dev/null + openssl genrsa -out .ssh/refresh_token_rsa 2048 2>/dev/null + openssl rsa -in .ssh/refresh_token_rsa -pubout -out .ssh/refresh_token_rsa.pub 2>/dev/null + openssl rand -base64 32 > .ssh/token_key_passphrase + # Placeholder GitHub App key (real one not needed for core tests) + openssl genrsa -out .secrets/github-app.pem 2048 2>/dev/null + echo "Config files and SSH keys ready." + + # ------------------------------------------------------------------ + # Start the stack. --wait blocks until all healthchecks pass. + # No separate health-check polling needed. + # ------------------------------------------------------------------ + - name: Start Minder Docker stack + run: | + docker compose -f docker-compose.minder.yaml pull --quiet + docker compose -f docker-compose.minder.yaml up -d --wait + + # ------------------------------------------------------------------ + # Bootstrap: create test user, get offline token, enroll user, + # extract project ID. Script writes MINDER_PROJECT to $GITHUB_ENV. + # ------------------------------------------------------------------ + - name: Bootstrap test user and project + run: ./scripts/bootstrap.sh + env: + KEYCLOAK_URL: http://localhost:8081 + MINDER_API_URL: http://localhost:8080 + MINDER_BINARY: /usr/local/bin/minder + OFFLINE_TOKEN_OUTPUT_PATH: ${{ github.workspace }}/offline.token + CLIENT_ID: smoke-test-client + + # ------------------------------------------------------------------ + # Run Robot Framework tests directly on the runner. + # MINDER_PROJECT is set by bootstrap.sh via $GITHUB_ENV and is + # available here as a shell env var — do NOT use ${{ env.X }} syntax + # for vars set in previous steps (those are static at parse time). + # ------------------------------------------------------------------ + - name: Run smoke tests + run: | + EXTRA_ARGS="" + if [ -n "${{ github.event.inputs.test_tags }}" ]; then + EXTRA_ARGS="-i ${{ github.event.inputs.test_tags }}" + fi + robot \ + --outputdir results \ + --pythonpath . \ + --xunit results/xoutput.xml \ + --loglevel INFO \ + ${EXTRA_ARGS} \ + minder-tests/ + env: + MINDER_CONFIG: ${{ github.workspace }}/smoke-test-config.yaml + MINDER_OFFLINE_TOKEN_PATH: ${{ github.workspace }}/offline.token + MINDER_TEST_ORG: ${{ secrets.MINDER_TEST_ORG }} + GH_TOKEN: ${{ secrets.GH_TOKEN }} + # MINDER_PROJECT is injected automatically from $GITHUB_ENV + # (set by bootstrap.sh) — no need to reference it here explicitly + + # ------------------------------------------------------------------ + # Results + # ------------------------------------------------------------------ + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: robot-test-results + path: results/ + retention-days: 30 + + - name: Publish test report + # Only run if the results file was actually created (tests ran) + if: always() && hashFiles('results/xoutput.xml') != '' + uses: dorny/test-reporter@v1 + with: + name: "Integration Test Results" + path: results/xoutput.xml + reporter: java-junit + fail-on-error: false + + # ------------------------------------------------------------------ + # Teardown + # All three steps guard against docker-compose.minder.yaml not + # existing (e.g. when an earlier step failed before creating it). + # ------------------------------------------------------------------ + - name: Show Minder logs on failure + if: failure() + run: | + if [ -f docker-compose.minder.yaml ]; then + docker compose -f docker-compose.minder.yaml logs minder --tail=100 + docker compose -f docker-compose.minder.yaml logs keycloak --tail=50 + else + echo "docker-compose.minder.yaml not found — stack was never started." + fi + + - name: Teardown Minder stack + if: always() + run: | + if [ -f docker-compose.minder.yaml ]; then + docker compose -f docker-compose.minder.yaml down -v --remove-orphans + else + echo "docker-compose.minder.yaml not found — nothing to tear down." + fi diff --git a/.gitignore b/.gitignore index b660548..6c6ae02 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ config.yaml log.html output.xml report.html +# integration test generated files +minder-project-id diff --git a/README.md b/README.md index d36cc06..b0dd7c9 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ task test Since the tests run in a container, they need a `minder` Linux binary in the path. If you're runninng on a non-Linux machine, you need to provide one with an environment variable: + ```bash MINDER_BINARY_PATH=/path/to/minder task test ``` @@ -30,6 +31,7 @@ tests that create, modify, or delete repositories and pull requests. It is possible to specify the org specifying `MINDER_TEST_ORG=` when running `task test`, so the previous example becomes + ```bash MINDER_BINARY_PATH=/path/to/minder MINDER_TEST_ORG=my-org-name task test ``` @@ -42,6 +44,7 @@ section for instructions on how to create repos in a test org. The `task test` command will authenticate using an offline token, by default using the `offline.token` file in the current directory. If you want to test against a different environment, you need to provide a configuration file that contains the endpoints and credentials for the environments you want to test against. For example, to run the tests against the staging environment, you can use the following command: + ```bash MINDER_CONFIG=$(pwd)/staging-config.yaml MINDER_OFFLINE_TOKEN_PATH=$(pwd)/staging-offline.token task test ``` @@ -71,6 +74,7 @@ Confusingly, the `issuer_url` needs to be `localhost` as that corresponds to the ### Using ruletypes from a local repository If you want to run the tests against a local Minder instance with ruletypes from a local repository, you can pass the path to the ruletypes directory as an environment variable. + ```bash MINDER_RULETYPES_PATH=$(pwd)/path/to/ruletypes task test ``` @@ -125,7 +129,6 @@ Valid user login Then the user is logged in ``` - ### Writing custom libraries Custom libraries are written in the `resources` directory. Each library should have its own @@ -155,3 +158,107 @@ Test with repo Given a copy of repo stacklok/demo-repo-python ${test_repo} Then assert stuff on ${test_repo} ``` + +### Running integration tests against a local Minder stack + +The integration test mode runs the smoke tests against a full Minder stack +started via `docker compose` (from the main +[mindersec/minder](https://github.com/mindersec/minder) repo). + +#### Prerequisites + +- Docker / Podman +- [Task](https://taskfile.dev/#/installation) +- A cloned copy of the `mindersec/minder` repository +- `curl`, `jq` on the host +- The `minder` CLI binary on PATH (or set `MINDER_BINARY`) + +#### Quick start + +```bash +# 1. Install Robot Framework on your host (one-time) +pip install -r requirements.txt + +# 2. Clone minder next to the smoke-tests repo (or set MINDER_REPO_PATH) +git clone git@github.com:mindersec/minder.git ../minder + +# 3. Run the full integration test lifecycle +task integration-test +``` + +This will: + +1. Start the Minder Docker stack (`docker compose up`) +2. Run `scripts/bootstrap.sh` to create a test user in Keycloak and generate + an offline token (no browser interaction needed) +3. Execute the Robot Framework test suite +4. Tear down the Docker stack + +#### Running tests manually (step by step) + +```bash +# Install Robot Framework (one-time) +pip install -r requirements.txt + +# Start Minder stack and bootstrap test user +task integration-setup + +# Run all tests against the running stack (no container needed) +task integration-run + +# OR run only core tests (no GitHub provider needed): +task integration-run-core +# which is equivalent to: +task integration-run TEST_TAGS=core + +# Tear down +task integration-teardown +``` + +> **Note on two execution modes:** +> - `task test` — runs tests **inside a Docker container** (good for local dev isolation, matches the original test runner) +> - `task integration-run` — runs tests **directly on the host** using the installed `robot` command (used by CI and for testing against a running stack with manual setup) + +#### Test tags + +Tests are tagged by their infrastructure requirements: + +| Tag | Meaning | +|-----|---------| +| `smoke` | All smoke tests (default) | +| `core` | Tests that only need Minder API (no GitHub) | +| `github-required` | Tests that need a live GitHub org and token | +| `provider-required` | Tests that need an enrolled GitHub provider | + +To run only core tests: + +```bash +task test -- -i core +``` + +To exclude GitHub-dependent tests: + +```bash +task test -- -e github-required +``` + +#### Environment variables for integration mode + +| Variable | Default | Description | +|----------|---------|-------------| +| `MINDER_REPO_PATH` | `../minder` | Path to cloned `mindersec/minder` repo | +| `KEYCLOAK_URL` | `http://localhost:8081` | Keycloak base URL | +| `MINDER_API_URL` | `http://localhost:8080` | Minder HTTP API URL | +| `MINDER_BINARY` | `minder` | Path to the minder CLI binary | +| `TEST_USER` | `smoke-test-user` | Keycloak test user to create | +| `TEST_PASS` | `smoke-test-password` | Test user password | + +#### CI Pipeline + +Integration tests run automatically via GitHub Actions: + +- **On demand**: via `workflow_dispatch` +- **Weekly**: Monday 06:00 UTC +- **On PR**: when CI infrastructure files change + +See `.github/workflows/integration-tests.yml` for details. diff --git a/Taskfile.yml b/Taskfile.yml index 7ae0092..9ac0bbc 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -1,8 +1,8 @@ --- -version: '3' +version: "3" dotenv: - - '.env' + - ".env" env: IMAGE: localhost/smoke-test-experiments @@ -26,7 +26,7 @@ tasks: The minder configuration file is only mounted if it is provided in the environment. Available variables: - + - MINDER_BINARY_PATH: The path to the minder binary. If not provided, the task will try to find the minder binary in the system. - MINDER_CONFIG: The path to the minder configuration file. If not provided, @@ -74,7 +74,7 @@ tasks: CTESTDIR: /robottests vars: _MINDER_BINARY_PATH: - sh: '{{if .MINDER_BINARY_PATH}} echo {{.MINDER_BINARY_PATH}} {{else}} which minder {{end}}' + sh: "{{if .MINDER_BINARY_PATH}} echo {{.MINDER_BINARY_PATH}} {{else}} which minder {{end}}" _CONTAINER_OPTS: '{{._GLOBAL_CONTAINER_OPTS}} {{if ._CONTAINER_CMD | contains "podman"}} {{._PODMAN_OPTS}} {{end}}' preconditions: - sh: test -f {{._MINDER_BINARY_PATH}} @@ -199,3 +199,107 @@ tasks: echo "Enabling subprojects for root projects..." docker exec -it postgres_container psql -h localhost -p 5432 -U postgres -d minder -c "$SET_HIERARCHICAL_SQL" + # ----------------------------------------------------------------------- + # Integration test targets — run smoke tests against a Minder Docker stack + # ----------------------------------------------------------------------- + + integration-test: + desc: Run full integration test lifecycle (setup → test → teardown) + summary: | + Starts a Minder Docker stack, bootstraps a test user, runs the smoke + tests directly via robot (no container), and tears everything down. + + Requires: + - MINDER_REPO_PATH: path to a cloned mindersec/minder repo + (default: ../minder) + - Docker / Podman + - Robot Framework: pip install -r requirements.txt + + Optional: + - MINDER_TEST_ORG: GitHub org for repo-related tests + - GH_TOKEN: GitHub token for repo operations + - TEST_TAGS: Robot tag filter passed to integration-run + + To run only core tests (no GitHub needed): + task integration-test TEST_TAGS=core + + Example: + MINDER_REPO_PATH=~/src/minder task integration-test + cmds: + - task: integration-setup + - defer: { task: integration-teardown } + - task: integration-run + + integration-setup: + desc: Start Minder Docker stack and bootstrap test user + vars: + MINDER_REPO_PATH: '{{.MINDER_REPO_PATH | default "../minder"}}' + cmds: + - cmd: | + echo "Starting Minder Docker stack..." + docker compose -f {{.MINDER_REPO_PATH}}/docker-compose.yaml up -d --wait + silent: false + - cmd: | + echo "Bootstrapping test user..." + ./scripts/bootstrap.sh + silent: false + env: + KEYCLOAK_URL: http://localhost:8081 + MINDER_API_URL: http://localhost:8080 + OFFLINE_TOKEN_OUTPUT_PATH: ./offline.token + preconditions: + - sh: test -f scripts/bootstrap.sh + msg: "bootstrap.sh not found. Are you in the smoke-tests directory?" + - sh: test -f {{.MINDER_REPO_PATH}}/docker-compose.yaml + msg: "Minder docker-compose.yaml not found at {{.MINDER_REPO_PATH}}. Set MINDER_REPO_PATH." + + # integration-run: run tests against a *already running* Minder stack. + # This uses robot directly (no container) — the host Python must have + # requirements.txt installed (e.g. `pip install -r requirements.txt`). + # + # NOTE: This is distinct from `task test`, which builds + runs a + # container image. Use `task test` for local dev isolation; use + # `task integration-run` when running against a pre-existing stack + # (e.g. in CI, or after `task integration-setup`). + integration-run: + desc: Run smoke tests against a running Minder stack (no container) + vars: + _TAG_FILTER: '{{if .TEST_TAGS}}-i {{.TEST_TAGS}}{{end}}' + cmds: + - cmd: | + robot \ + --outputdir {{.ROOT_DIR}}/results \ + --pythonpath {{.ROOT_DIR}} \ + --xunit {{.ROOT_DIR}}/results/xoutput.xml \ + --loglevel INFO \ + {{._TAG_FILTER}} \ + {{.ROOT_DIR}}/minder-tests + env: + MINDER_CONFIG: '{{.ROOT_DIR}}/smoke-test-config.yaml' + MINDER_OFFLINE_TOKEN_PATH: '{{.ROOT_DIR}}/offline.token' + preconditions: + - sh: test -f {{.ROOT_DIR}}/offline.token + msg: "offline.token not found. Run task integration-setup first." + - sh: python3 -c "import robot" 2>/dev/null + msg: "Robot Framework not installed. Run: pip install -r requirements.txt" + + integration-run-core: + desc: Run only core tests (no GitHub provider needed) + cmds: + - task: integration-run + vars: + TEST_TAGS: core + + integration-teardown: + desc: Tear down the Minder Docker stack + vars: + MINDER_REPO_PATH: '{{.MINDER_REPO_PATH | default "../minder"}}' + cmds: + - cmd: | + echo "Tearing down Minder Docker stack..." + docker compose -f {{.MINDER_REPO_PATH}}/docker-compose.yaml down -v --remove-orphans + silent: false + - cmd: | + echo "Cleaning up generated files..." + rm -f offline.token minder-project-id + silent: false diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 0000000..c25d0c0 --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,43 @@ +# docker-compose.test.yml +# +# Compose override that adds the smoke-test runner to the Minder Docker stack. +# This file is meant to be used alongside the main Minder docker-compose.yaml: +# +# docker compose -f /path/to/minder/docker-compose.yaml \ +# -f docker-compose.test.yml up --abort-on-container-exit +# +# The smoke-test runner waits for the Minder server to be healthy, then +# executes the Robot Framework test suite and exits. + +services: + smoke-tests: + container_name: smoke_test_runner + build: + context: . + dockerfile: Dockerfile + depends_on: + minder: + condition: service_healthy + environment: + - MINDER_CONFIG=/etc/minder/config.yaml + - MINDER_OFFLINE_TOKEN_PATH=/opt/minder-offline.token + - MINDER_TEST_ORG=${MINDER_TEST_ORG:-} + - GH_TOKEN=${GH_TOKEN:-} + - MINDER_RULETYPES_PATH=/opt/minder-ruletypes + volumes: + - ./:/robottests:z + - ./offline.token:/opt/minder-offline.token:z + - ./smoke-test-config-docker.yaml:/etc/minder/config.yaml:z + networks: + - app_net + command: > + --outputdir /robottests/results + --pythonpath /robottests + --xunit /robottests/results/xoutput.xml + --loglevel INFO + /robottests/minder-tests + +networks: + app_net: + external: true + name: minder_app_net diff --git a/minder-tests/api-history.robot b/minder-tests/api-history.robot index 789f79a..71dc113 100644 --- a/minder-tests/api-history.robot +++ b/minder-tests/api-history.robot @@ -2,6 +2,8 @@ Resource resources/keywords.robot Resource resources/variables.robot +Test Tags smoke github-required provider-required + Library resources.helpers Library resources.profiles.Profiles Library resources.github.GitHub diff --git a/minder-tests/api-repositories.robot b/minder-tests/api-repositories.robot index 5362bbd..a029f41 100644 --- a/minder-tests/api-repositories.robot +++ b/minder-tests/api-repositories.robot @@ -2,6 +2,8 @@ Resource resources/keywords.robot Resource resources/variables.robot +Test Tags smoke github-required provider-required + Library OperatingSystem Library BuiltIn Library RequestsLibrary diff --git a/minder-tests/minder-tests.robot b/minder-tests/minder-tests.robot index 671c36f..644aa36 100644 --- a/minder-tests/minder-tests.robot +++ b/minder-tests/minder-tests.robot @@ -1,7 +1,7 @@ *** Settings *** Documentation Test suite for some simple Minder operations -Test Tags smoke +Test Tags smoke core Resource resources/keywords.robot Resource resources/variables.robot diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh new file mode 100755 index 0000000..8e3530f --- /dev/null +++ b/scripts/bootstrap.sh @@ -0,0 +1,201 @@ +#!/bin/bash +# bootstrap.sh — Programmatically create a test user in Keycloak and generate +# an offline token for use by the smoke-test suite. +# +# This script eliminates the need for a human to manually authenticate via +# browser-based GitHub OAuth, enabling fully automated integration testing. +# +# Prerequisites: +# - Keycloak is running and healthy +# - Minder server is running (or will be shortly) +# - curl, jq, and minder CLI are available on PATH +# +# Usage: +# ./scripts/bootstrap.sh +# +# Environment variables (all have sensible defaults for run-docker): +# KEYCLOAK_URL — Keycloak base URL (default: http://localhost:8081) +# KC_REALM — Keycloak realm (default: stacklok) +# KC_ADMIN_USER — Keycloak admin username (default: admin) +# KC_ADMIN_PASS — Keycloak admin password (default: admin) +# TEST_USER — Test user to create (default: smoke-test-user) +# TEST_PASS — Test user password (default: smoke-test-password) +# CLIENT_ID — OIDC client ID (default: minder-cli) +# MINDER_API_URL — Minder HTTP API URL (default: http://localhost:8080) +# OFFLINE_TOKEN_OUTPUT_PATH — Where to write token (default: ./offline.token) +# MINDER_BINARY — Path to minder binary (default: minder) + +set -euo pipefail + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- + +KEYCLOAK_URL="${KEYCLOAK_URL:-http://localhost:8081}" +KC_REALM="${KC_REALM:-stacklok}" +KC_ADMIN_USER="${KC_ADMIN_USER:-admin}" +KC_ADMIN_PASS="${KC_ADMIN_PASS:-admin}" +TEST_USER="${TEST_USER:-smoke-test-user}" +TEST_PASS="${TEST_PASS:-smoke-test-password}" +CLIENT_ID="${CLIENT_ID:-minder-cli}" +MINDER_API_URL="${MINDER_API_URL:-http://localhost:8080}" +OFFLINE_TOKEN_OUTPUT_PATH="${OFFLINE_TOKEN_OUTPUT_PATH:-./offline.token}" +MINDER_BINARY="${MINDER_BINARY:-minder}" + +MAX_RETRIES=30 +RETRY_INTERVAL=5 + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +log() { echo "==> $*"; } +warn() { echo "==> [WARN] $*" >&2; } +fatal() { echo "==> [FATAL] $*" >&2; exit 1; } + +wait_for_url() { + local url="$1" + local label="$2" + local retries=0 + + log "Waiting for ${label} at ${url}..." + until curl -sf "${url}" > /dev/null 2>&1; do + retries=$((retries + 1)) + if [ "${retries}" -ge "${MAX_RETRIES}" ]; then + fatal "${label} did not become ready after ${MAX_RETRIES} attempts" + fi + sleep "${RETRY_INTERVAL}" + done + log "${label} is ready." +} + +# --------------------------------------------------------------------------- +# Step 1: Wait for Keycloak +# --------------------------------------------------------------------------- + +wait_for_url "${KEYCLOAK_URL}/health/ready" "Keycloak" + +# --------------------------------------------------------------------------- +# Step 2: Obtain Keycloak admin access token +# --------------------------------------------------------------------------- + +log "Obtaining Keycloak admin access token..." +ADMIN_TOKEN_RESPONSE=$(curl -sf -X POST \ + "${KEYCLOAK_URL}/realms/master/protocol/openid-connect/token" \ + -d "grant_type=password" \ + -d "username=${KC_ADMIN_USER}" \ + -d "password=${KC_ADMIN_PASS}" \ + -d "client_id=admin-cli") \ + || fatal "Failed to obtain admin token from Keycloak" + +ADMIN_TOKEN=$(echo "${ADMIN_TOKEN_RESPONSE}" | jq -r '.access_token') +if [ -z "${ADMIN_TOKEN}" ] || [ "${ADMIN_TOKEN}" = "null" ]; then + fatal "Admin access token is empty or null" +fi +log "Admin token obtained successfully." + +# --------------------------------------------------------------------------- +# Step 3: Create test user in Keycloak +# --------------------------------------------------------------------------- + +log "Creating test user '${TEST_USER}' in realm '${KC_REALM}'..." +HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ + "${KEYCLOAK_URL}/admin/realms/${KC_REALM}/users" \ + -H "Authorization: Bearer ${ADMIN_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"username\": \"${TEST_USER}\", + \"enabled\": true, + \"emailVerified\": true, + \"email\": \"${TEST_USER}@test.local\", + \"firstName\": \"Smoke\", + \"lastName\": \"Test\", + \"credentials\": [{ + \"type\": \"password\", + \"value\": \"${TEST_PASS}\", + \"temporary\": false + }] + }") + +case "${HTTP_STATUS}" in + 201) log "User '${TEST_USER}' created successfully." ;; + 409) log "User '${TEST_USER}' already exists — skipping creation." ;; + *) fatal "Unexpected status ${HTTP_STATUS} creating user. Check Keycloak logs." ;; +esac + +# --------------------------------------------------------------------------- +# Step 4: Request offline token via Resource Owner Password Credentials +# --------------------------------------------------------------------------- + +log "Requesting offline token for '${TEST_USER}' via ROPC grant..." +TOKEN_RESPONSE=$(curl -sf -X POST \ + "${KEYCLOAK_URL}/realms/${KC_REALM}/protocol/openid-connect/token" \ + -d "grant_type=password" \ + -d "client_id=${CLIENT_ID}" \ + -d "username=${TEST_USER}" \ + -d "password=${TEST_PASS}" \ + -d "scope=openid offline_access") \ + || fatal "Failed to obtain offline token. Is the '${CLIENT_ID}' client configured for Direct Access Grants (ROPC)?" + +OFFLINE_TOKEN=$(echo "${TOKEN_RESPONSE}" | jq -r '.refresh_token') +if [ -z "${OFFLINE_TOKEN}" ] || [ "${OFFLINE_TOKEN}" = "null" ]; then + echo "Token response:" >&2 + echo "${TOKEN_RESPONSE}" | jq '.' >&2 + fatal "Offline token (refresh_token) is empty or null. The '${CLIENT_ID}' client may not have offline_access scope or ROPC enabled." +fi + +echo "${OFFLINE_TOKEN}" > "${OFFLINE_TOKEN_OUTPUT_PATH}" +log "Offline token saved to ${OFFLINE_TOKEN_OUTPUT_PATH}" + +# --------------------------------------------------------------------------- +# Step 5: Wait for Minder server +# --------------------------------------------------------------------------- + +wait_for_url "${MINDER_API_URL}/api/v1/health" "Minder server" + +# --------------------------------------------------------------------------- +# Step 6: Bootstrap user in Minder (first auth triggers auto-enrollment) +# --------------------------------------------------------------------------- + +log "Authenticating test user with Minder (triggers auto-enrollment)..." +${MINDER_BINARY} auth offline-token use --file "${OFFLINE_TOKEN_OUTPUT_PATH}" \ + || fatal "Failed to authenticate with Minder using offline token" + +# --------------------------------------------------------------------------- +# Step 7: Extract root project ID +# --------------------------------------------------------------------------- + +log "Retrieving root project ID..." +PROJECT_LIST=$(${MINDER_BINARY} project list -o json 2>/dev/null) \ + || fatal "Failed to list Minder projects" + +PROJECT_ID=$(echo "${PROJECT_LIST}" | jq -r '.projects[0].projectId // empty') +if [ -z "${PROJECT_ID}" ]; then + fatal "No projects found. Auto-enrollment may have failed." +fi + +log "Root project ID: ${PROJECT_ID}" + +# Export for downstream use (e.g., GitHub Actions, Taskfile) +export MINDER_PROJECT="${PROJECT_ID}" + +# If running in GitHub Actions, persist to $GITHUB_ENV +if [ -n "${GITHUB_ENV:-}" ]; then + echo "MINDER_PROJECT=${PROJECT_ID}" >> "${GITHUB_ENV}" + log "MINDER_PROJECT exported to GITHUB_ENV" +fi + +# Also write to a file for non-GHA consumers +echo "${PROJECT_ID}" > ./minder-project-id +log "Project ID written to ./minder-project-id" + +# --------------------------------------------------------------------------- +# Done +# --------------------------------------------------------------------------- + +log "============================================" +log " Bootstrap complete!" +log " User: ${TEST_USER}" +log " Token: ${OFFLINE_TOKEN_OUTPUT_PATH}" +log " Project: ${PROJECT_ID}" +log "============================================" diff --git a/smoke-test-config-docker.yaml b/smoke-test-config-docker.yaml new file mode 100644 index 0000000..a21e381 --- /dev/null +++ b/smoke-test-config-docker.yaml @@ -0,0 +1,22 @@ +# smoke-test-config-docker.yaml +# +# Minder CLI configuration for when the test runner runs INSIDE the +# Docker Compose network (via docker-compose.test.yml). +# Uses Docker service names instead of localhost. + +http_server: + host: minder + port: 8080 + +grpc_server: + host: minder + port: 8090 + insecure: true + +identity: + cli: + # The issuer_url uses 'keycloak' (Docker service name) because the + # token exchange happens inside the Docker network. + issuer_url: http://keycloak:8080 + realm: stacklok + client_id: minder-cli diff --git a/smoke-test-config.yaml b/smoke-test-config.yaml new file mode 100644 index 0000000..c6ca212 --- /dev/null +++ b/smoke-test-config.yaml @@ -0,0 +1,25 @@ +# smoke-test-config.yaml +# +# Minder CLI configuration for running smoke tests against a local +# Docker Compose Minder stack. When the test runner is INSIDE the +# Docker network (via docker-compose.test.yml), use the service names +# (minder, keycloak). When running from the HOST, use localhost with +# the mapped ports. +# +# This file is the "inside Docker" variant. For host-side use, see +# the README instructions to override with localhost values. + +http_server: + host: localhost + port: 8080 + +grpc_server: + host: localhost + port: 8090 + insecure: true + +identity: + cli: + issuer_url: http://localhost:8081 + realm: stacklok + client_id: minder-cli