This directory contains integration tests that exercise JiraPS against a real Jira instance.
Integration tests verify that JiraPS functions work correctly with actual Jira APIs. Unlike unit tests that mock API calls, integration tests make real HTTP requests and validate real responses.
The integration suite has two deployment targets:
| Track | Target | Auth | Trigger |
|---|---|---|---|
| Cloud | A live Jira Cloud instance configured via JIRA_CLOUD_* secrets |
API token + email | Smoke gated per-PR + push by .github/workflows/ci.yml; full suite by .github/workflows/integration_tests.yml (scheduled + manual) |
| Server | A Dockerized Jira Data Center instance (moveworkforward/atlas-run-standalone:jira-11 — Atlassian Plugin SDK 9.6.0 + Jira Software 11.0.1, defined in docker-compose.yml) booted on demand |
Basic auth (admin/admin) |
server_integration_tests job in .github/workflows/integration_tests.yml (scheduled + manual workflow_dispatch only — the ~25 min cold-boot cost is too expensive to gate every PR on) |
Local prerequisite — Docker memory. The container asks for a 4 GiB Java heap and needs ~1 GiB of base overhead, so allocate at least 6 GiB to Docker Desktop in Settings > Resources before running locally. CI runners (~7 GiB) have enough headroom out of the box.
Apple Silicon — switch to the native arm64 build. The default
jira-11tag is the multi-arch amd64 build that CI uses; on Apple Silicon setJIRA_IMAGE_TAG=jira-11-arm64in your.envto pull the native arm64 build (~3 GB compressed) and avoid QEMU emulation overhead. Both tags ship the same SDK + Jira version.Why this image and not
addono/jira-software-standalone.addono's bundled SDK 8.2.8 dies at boot fetching the retiredmarketplace.atlassian.comendpoint;moveworkforward's SDK 9.6.0 resolves dependencies from the still-livepackages.atlassian.comand boots end-to-end (same wallpycontribs/jirahit).
The CI_JIRA_TYPE environment variable selects the track.
Setting CI_JIRA_TYPE=Server switches Initialize-IntegrationEnvironment, Connect-JiraTestServer, and the TestIntegration build task to the Server-track configuration; the default (Cloud) preserves existing behaviour.
Tests route via Pester tags on each Describe block:
'Integration', 'Server', 'Cloud'— runs on both tracks (the default for new tests)'Integration', 'Cloud'— Cloud-only (ADF v3,accountId-shaped fixtures,/rest/api/3/*endpoints)'Integration', 'Server'— Server-only (DC-specific identity model, basic-auth-only flows)
Inside a test, branch on the env config rather than hard-coding identity:
$userIdParam = @{ ($env.UserIdProperty) = $userIdValue }
Get-JiraUser @userIdParam$env.IsCloud ($true / $false) and $env.UserIdProperty ('accountId' / 'name') are both surfaced by Initialize-IntegrationEnvironment.
The Server track is fully self-contained — no secrets, no live Jira, just Docker:
# Start Jira, run the Server-tagged suite, and stop the container.
Invoke-Build -Task TestIntegrationServerFor manual loops where you want to keep the container running between test runs:
Invoke-Build -Task StartJiraDocker # ~5 min on first run while image pulls + Jira boots
# Full Server-tagged suite — the same set of files the server_integration_tests job runs.
Invoke-Build -Task TestIntegration -Tag 'Server'
Invoke-Build -Task StopJiraDockerStartJiraDocker sets the Server-track environment defaults (CI_JIRA_TYPE=Server, local Jira URL, admin/user credentials, and JIRA_TEST_PROJECT=TEST), clears Cloud-only optional fixture vars, runs docker compose up -d against the repo-root docker-compose.yml, and then invokes Tools/Wait-JiraServer.ps1 to poll until Jira is reachable, provision the regular test user (jira_user/jira), discover and provision the fixture project (TEST), and seed one baseline issue.
StopJiraDocker runs docker compose down -v to discard the container and its volumes.
Wait-JiraServer.ps1 writes provisioned fixture values into the current process and, in GitHub Actions, into $GITHUB_ENV for downstream steps.
Server runs intentionally ignore Cloud fixture values such as JIRA_TEST_ISSUE, JIRA_TEST_FILTER, and JIRA_TEST_VERSION from local .env files so a Cloud test profile cannot point the Docker suite at non-existent Server data.
If TestIntegrationServer fails before teardown, it captures jira-container.log for post-mortem diagnostics.
| Workflow / Job | Trigger | Notes |
|---|---|---|
ci.yml → smoke_tests (Cloud) |
every PR + every push to master |
Skipped on fork / Dependabot PRs (no secrets); gates continuous delivery via the CI Result aggregator |
integration_tests.yml → cloud_integration_tests |
0 5 * * 0 + manual workflow_dispatch |
Full Cloud suite — weekly + manual only; PR-level coverage is the smoke job above |
integration_tests.yml → server_integration_tests |
0 5 * * 0 + manual workflow_dispatch |
Never on PRs — ~25 min cold boot is too expensive to gate every PR on; PR-level Server coverage comes from the Server-tagged unit tests in ci.yml. Jira boot dominates wall time |
- Jira Cloud Instance: You need access to a Jira Cloud instance for testing
- API Token: Generate an API token from Atlassian Account Settings
- Test Project: A dedicated project for running tests (recommended)
- Test Fixtures: Pre-existing test data (issue, user, group, etc.)
Copy the example environment file and configure your credentials:
Copy-Item .env.example .envEdit .env with your Jira Cloud connection details:
JIRA_CLOUD_URL=https://your-instance.atlassian.net/
JIRA_CLOUD_USERNAME=your-email@example.com
JIRA_CLOUD_PASSWORD=your-api-token
JIRA_TEST_PROJECT=TV
JIRA_TEST_ISSUE=TV-1
JIRA_TEST_USER=557058:12345678-...
JIRA_TEST_GROUP=jira-users
Ensure the following exist in your Jira instance:
| Fixture | Description |
|---|---|
| Test Project | A project dedicated to testing (e.g., TV) |
| Test Issue | A permanent issue for read tests (e.g., TV-1) |
| Test User | Your account ID for user tests |
| Test Group | A group for membership tests |
Run a quick verification:
. ./Tests/Helpers/IntegrationTestTools.ps1
$env = Initialize-IntegrationEnvironment
$session = Connect-JiraTestServer -Environment $env
Get-JiraServerInformationIntegration tests can run directly against source code without building. This is faster and simpler for development.
The easiest way to run integration tests is through the build system:
# Run all integration tests in parallel
Invoke-Build -Task TestIntegration
# Run smoke tests only
Invoke-Build -Task TestIntegration -Tag 'Smoke'
# Increase parallelism (default is 4)
Invoke-Build -Task TestIntegration -ThrottleLimit 8
# More verbose output
Invoke-Build -Task TestIntegration -PesterVerbosity DetailedThis runs 4 test files concurrently by default, reducing total time from ~10 minutes to ~3.5 minutes.
For more control, use the parallel runner script directly:
./Tests/Invoke-ParallelPester.ps1 -ThrottleLimit 4
./Tests/Invoke-ParallelPester.ps1 -Tag 'Smoke' -Output Detailed$config = New-PesterConfiguration
$config.Run.Path = './Tests/Integration/'
$config.Filter.Tag = @('Integration')
$config.Output.Verbosity = 'Detailed'
Invoke-Pester -Configuration $configSmoke tests cover:
- Authentication (New-JiraSession, Get-JiraSession)
- Server connectivity (Get-JiraServerInformation)
- Issue retrieval (Get-JiraIssue)
- Issue creation (New-JiraIssue)
- JQL search (Get-JiraIssue -Query)
Use smoke tests for:
- Quick pre-commit validation
- CI pipeline health checks
- Verifying environment setup
Invoke-Pester ./Tests/Integration/Get-JiraIssue.Integration.Tests.ps1 -Output DetailedIf you need to test against the built module:
Invoke-Build -Task Build
Invoke-Build -Task Test -Tag 'Integration'The Invoke-ParallelPester.ps1 script uses PowerShell 7 thread jobs to run test files concurrently.
Its console summary always includes the skipped-test count; when any tests skip, it also prints a SKIPPED TESTS section with each test name and Pester skip reason so a green integration run cannot hide lost coverage.
# Run all integration tests with 4 concurrent files
./Tests/Invoke-ParallelPester.ps1 -ThrottleLimit 4
# Run only smoke tests
./Tests/Invoke-ParallelPester.ps1 -Tag 'Smoke' -ThrottleLimit 6
# Run specific files
./Tests/Invoke-ParallelPester.ps1 -Path @(
'./Tests/Integration/Authentication.Integration.Tests.ps1',
'./Tests/Integration/Get-JiraIssue.Integration.Tests.ps1'
)
# Minimal output
./Tests/Invoke-ParallelPester.ps1 -Output Normal| Parameter | Description | Default |
|---|---|---|
-Path |
Directory or file paths | ./Tests/Integration/ |
-ThrottleLimit |
Max concurrent tests | 4 |
-Tag |
Filter by tag | (none) |
-ExcludeTag |
Exclude by tag | (none) |
-Output |
Verbosity level | Normal |
| Mode | Time | Notes |
|---|---|---|
| Sequential | ~10+ min | Single file at a time |
| Parallel (4) | ~3.5 min | 3x faster |
| Parallel (6) | ~2.5 min | Diminishing returns |
Note: Requires PowerShell 7+ for parallel thread-job execution.
Integration tests follow the same Pester 6 patterns as unit tests:
#requires -modules @{ ModuleName = "Pester"; ModuleVersion = "6.2.0"; MaximumVersion = "6.999" }
BeforeDiscovery {
. "$PSScriptRoot/../Helpers/TestTools.ps1"
. "$PSScriptRoot/../Helpers/IntegrationTestTools.ps1"
Initialize-TestEnvironment
$script:moduleToTest = Resolve-ModuleSource
Import-Module $script:moduleToTest -Force -ErrorAction Stop
# Skip if environment not configured
$script:Skip = Skip-IntegrationTest
}
InModuleScope JiraPS {
Describe "Get-JiraIssue" -Tag 'Integration' -Skip:$Skip {
BeforeAll {
. "$PSScriptRoot/../Helpers/IntegrationTestTools.ps1"
$script:env = Initialize-IntegrationEnvironment
$script:session = Connect-JiraTestServer -Environment $env
$script:fixtures = Get-TestFixture -Environment $env
}
AfterAll {
Remove-JiraSession -ErrorAction SilentlyContinue
}
Context "Issue Retrieval" {
It "retrieves an issue by key" {
$issue = Get-JiraIssue -Key $fixtures.TestIssue
$issue | Should -Not -BeNullOrEmpty
$issue.Key | Should -Be $fixtures.TestIssue
}
}
}
}The IntegrationTestTools.ps1 module provides:
| Function | Description |
|---|---|
Initialize-IntegrationEnvironment |
Loads .env and returns config object |
Connect-JiraTestServer |
Establishes authenticated session |
Get-TestFixture |
Returns hashtable of test fixture references |
Skip-IntegrationTest |
Returns $true if environment not configured |
New-TemporaryTestIssue |
Creates a temporary issue for write tests |
New-TestResourceName |
Generates prefixed name like JiraPS-IntTest-Issue-20260412... |
Get-TestResourcePrefix |
Returns the prefix used for test resources |
Remove-StaleTestResource |
Cleans up resources from failed test runs |
Integration tests create real resources in Jira. Here's how we ensure cleanup happens even when tests fail:
All test resources use a discoverable prefix (JiraPS-IntTest-):
$summary = New-TestResourceName -Type "Issue"
# Returns: "JiraPS-IntTest-Issue-20260412233045-a1b2c3"Use ArrayList (not arrays) initialized in BeforeAll:
BeforeAll {
# Initialize BEFORE any test might fail
$script:createdIssues = [System.Collections.ArrayList]::new()
}
It "creates an issue" {
$issue = New-JiraIssue ...
$null = $script:createdIssues.Add($issue.Key) # $null suppresses index output
}Remove stale resources from previous failed runs:
BeforeAll {
# This finds and deletes old JiraPS-IntTest-* resources
Remove-StaleTestResource -Fixtures $fixtures
}Handle null/empty arrays gracefully:
AfterAll {
if ($script:createdIssues -and $script:createdIssues.Count -gt 0) {
foreach ($key in $script:createdIssues) {
try {
Remove-JiraIssue -IssueId $key -Force -ErrorAction SilentlyContinue
}
catch { <# ignore cleanup failures #> }
}
}
}If tests leave behind resources, clean them manually:
# Find stale resources via JQL
Get-JiraIssue -Query "project = TV AND summary ~ 'JiraPS-IntTest-'"
# Or run cleanup helper
. ./Tests/Helpers/IntegrationTestTools.ps1
$env = Initialize-IntegrationEnvironment
Connect-JiraTestServer -Environment $env
Remove-StaleTestResource -MaxAge (New-TimeSpan -Minutes 5)For tests that create, update, or delete resources:
Context "Write Operations" -Skip:($fixtures.ReadOnly) {
BeforeAll {
$script:tempIssue = New-TemporaryTestIssue -Summary "Test $(Get-Date -Format 'yyyyMMddHHmmss')"
}
AfterAll {
if ($tempIssue) {
Remove-JiraIssue -IssueId $tempIssue.Key -Force -ErrorAction SilentlyContinue
}
}
It "updates an issue" {
Set-JiraIssue -Issue $tempIssue.Key -Summary "Updated"
# ...
}
}Cloud smoke runs as part of the standard .github/workflows/ci.yml pipeline; the full Cloud suite and the full Server (Data Center, Dockerized) suite share .github/workflows/integration_tests.yml, which runs weekly and on manual dispatch.
| Trigger | Smoke (ci.yml) |
Cloud full (integration_tests.yml) |
Server full (integration_tests.yml) |
|---|---|---|---|
| Pull Request (first-party) | ✅ | ❌ — use workflow_dispatch to opt in |
❌ — use workflow_dispatch to opt in |
| Pull Request (fork / Dependabot) | ⚪ skipped (no secrets) | ❌ | ❌ |
Push to master |
✅ | ❌ | ❌ |
| Weekly (Sunday at 5 AM UTC) | — | ✅ | ✅ |
Manual (workflow_dispatch) |
re-run via Actions UI | ✅ — pass track=cloud (or both) |
✅ — pass track=server (or both) |
Configure these in your repository settings:
| Secret | Required | Description |
|---|---|---|
JIRA_CLOUD_URL |
✅ | Jira Cloud instance URL |
JIRA_CLOUD_USERNAME |
✅ | Email address |
JIRA_CLOUD_PASSWORD |
✅ | API token |
JIRA_TEST_PROJECT |
✅ | Project key (e.g., TV) |
JIRA_TEST_ISSUE |
✅ | Existing issue key (e.g., TV-1) |
JIRA_TEST_USER |
⚪ | Account ID for user tests |
JIRA_TEST_GROUP |
⚪ | Group name for group tests |
JIRA_TEST_FILTER |
⚪ | Filter ID for filter tests |
JIRA_TEST_VERSION |
⚪ | Version name for version tests |
If secrets are not configured, integration tests are skipped automatically.
To run the full suite against an in-flight PR, dispatch the workflow manually:
- Open the Actions tab → Integration Tests workflow.
- Click Run workflow, pick the branch, and choose the
trackinput (cloud,server, orboth; defaultboth). - Inspect the run in the same workflow's history — results upload as the
Cloud-Integration-Testsand/orServer-Integration-Testsartifacts.
For first-party PR branches (those on AtlassianPS/JiraPS itself), the branch appears in the Run workflow dropdown directly. For fork PRs, the dropdown only sees branches on AtlassianPS/JiraPS, so a maintainer needs to either:
- Cherry-pick (or push) the fork's commits onto a maintainer-owned branch first and dispatch from there, or
- Run
gh workflow run "Integration Tests" --ref <branch> -f track=<cloud|server|both>from a clone that has a remote pointing at the fork (the dispatched run still usesAtlassianPS/JiraPS's secrets, not the fork's).
- No build required: tests run directly against source for speed.
- Parallel execution: Cloud uses
ThrottleLimit=6; Server usesThrottleLimit=2(halved because the AMPS/H2 backend serialises Lucene write commits — see the inline comment on theserver_integration_testsjob for the contention details). - Concurrency control: weekly + dispatched runs share one concurrency group with
cancel-in-progress: false, so an in-flight run is never killed by a dispatch retry. - NUnit results artifacts:
Cloud-Integration-TestsandServer-Integration-Tests(each containingTest-Integration.xml) are retained for 14 days. The Server job retainsServer-Jira-Container-Logsfor 7 days after failures, manual runs withdebugenabled, or runs with GitHub runner debug logging enabled.
- Ensure
.envfile exists in project root - Check all required variables are set
- Run
Initialize-IntegrationEnvironmentmanually to see which variables are missing
- Verify API token is valid (not expired)
- Check username is your email address (not username)
- Ensure the URL includes trailing slash
- Verify your account has access to the test project
- Check group membership for group tests
- Ensure you have permission to create/edit issues for write tests
- Copy
.template.ps1to<FunctionName>.Integration.Tests.ps1 - Update the function name in
Describe - Add appropriate test contexts and assertions
- Run the test locally before committing
See existing tests for examples of common patterns.