Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
fa46377
initial commit of kickstart:apply working
mark-robustelli May 20, 2026
03238a5
move to apply command
mark-robustelli May 21, 2026
315dba5
removing unused types
mark-robustelli May 21, 2026
93cec62
removing files for move
mark-robustelli May 21, 2026
3b9531b
adding untracked files
mark-robustelli May 21, 2026
8b05740
removing unused type
mark-robustelli May 21, 2026
fa2f189
fixing line number tracking
mark-robustelli May 21, 2026
bb3ddd2
updating line-tracker
mark-robustelli May 22, 2026
f3f3811
moving helpers to utilities
mark-robustelli May 22, 2026
4023af4
Merge remote-tracking branch 'origin/main' into mcr/kickstart-apply
mark-robustelli May 28, 2026
9777886
updating prompt inputs
mark-robustelli May 28, 2026
069019b
adding email template file inclusion
mark-robustelli May 28, 2026
c2e8bea
adding unit tests
mark-robustelli Jun 2, 2026
e3b4f9a
adding test folders
mark-robustelli Jun 2, 2026
db499ac
adding tests
mark-robustelli Jun 5, 2026
b901ffe
updating validator tests
mark-robustelli Jun 8, 2026
99b7c4e
cleaning up tests
mark-robustelli Jun 8, 2026
0b36810
adding github action
mark-robustelli Jun 8, 2026
4da5610
troubleshooting tests
mark-robustelli Jun 8, 2026
c16f188
updating docker compose command
mark-robustelli Jun 8, 2026
f1d739b
debuggin action
mark-robustelli Jun 8, 2026
cecf6f6
debuggin action
mark-robustelli Jun 8, 2026
34edfe2
update tests to run all
mark-robustelli Jun 8, 2026
d068232
cleaning up files
mark-robustelli Jun 8, 2026
849a595
updating README.md
mark-robustelli Jun 8, 2026
ba664b0
updating integration README.md
mark-robustelli Jun 8, 2026
7abcab5
cleaning up some unused functions
mark-robustelli Jun 8, 2026
e1f7027
updating integration test
mark-robustelli Jun 9, 2026
216f9a5
test clean up
mark-robustelli Jun 9, 2026
bc2fad6
adding email templates
mark-robustelli Jun 9, 2026
c95a5cc
Potential fix for pull request finding
mark-robustelli Jun 9, 2026
6920e7d
Potential fix for pull request finding
mark-robustelli Jun 9, 2026
5b0acbf
Potential fix for pull request finding
mark-robustelli Jun 9, 2026
5904fcc
Potential fix for pull request finding
mark-robustelli Jun 9, 2026
5c54106
Potential fix for pull request finding
mark-robustelli Jun 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Integration Tests

on:
workflow_dispatch:

jobs:
integration-tests:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Docker
uses: docker/setup-buildx-action@v3

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'

- name: Install dependencies
run: npm install

- name: Build
run: npm run build

- name: Run integration tests
env:
RUN_INTEGRATION_TESTS: true
SKIP_UNIT_TESTS: false
SKIP_TEARDOWN: false
REUSE_CONTAINER: false
NODE_ENV: test
FUSIONAUTH_TELEMETRY: false
run: node ./__tests__/test.js
timeout-minutes: 15
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ Fake user generation
- `fusionauth kickstart:start` - Run in the directory of a FusionAuth Docker image to run the image
- `fusionauth kickstart:stop` - Run in the directory of a FusionAuth Docker image to stop the image
- `fusionauth kickstart:kill` - Run in the directory of a FusionAuth Docker image to shutdown and wipe the FusionAuth instance
- Apply Configuration
- `fusionauth apply --file <path>` - Apply a kickstart configuration file to a FusionAuth instance. Supports additional variable substitution with the following patterns:
- `#{DEFAULT_TENANT_ID()}` - Fetch the default tenant ID from the FusionAuth instance
- `#{ENV.VARIABLE_NAME}` - Access environment variables
- `#{PROMPT('message')}` - Prompt user for input (displays value in console)
- `#{PROMPT_HIDDEN('message')}` - Prompt user for input (hides value, suitable for passwords)
- Lambdas
- `fusionauth lambda:update` - Update a lambda on a FusionAuth server.
- `fusionauth lambda:delete` - Delete a lambda from a FusionAuth server.
Expand Down
148 changes: 148 additions & 0 deletions __tests__/integration/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Integration Tests

