fnox 1.23.0

A flexible secret management tool supporting multiple providers and encryption methods
Documentation
#!/usr/bin/env bats
#
# GitHub App Lease Backend Tests
#
# These tests verify the GitHub App installation token lease backend.
# They use a mock HTTP server to avoid requiring real GitHub credentials.

setup() {
	load 'test_helper/common_setup'
	_common_setup
}

teardown() {
	# Kill mock server if running
	if [[ -n ${MOCK_PID:-} ]]; then
		kill "$MOCK_PID" 2>/dev/null || true
		wait "$MOCK_PID" 2>/dev/null || true
	fi
	_common_teardown
}

# Helper: generate a test RSA private key
generate_test_key() {
	openssl genrsa 2048 2>/dev/null >"$TEST_TEMP_DIR/test-app.pem"
}

# Helper: start a mock GitHub API server that returns a valid token response.
# Binds to an OS-assigned free port and exports MOCK_PORT.
start_mock_github_api() {
	local token="${1:-ghs_mock_installation_token_abc123}"
	local expires_at="${2:-2099-01-01T00:00:00Z}"

	cat >"$TEST_TEMP_DIR/mock_github.py" <<PYEOF
import http.server, json, sys, socket

class Handler(http.server.BaseHTTPRequestHandler):
    def do_POST(self):
        body = json.dumps({
            "token": "$token",
            "expires_at": "$expires_at",
            "permissions": {"contents": "read"}
        })
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body.encode())
    def do_DELETE(self):
        # DELETE /installation/token — revocation endpoint
        self.send_response(204)
        self.end_headers()
    def log_message(self, format, *args):
        pass

# Bind to port 0 to let the OS pick a free port
server = http.server.HTTPServer(("127.0.0.1", 0), Handler)
port = server.server_address[1]
# Write the assigned port so the test can read it
with open("$TEST_TEMP_DIR/mock_port", "w") as f:
    f.write(str(port))
server.serve_forever()
PYEOF

	python3 "$TEST_TEMP_DIR/mock_github.py" &
	MOCK_PID=$!
	# Wait for the port file to appear
	for _ in $(seq 1 30); do
		if [[ -f "$TEST_TEMP_DIR/mock_port" ]]; then
			break
		fi
		sleep 0.1
	done
	MOCK_PORT=$(cat "$TEST_TEMP_DIR/mock_port")
	export MOCK_PORT
}

@test "github-app: creates installation token with private key file" {
	generate_test_key
	start_mock_github_api

	cat >"$FNOX_CONFIG_FILE" <<EOF
[leases.github]
type = "github-app"
app_id = "12345"
installation_id = "67890"
private_key_file = "$TEST_TEMP_DIR/test-app.pem"
api_base = "http://127.0.0.1:$MOCK_PORT"
EOF

	run fnox lease create github
	assert_success
	assert_output --partial "github"
}

@test "github-app: creates installation token via env var" {
	generate_test_key
	start_mock_github_api

	export FNOX_GITHUB_APP_PRIVATE_KEY
	FNOX_GITHUB_APP_PRIVATE_KEY="$(cat "$TEST_TEMP_DIR/test-app.pem")"

	cat >"$FNOX_CONFIG_FILE" <<EOF
[leases.github]
type = "github-app"
app_id = "12345"
installation_id = "67890"
api_base = "http://127.0.0.1:$MOCK_PORT"
EOF

	run fnox lease create github
	assert_success
}

@test "github-app: token is available via fnox exec" {
	generate_test_key
	start_mock_github_api

	cat >"$FNOX_CONFIG_FILE" <<EOF
[leases.github]
type = "github-app"
app_id = "12345"
installation_id = "67890"
private_key_file = "$TEST_TEMP_DIR/test-app.pem"
api_base = "http://127.0.0.1:$MOCK_PORT"
EOF

	run fnox exec -- printenv GITHUB_TOKEN
	assert_success
	assert_line "ghs_mock_installation_token_abc123"
}

