Skip to content

Integration Tests

Integration Tests #503

Workflow file for this run

# Integration Tests
# =================
# Test PowerNetbox against real Netbox instances via Docker
#
# This workflow:
# - Spins up Netbox via Docker Compose
# - Runs integration tests against the live API
# - Tests multiple Netbox versions in parallel (matrix)
# - Only runs on ubuntu (Docker required)
#
# Triggers:
# - Push to dev/main/beta branches (post-merge validation)
# - Manual dispatch with custom Netbox version
# - Weekly scheduled run (catch upstream changes)
#
# NOTE: Intentionally not triggered on pull_request. Integration tests
# take ~5 min (mostly Docker/Netbox startup) and are not required checks.
# Unit tests (2350+) provide sufficient pre-merge coverage. Integration
# runs post-merge on push to catch any regressions immediately.
name: Integration Tests
on:
push:
branches: [dev, main, beta]
# Allow manual trigger with custom version
workflow_dispatch:
inputs:
netbox_version:
description: 'Netbox Docker image tag (e.g., v4.5.8-4.0.2)'
required: false
default: 'v4.5.8-4.0.2'
type: string
# Run weekly to catch upstream Netbox changes
schedule:
- cron: '0 6 * * 1' # Every Monday at 6 AM UTC
env:
# Default API token (matches docker-compose.ci.yml)
NETBOX_TOKEN: "0123456789abcdef0123456789abcdef01234567"
# Docker Compose project name (for predictable container names)
COMPOSE_PROJECT_NAME: "powernetbox"
permissions:
contents: read
jobs:
# ============================================================
# Integration Tests - Matrix of Netbox versions
# ============================================================
integration:
name: Netbox ${{ matrix.netbox_short }}
runs-on: ubuntu-latest
# Don't fail entire workflow if experimental version fails
continue-on-error: ${{ matrix.experimental }}
strategy:
fail-fast: false
matrix:
include:
# ----------------------------------------------------------
# Minimum supported version
# ----------------------------------------------------------
- netbox: "v4.3.7-3.3.0"
netbox_short: "4.3.7"
experimental: false
# ----------------------------------------------------------
# Stable version - MUST pass (production target)
# ----------------------------------------------------------
- netbox: "v4.4.10-3.4.2"
netbox_short: "4.4.10"
experimental: false
# ----------------------------------------------------------
# Latest version - Netbox 4.5.8 (netbox-docker 4.0.2)
# ----------------------------------------------------------
- netbox: "v4.5.8-4.0.2"
netbox_short: "4.5.8"
experimental: false
env:
NETBOX_VERSION: ${{ github.event.inputs.netbox_version || matrix.netbox }}
NETBOX_HOST: "localhost:8000"
steps:
# ----------------------------------------------------------
# Setup
# ----------------------------------------------------------
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
# ----------------------------------------------------------
# Start Netbox Stack
# ----------------------------------------------------------
- name: Pull Docker images
run: |
echo "Pulling images for Netbox ${{ env.NETBOX_VERSION }}..."
docker compose -f docker-compose.ci.yml pull
- name: Start Netbox stack
run: |
echo "Starting Netbox stack..."
docker compose -f docker-compose.ci.yml up -d
echo ""
echo "Container status:"
docker compose -f docker-compose.ci.yml ps
- name: Wait for Netbox to be ready
run: |
echo "Waiting for Netbox to be ready (this may take 5-6 minutes for Netbox 4.5+ migrations)..."
echo ""
MAX_WAIT=420 # 7 minutes max (4.5.0 has many migrations)
ELAPSED=0
INTERVAL=10
while [ $ELAPSED -lt $MAX_WAIT ]; do
# Check container health status
STATUS=$(docker inspect --format='{{.State.Health.Status}}' powernetbox-netbox-1 2>/dev/null || echo "starting")
echo "[${ELAPSED}s] Netbox container status: $STATUS"
if [ "$STATUS" = "healthy" ]; then
echo ""
echo "Netbox is healthy! Verifying web server..."
# Double-check web server is responding (use /login/ - no auth required)
if curl -sf http://localhost:8000/login/ > /dev/null 2>&1; then
echo "Netbox web server is responding!"
exit 0
fi
fi
if [ "$STATUS" = "unhealthy" ]; then
echo ""
echo "ERROR: Container became unhealthy!"
echo ""
echo "=== Container Logs ==="
docker compose -f docker-compose.ci.yml logs netbox --tail 100
exit 1
fi
sleep $INTERVAL
ELAPSED=$((ELAPSED + INTERVAL))
done
echo ""
echo "ERROR: Timeout waiting for Netbox to be ready after ${MAX_WAIT}s"
echo ""
echo "=== Container Status ==="
docker compose -f docker-compose.ci.yml ps
echo ""
echo "=== Netbox Logs ==="
docker compose -f docker-compose.ci.yml logs netbox --tail 100
exit 1
- name: Display Netbox version info
run: |
echo "=== Netbox Status API ==="
curl -s http://localhost:8000/api/status/ | jq .
# Extract version for later use
VERSION=$(curl -s http://localhost:8000/api/status/ | jq -r '.["netbox-version"]')
echo ""
echo "Running Netbox version: $VERSION"
echo "NETBOX_RUNNING_VERSION=$VERSION" >> $GITHUB_ENV
# ----------------------------------------------------------
# Install PowerShell & Dependencies
# ----------------------------------------------------------
- name: Install PowerShell dependencies
shell: pwsh
run: |
Write-Host "Installing Pester..." -ForegroundColor Cyan
Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module Pester -Force -Scope CurrentUser -MinimumVersion 5.0.0
$pesterVersion = (Get-Module Pester -ListAvailable | Select-Object -First 1).Version
Write-Host "Pester version: $pesterVersion" -ForegroundColor Green
- name: Build PowerNetbox module
shell: pwsh
run: |
Write-Host "Building module..." -ForegroundColor Cyan
./deploy.ps1 -Environment dev -SkipVersion
Write-Host "Importing module..." -ForegroundColor Cyan
Import-Module ./PowerNetbox/PowerNetbox.psd1 -Force
$cmdCount = (Get-Command -Module PowerNetbox).Count
Write-Host "Module loaded with $cmdCount commands" -ForegroundColor Green
# ----------------------------------------------------------
# v2 Token Tests (Netbox 4.5+)
# ----------------------------------------------------------
- name: Test v2 token format support
if: startsWith(matrix.netbox_short, '4.5')
shell: pwsh
env:
NETBOX_HOST: ${{ env.NETBOX_HOST }}
NETBOX_TOKEN: ${{ env.NETBOX_TOKEN }}
NETBOX_RUNNING_VERSION: ${{ env.NETBOX_RUNNING_VERSION }}
run: |
Write-Host "============================================" -ForegroundColor Cyan
Write-Host " Testing v2 Token Support (Netbox 4.5+)" -ForegroundColor Cyan
Write-Host "============================================" -ForegroundColor Cyan
Write-Host ""
Import-Module ./PowerNetbox/PowerNetbox.psd1 -Force
# Check if Netbox is responding correctly
# The Docker image for 4.5-beta may have issues
if ($env:NETBOX_RUNNING_VERSION -eq 'null' -or [string]::IsNullOrEmpty($env:NETBOX_RUNNING_VERSION)) {
Write-Host "WARNING: Netbox version detection returned null" -ForegroundColor Yellow
Write-Host " This indicates the Docker image may not be fully functional." -ForegroundColor Yellow
Write-Host " Skipping token auth tests (known 4.5-beta Docker image issue)." -ForegroundColor Yellow
Write-Host ""
# Only run regex pattern tests (no API calls)
Write-Host "Running v2 token format detection tests only..." -ForegroundColor Cyan
}
# v2 token format: nbt_<KEY>.<SECRET>
$v2Pattern = '^nbt_[A-Za-z0-9]+\.[A-Za-z0-9]+$'
$isV2Token = $env:NETBOX_TOKEN -match $v2Pattern
$netboxWorking = $env:NETBOX_RUNNING_VERSION -ne 'null' -and -not [string]::IsNullOrEmpty($env:NETBOX_RUNNING_VERSION)
# Test 1: Bearer header (only works with v2 tokens and working Netbox)
if (-not $netboxWorking) {
Write-Host "Test 1: SKIPPED - Netbox not responding correctly" -ForegroundColor Yellow
} elseif ($isV2Token) {
Write-Host "Test 1: Testing Bearer header with v2 token..." -ForegroundColor Yellow
$headers = @{
'Authorization' = "Bearer $env:NETBOX_TOKEN"
'Accept' = 'application/json'
}
try {
$response = Invoke-RestMethod -Uri "http://$env:NETBOX_HOST/api/status/" -Headers $headers
Write-Host " PASS: Bearer header works with v2 token" -ForegroundColor Green
Write-Host " Version: $($response.'netbox-version')" -ForegroundColor Gray
} catch {
Write-Host " FAIL: Bearer header failed: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
} else {
Write-Host "Test 1: SKIPPED - Bearer auth requires v2 token (nbt_xxx.yyy format)" -ForegroundColor Yellow
Write-Host " Current token is v1 format (40-char hex)" -ForegroundColor Gray
}
# Test 2: Token header should work with both v1 and v2 tokens (backwards compat)
if (-not $netboxWorking) {
Write-Host "Test 2: SKIPPED - Netbox not responding correctly" -ForegroundColor Yellow
} else {
Write-Host "Test 2: Testing Token header (backwards compat)..." -ForegroundColor Yellow
$headers = @{
'Authorization' = "Token $env:NETBOX_TOKEN"
'Accept' = 'application/json'
}
try {
$response = Invoke-RestMethod -Uri "http://$env:NETBOX_HOST/api/status/" -Headers $headers
Write-Host " PASS: Token header works on Netbox 4.5+" -ForegroundColor Green
Write-Host " Version: $($response.'netbox-version')" -ForegroundColor Gray
} catch {
Write-Host " FAIL: Token header failed: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
}
# Test 3: v2 token format detection (regex pattern matching - no API needed)
Write-Host "Test 3: Testing v2 token format detection..." -ForegroundColor Yellow
$v2Token = "nbt_ExampleKey123.ExampleSecretValue456789"
$v1Token = "0123456789abcdef0123456789abcdef01234567"
if ($v2Token -match $v2Pattern) {
Write-Host " PASS: v2 token format correctly identified" -ForegroundColor Green
} else {
Write-Host " FAIL: v2 token format not recognized" -ForegroundColor Red
exit 1
}
if ($v1Token -notmatch $v2Pattern) {
Write-Host " PASS: v1 token correctly not matched as v2" -ForegroundColor Green
} else {
Write-Host " FAIL: v1 token incorrectly matched as v2" -ForegroundColor Red
exit 1
}
Write-Host ""
Write-Host "All v2 token tests passed!" -ForegroundColor Green
# ----------------------------------------------------------
# Setup API Token (v1 or v2 depending on Netbox version)
# ----------------------------------------------------------
- name: Setup API Token
id: token
run: |
set +e # Don't exit on error - we handle errors ourselves
echo "Testing v1 token authentication..."
V1_TOKEN="0123456789abcdef0123456789abcdef01234567"
# Test if v1 token works
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Token $V1_TOKEN" http://localhost:8000/api/status/)
if [ "$RESPONSE" = "200" ]; then
echo "V1 token works - using legacy token"
echo "token=$V1_TOKEN" >> $GITHUB_OUTPUT
echo "token_type=v1" >> $GITHUB_OUTPUT
else
echo "V1 token failed (HTTP $RESPONSE) - creating v2 token for Netbox 4.5.0+"
# Create a v2 token using Netbox's Token model
echo "Running Django shell to create token..."
DJANGO_OUTPUT=$(docker exec powernetbox-netbox-1 /opt/netbox/venv/bin/python /opt/netbox/netbox/manage.py shell -c "
import sys
from users.models import Token
from django.contrib.auth import get_user_model
try:
User = get_user_model()
user = User.objects.filter(username='admin').first()
print(f'DEBUG: Found user: {user}', file=sys.stderr)
if not user:
user = User.objects.create_superuser('admin', 'admin@example.com', 'admin')
print(f'DEBUG: Created superuser', file=sys.stderr)
# Delete existing tokens
deleted = Token.objects.filter(user=user).delete()
print(f'DEBUG: Deleted tokens: {deleted}', file=sys.stderr)
# Create token instance
token = Token(user=user)
# Generate the plaintext secret
secret = Token.generate() # Static method
print(f'DEBUG: Generated secret: {secret[:10]}...', file=sys.stderr)
# Set via the 'token' property - this automatically:
# 1. Generates the key (token.key)
# 2. Calls update_digest() to compute HMAC
token.token = secret
print(f'DEBUG: Set token property, key={token.key}', file=sys.stderr)
# Save the token
token.save()
print(f'DEBUG: Token saved, id={token.id}, hmac_digest={token.hmac_digest[:10] if token.hmac_digest else None}...', file=sys.stderr)
# Output the full v2 token: nbt_{key}.{secret}
full_token = f'nbt_{token.key}.{secret}'
print(full_token)
print(f'DEBUG: Created v2 token successfully', file=sys.stderr)
except Exception as e:
print(f'ERROR: {type(e).__name__}: {e}', file=sys.stderr)
import traceback
traceback.print_exc()
" 2>&1)
echo "Django output:"
echo "$DJANGO_OUTPUT"
# Extract the token line
TOKEN_OUTPUT=$(echo "$DJANGO_OUTPUT" | grep -E '^nbt_' || true)
if [ -n "$TOKEN_OUTPUT" ] && [[ "$TOKEN_OUTPUT" == nbt_* ]]; then
echo "Created v2 token: ${TOKEN_OUTPUT:0:20}..."
echo "::add-mask::$TOKEN_OUTPUT"
echo "token=$TOKEN_OUTPUT" >> $GITHUB_OUTPUT
echo "token_type=v2" >> $GITHUB_OUTPUT
else
echo "WARNING: Failed to create v2 token. Using v1 fallback (tests may be skipped)."
echo "token=$V1_TOKEN" >> $GITHUB_OUTPUT
echo "token_type=v1-fallback" >> $GITHUB_OUTPUT
fi
fi
# ----------------------------------------------------------
# Verify API Auth Works
# ----------------------------------------------------------
- name: Verify API authentication
id: verify_api
shell: pwsh
env:
NETBOX_HOST: ${{ env.NETBOX_HOST }}
NETBOX_TOKEN: ${{ steps.token.outputs.token }}
TOKEN_TYPE: ${{ steps.token.outputs.token_type }}
run: |
Write-Host "Verifying API authentication..." -ForegroundColor Cyan
Write-Host "Token type: $env:TOKEN_TYPE" -ForegroundColor Gray
# Determine auth header based on token type
$authHeader = if ($env:NETBOX_TOKEN -match '^nbt_') {
"Bearer $env:NETBOX_TOKEN"
} else {
"Token $env:NETBOX_TOKEN"
}
$headers = @{
'Authorization' = $authHeader
'Accept' = 'application/json'
}
try {
$response = Invoke-RestMethod -Uri "http://$env:NETBOX_HOST/api/dcim/sites/?limit=1" -Headers $headers
Write-Host "API authentication successful!" -ForegroundColor Green
echo "api_working=true" >> $env:GITHUB_OUTPUT
} catch {
Write-Host "WARNING: API authentication failed: $($_.Exception.Message)" -ForegroundColor Yellow
Write-Host "This may be a Docker image issue. Integration tests will be skipped." -ForegroundColor Yellow
echo "api_working=false" >> $env:GITHUB_OUTPUT
}
# ----------------------------------------------------------
# Run Integration Tests
# ----------------------------------------------------------
- name: Run Integration tests
id: integration_tests
if: steps.verify_api.outputs.api_working == 'true'
shell: pwsh
env:
NETBOX_HOST: ${{ env.NETBOX_HOST }}
NETBOX_TOKEN: ${{ steps.token.outputs.token }}
NETBOX_VERSION: ${{ matrix.netbox_short }}
run: |
Write-Host "============================================" -ForegroundColor Cyan
Write-Host " Running Integration Tests" -ForegroundColor Cyan
Write-Host " Netbox: $env:NETBOX_VERSION" -ForegroundColor Cyan
Write-Host " Host: $env:NETBOX_HOST" -ForegroundColor Cyan
Write-Host "============================================" -ForegroundColor Cyan
Write-Host ""
$config = New-PesterConfiguration
$config.Run.Path = './Tests/Integration.Tests.ps1'
$config.Run.Exit = $true
$config.Filter.Tag = @('Integration')
$config.TestResult.Enabled = $true
$config.TestResult.OutputPath = 'IntegrationTestResults.xml'
$config.TestResult.OutputFormat = 'NUnitXml'
$config.Output.Verbosity = 'Detailed'
Invoke-Pester -Configuration $config
- name: Skip Integration tests (API not working)
if: steps.verify_api.outputs.api_working != 'true'
run: |
echo "::warning::Skipping integration tests - API authentication not working (likely Docker image issue)"
echo "This is expected for some beta/experimental Netbox versions."
# ----------------------------------------------------------
# Artifacts & Cleanup
# ----------------------------------------------------------
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: integration-results-${{ matrix.netbox_short }}
path: IntegrationTestResults.xml
retention-days: 30
- name: Collect debug logs on failure
if: failure()
run: |
echo "=== Collecting debug information ===" > debug-logs.txt
echo "" >> debug-logs.txt
echo "=== Docker Compose Status ===" >> debug-logs.txt
docker compose -f docker-compose.ci.yml ps >> debug-logs.txt 2>&1
echo "" >> debug-logs.txt
echo "=== Netbox Logs (last 200 lines) ===" >> debug-logs.txt
docker compose -f docker-compose.ci.yml logs netbox --tail 200 >> debug-logs.txt 2>&1
echo "" >> debug-logs.txt
echo "=== PostgreSQL Logs ===" >> debug-logs.txt
docker compose -f docker-compose.ci.yml logs postgres --tail 50 >> debug-logs.txt 2>&1
echo "" >> debug-logs.txt
echo "=== Redis Logs ===" >> debug-logs.txt
docker compose -f docker-compose.ci.yml logs redis --tail 50 >> debug-logs.txt 2>&1
- name: Upload debug logs on failure
uses: actions/upload-artifact@v4
if: failure()
with:
name: debug-logs-${{ matrix.netbox_short }}
path: debug-logs.txt
retention-days: 7
- name: Cleanup Docker resources
if: always()
run: |
echo "Stopping containers..."
docker compose -f docker-compose.ci.yml down -v --remove-orphans
echo "Pruning unused Docker resources..."
docker system prune -f --volumes
# ============================================================
# Summary Job - Report overall status
# ============================================================
summary:
name: Integration Summary
runs-on: ubuntu-latest
needs: integration
if: always()
steps:
- name: Check integration results
run: |
echo "Integration test result: ${{ needs.integration.result }}"
if [ "${{ needs.integration.result }}" = "failure" ]; then
echo "::error::One or more integration tests failed!"
exit 1
elif [ "${{ needs.integration.result }}" = "cancelled" ]; then
echo "::warning::Integration tests were cancelled"
exit 0
else
echo "::notice::All integration tests passed!"
fi