This directory contains integration tests that verify the `apply` command works end-to-end with a running FusionAuth instance.

## Prerequisites

- Docker and Docker Compose must be installed and running
- Node.js 18+
- Port 9011 must be available (FusionAuth default port)

## Running Integration Tests

```bash
npm run test:integration
```

### Run Only Unit Tests (excludes integration)

```bash
RUN_INTEGRATION_TESTS=false SKIP_UNIT_TESTS=false NODE_ENV=test npm test
```

### Run Only Integration Tests

```bash
RUN_INTEGRATION_TESTS=true SKIP_UNIT_TESTS=true NODE_ENV=test node __tests__/test.js
```

### Environment Variables

- `NODE_ENV=test` — Required for test mode (prevents `process.exit()` calls in executeAction)
- `RUN_INTEGRATION_TESTS=true` — Enable integration tests
- `SKIP_UNIT_TESTS=true|false` — Control whether unit tests run
- `SKIP_TEARDOWN=true` — Keep FusionAuth container running after tests (useful for debugging)
- `REUSE_CONTAINER=true` — Reuse existing FusionAuth container instead of creating new one
- `FUSIONAUTH_TELEMETRY=false` — Disable telemetry in tests

## What Integration Tests Cover

Currently, the integration tests verify the following scenario:

1. **POC Kickstart Configuration** — Applies a complete kickstart configuration that:
- Configures SMTP email settings (host, port, security, default from email, username)
- Creates an admin user with application registration and admin role
- Validates all settings are properly persisted in the FusionAuth instance

## How It Works

1. `setup.js` — Manages FusionAuth container lifecycle:
- Loads docker-compose from `fixtures/kickstarts/fusionauth-integration-test-base/`
- Starts PostgreSQL, OpenSearch, and FusionAuth containers
- FusionAuth auto-loads `fusionauth-integration-test-base/kickstart.json` on startup (creates API key)
- Waits for health checks
- Provides utilities for API requests using native Node.js fetch

2. `apply/apply.integration.test.js` — Executes the actual tests:
- Uses `poc/kickstart.json` fixture to apply complete FusionAuth configuration
- Initializes variable substitution with dynamic variables
- Makes API requests to running FusionAuth instance
- Verifies SMTP configuration via tenant API query
- Verifies user creation and application registration via user API query
- Uses hardcoded POC test IDs: `appId: '3c219e58-ed0e-4b18-ad48-f4f92793ae32'`, `tenantId: '886a57e0-f2ac-440a-9a9d-d10c17b6f1a1'`

3. `fixtures/kickstarts/` — Test configurations:
- `poc/kickstart.json` — Complete POC configuration with SMTP settings and admin user
- `fusionauth-integration-test-base/docker-compose.yml` — Docker Compose for test environment
- `fusionauth-integration-test-base/kickstart.json` — Auto-loads API key on container startup

## Docker Setup

The integration tests use a self-contained Docker setup located in `fixtures/kickstarts/fusionauth-integration-test-base/`:

- **docker-compose.yml** — Defines PostgreSQL, OpenSearch, and FusionAuth services
- **kickstart.json** — Auto-loads on FusionAuth startup (via `FUSIONAUTH_APP_KICKSTART_FILE`)
- **.env.test** — Generated at runtime with database credentials

The FusionAuth container will automatically:
1. Initialize PostgreSQL database
2. Configure search engine (OpenSearch)
3. Load the API key from `kickstart.json`
4. Be ready to accept requests on `http://localhost:9011`

## Test Architecture

The integration tests use a three-layer architecture:

1. **Layer 1: CLI Wrapper** (`action()`) — Command-line interface entry point
2. **Layer 2: Action Handler** (`executeAction()`) — Returns `{success: boolean, error?: string, results?: any}` without calling `process.exit()`
3. **Layer 3: Core Logic** (`executeKickstart()`) — Handles kickstart file parsing and API request execution

This architecture allows tests to run in Node.js test runner while preserving CLI behavior in production mode.

## Debugging

If tests fail, check:

1. **Docker availability** — Run `docker ps` and `docker-compose --version`
2. **Logs** — Check FusionAuth container logs: `docker logs fusionauth-1` (or check exact container name with `docker ps`)
3. **Port conflicts** — Ensure port 9011 is not in use
4. **Network issues** — Check Docker network connectivity
5. **NODE_ENV** — Always set `NODE_ENV=test` when running tests to prevent `process.exit()` calls
6. **Container reuse** — Use `SKIP_TEARDOWN=true REUSE_CONTAINER=true` to keep containers running between test runs for faster iteration