@test "github-app: custom env_var" {
	generate_test_key
	start_mock_github_api

	cat >"$FNOX_CONFIG_FILE" <<EOF
[leases.github]
type = "github-app"
app_id = "12345"
installation_id = "67890"
private_key_file = "$TEST_TEMP_DIR/test-app.pem"
env_var = "GH_TOKEN"
api_base = "http://127.0.0.1:$MOCK_PORT"
EOF

	run fnox exec -- printenv GH_TOKEN
	assert_success
	assert_line "ghs_mock_installation_token_abc123"
}

@test "github-app: lease is recorded in ledger after create" {
	generate_test_key
	start_mock_github_api

	cat >"$FNOX_CONFIG_FILE" <<EOF
[leases.github]
type = "github-app"
app_id = "12345"
installation_id = "67890"
private_key_file = "$TEST_TEMP_DIR/test-app.pem"
api_base = "http://127.0.0.1:$MOCK_PORT"
EOF

	run fnox lease create github
	assert_success

	# Verify the lease shows up as active
	run fnox lease list --active
	assert_success
	assert_output --partial "github"
}

@test "github-app: fails with missing private key" {
	cat >"$FNOX_CONFIG_FILE" <<EOF
[leases.github]
type = "github-app"
app_id = "12345"
installation_id = "67890"
private_key_file = "$TEST_TEMP_DIR/nonexistent.pem"
EOF

	run fnox lease create github
	assert_failure
	assert_output --partial "private key"
}

@test "github-app: supports permissions config" {
	generate_test_key
	start_mock_github_api

	cat >"$FNOX_CONFIG_FILE" <<EOF
[leases.github]
type = "github-app"
app_id = "12345"
installation_id = "67890"
private_key_file = "$TEST_TEMP_DIR/test-app.pem"
api_base = "http://127.0.0.1:$MOCK_PORT"

[leases.github.permissions]
contents = "read"
pull_requests = "write"
EOF

	run fnox lease create github
	assert_success
}

@test "github-app: supports repositories config" {
	generate_test_key
	start_mock_github_api

	cat >"$FNOX_CONFIG_FILE" <<EOF
[leases.github]
type = "github-app"
app_id = "12345"
installation_id = "67890"
private_key_file = "$TEST_TEMP_DIR/test-app.pem"
api_base = "http://127.0.0.1:$MOCK_PORT"
repositories = ["my-repo"]
EOF

	run fnox lease create github
	assert_success
}

@test "github-app: lease list shows created lease" {
	generate_test_key
	start_mock_github_api

	cat >"$FNOX_CONFIG_FILE" <<EOF
[leases.github]
type = "github-app"
app_id = "12345"
installation_id = "67890"
private_key_file = "$TEST_TEMP_DIR/test-app.pem"
api_base = "http://127.0.0.1:$MOCK_PORT"
EOF

	run fnox lease create github
	assert_success

	run fnox lease list
	assert_success
	assert_output --partial "github"
}

@test "github-app: lease revoke calls DELETE /installation/token" {
	generate_test_key
	start_mock_github_api

	cat >"$FNOX_CONFIG_FILE" <<EOF
[leases.github]
type = "github-app"
app_id = "12345"
installation_id = "67890"
private_key_file = "$TEST_TEMP_DIR/test-app.pem"
api_base = "http://127.0.0.1:$MOCK_PORT"
EOF

	# Create the lease
	run fnox lease create github
	assert_success

	# Read the ledger file directly to get the full (non-truncated) lease ID
	local lease_id
	lease_id=$(grep 'lease_id' "$HOME"/.local/state/fnox/leases/*.toml | head -1 | sed 's/.*= *"//;s/".*//')

	# Revoke it (mock server returns 204 for DELETE)
	run fnox lease revoke "$lease_id"
	assert_success
	assert_output --partial "revoked"
}