## Troubleshooting

### Container won't start

Clear previous containers and volumes:
```bash
cd __tests__/integration/fixtures/kickstarts/fusionauth-integration-test-base
docker-compose down -v
cd - && npm run test:integration
```

### Tests timeout waiting for FusionAuth

FusionAuth container can take 30-60 seconds to start. Increase the `HEALTH_CHECK_TIMEOUT` in `setup.js` if needed (default: 120000ms).

### Tests fail with NODE_ENV errors

Always set `NODE_ENV=test` when running tests. This prevents `executeAction()` from calling `process.exit()`:
```bash
NODE_ENV=test RUN_INTEGRATION_TESTS=true npm test
```

### API requests fail with 401 or authentication errors

The API key is generated automatically by the FusionAuth container from `kickstart.json`. Ensure:
1. The container is fully started (wait for health check)
2. The API key in `setup.js` matches the generated key: `'90dd6b25-d1ef-4175-9656-159dd994932e'`
3. The FusionAuth container has fully initialized (check logs with `docker logs fusionauth-1`)

### SMTP configuration not persisted

Verify that:
1. The PATCH request to `/api/tenant/{tenantId}` completes successfully
2. The tenantId in the test matches the actual FusionAuth default tenant ID
3. The SMTP configuration in `poc/kickstart.json` is valid

### Port 9011 already in use

Kill the process using port 9011:
```bash
lsof -i :9011
kill -9 <PID>
```

Or use a different port by modifying `setup.js`.
155 changes: 155 additions & 0 deletions __tests__/integration/apply/apply.integration.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { describe, test, before, after } from "node:test"

import assert from "node:assert"
import {
startFusionAuthContainer,
stopFusionAuthContainer,
getUser,
getTenant,
getEmailTemplateByName,
getMessageTemplateByName
} from "../setup.js"
import { executeAction } from "../../../dist/commands/apply.js"

/**
* Display kickstart execution errors and warnings to console
*/
function displayKickstartErrors(result) {
console.log('❌ Apply failed:', result.error)

if (result.results?.steps) {
console.log('\n📋 Kickstart execution details:')
for (const step of result.results.steps) {
if (step.status === 'error' || step.status === 'warning') {
console.log(`\n Step: ${step.id} (${step.action} ${step.request?.url})`)
console.log(` Status: ${step.status.toUpperCase()}`)
Comment on lines +21 to +25
console.log(` Response Status: ${step.response?.status}`)

if (step.error?.message) {
console.log(` Message: ${step.error.message}`)
}

if (step.response?.body?.fieldErrors) {
console.log(' Field Errors:')
for (const [field, fieldErrs] of Object.entries(step.response.body.fieldErrors)) {
for (const err of fieldErrs) {
console.log(` - ${field}: ${err.message}`)
}
}
}

if (step.response?.body?.generalErrors && step.response.body.generalErrors.length > 0) {
console.log(' General Errors:')
for (const err of step.response.body.generalErrors) {
console.log(` - ${err.message || err}`)
}
}
}
}
}
}

export function applyIntegration() {
let fusionAuthUrl
let apiKey

// Test configuration
const appId = '3c219e58-ed0e-4b18-ad48-f4f92793ae32'
const tenantId = '886a57e0-f2ac-440a-9a9d-d10c17b6f1a1'

// Static execute action options for POC
const pocExecuteActionOptionsStatic = {
file: '__tests__/integration/fixtures/kickstarts/poc/kickstart.json',
quiet: true,
continueOnError: false
}

// Expected values for SMTP configuration
const expectedSmtp = {
host: 'smtp.sendgrid.net',
port: 587,
security: 'TLS',
defaultFromEmail: 'poc@fusionauth.io'
}

// Expected admin user properties
const expectedAdminUser = {
email: 'admin@example.com',
active: true,
shouldHaveAdminRole: true
}

before(async () => {
const container = await startFusionAuthContainer()
fusionAuthUrl = container.url
apiKey = container.apiKey
})

after(async () => {
await stopFusionAuthContainer()
})

describe('Apply Command Integration Tests', () => {
test('should properly process poc/kickstart.json test file.', async (t) => {
// Merge static and dynamic options
const pocExecuteActionOptions = {
...pocExecuteActionOptionsStatic,
host: fusionAuthUrl,
key: apiKey
}

const result = await executeAction(pocExecuteActionOptions)

if (!result.success) {
displayKickstartErrors(result)
throw new Error(`Apply action failed: ${result.error}`)
}

// Verify SMTP configuration was properly persisted in test instance
const tenant = await getTenant(tenantId, apiKey)

assert(tenant.emailConfiguration, 'Tenant should have email configuration')
assert.equal(tenant.emailConfiguration.host, expectedSmtp.host, `SMTP host should be correctly set to ${expectedSmtp.host}`)
assert.equal(tenant.emailConfiguration.port, expectedSmtp.port, `SMTP port should be correctly set to ${expectedSmtp.port}`)
assert.equal(tenant.emailConfiguration.security, expectedSmtp.security, `SMTP security should be correctly set to ${expectedSmtp.security}`)
assert.equal(tenant.emailConfiguration.defaultFromEmail, expectedSmtp.defaultFromEmail, `Default from email should be correctly set to ${expectedSmtp.defaultFromEmail}`)
assert(tenant.emailConfiguration.username !== undefined && tenant.emailConfiguration.username !== null, 'SMTP username should be configured')

// Verify admin user was created with correct properties
const retrievedUser = await getUser(expectedAdminUser.email, apiKey)
assert.equal(retrievedUser.email, expectedAdminUser.email, `Admin user email should be set to ${expectedAdminUser.email}`)
assert(retrievedUser.active === expectedAdminUser.active, `Admin user should be active`)
assert(retrievedUser.registrations, 'Admin user should have application registrations')

const adminRegistration = retrievedUser.registrations.find(r => r.applicationId === appId)
assert(adminRegistration, 'Admin user should be registered to the created application')
assert(adminRegistration.roles && adminRegistration.roles.includes('admin'), 'Admin user should have admin role for the application')

// Verify email templates were created
const setupPasswordTemplate = await getEmailTemplateByName('Set up Password', apiKey)
assert(setupPasswordTemplate, 'Set up Password email template should exist')
assert(setupPasswordTemplate.defaultHtmlTemplate, 'Set up Password template should have HTML content')
assert(setupPasswordTemplate.defaultTextTemplate, 'Set up Password template should have text content')

const twoFactorTemplate = await getEmailTemplateByName('Two Factor Authentication', apiKey)
assert(twoFactorTemplate, 'Two Factor Authentication email template should exist')
assert(twoFactorTemplate.defaultHtmlTemplate, 'Two Factor Authentication template should have HTML content')
assert(twoFactorTemplate.defaultTextTemplate, 'Two Factor Authentication template should have text content')

// Verify message template was created
const voiceTwoFactorTemplate = await getMessageTemplateByName('Default Voice Two Factor Request', apiKey)
assert(voiceTwoFactorTemplate, 'Default Voice Two Factor Request message template should exist')
assert.equal(voiceTwoFactorTemplate.type, 'Voice', 'Voice template should have type Voice')
assert(voiceTwoFactorTemplate.defaultTemplate, 'Voice Two Factor Request template should have default content')

// Verify forgot password templates are configured in tenant
assert(tenant.emailConfiguration, 'Tenant should have email configuration')
assert(tenant.emailConfiguration.forgotPasswordEmailTemplateId, 'Tenant should have forgot password email template configured')
assert(tenant.emailConfiguration.verificationEmailTemplateId, 'Tenant should have verification email template configured')

assert(tenant.phoneConfiguration, 'Tenant should have Phone configuration')
assert(tenant.phoneConfiguration.verificationTemplateId, 'Tenant should have forgot password Phone template configured')

})
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
DATABASE_PASSWORD=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_USER=postgres
DATABASE_USERNAME=fusionauth
DATABASE_URL=jdbc:postgresql://db:5432/fusionauth
FUSIONAUTH_APP_MEMORY=512M
FUSIONAUTH_APP_RUNTIME_MODE=development
FUSIONAUTH_APP_KICKSTART_FILE=/usr/local/fusionauth/kickstart/kickstart.json
KICKSTART_FILE_PATH=/Users/mark.robustelli/Projects/fusionauth-node-cli/__tests__/integration/fixtures/kickstarts/fusionauth-integration-test-base/kickstart.json
OPENSEARCH_JAVA_OPTS=-Xms256m -Xmx256m
Comment on lines +8 to +10
Loading