Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 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
4dc2da4
fixing error from copilot review suggestion
mark-robustelli Jun 10, 2026
bddaa1c
refactor(tests): remove exported function wrappers from test files
mark-robustelli Jun 10, 2026
b5d55d4
refactor(tests): replace manual test aggregator with node --test scripts
mark-robustelli Jun 10, 2026
f89d6d8
refactor(tests): rename test files to follow *.test.js convention
mark-robustelli Jun 10, 2026
57f6678
refactor(tests): import from TypeScript source instead of compiled ou…
mark-robustelli Jun 10, 2026
de23918
refactor(tests): fixing failure in github action.
mark-robustelli Jun 10, 2026
049aa96
fix(tests): replace execSync with async exec to prevent event loop bl…
Copilot Jun 10, 2026
5bc80d7
docs(tests): add JSDoc comment explaining async exec usage in setup.js
Copilot Jun 10, 2026
71e211a
refactor(deps): replace ts-node with tsx for TypeScript loading
mark-robustelli Jun 10, 2026
68ba1b4
ci: upgrade GitHub Actions to Node.js 24-compatible versions
mark-robustelli Jun 10, 2026
c1ca439
fix(tests): replace before/after hooks inside test bodies with try/fi…
mark-robustelli Jun 10, 2026
ae3be92
fix(tests): telemetry test was duplicated
mark-robustelli Jun 10, 2026
cf24894
fix(tests): cleaning up should resolve ENV Variables test
mark-robustelli Jun 10, 2026
f856de5
refactor(tests): update tests to use strict assertions
mark-robustelli Jun 10, 2026
5f28af4
refactor(tests): remove unused test context parameter from sync tests
mark-robustelli Jun 10, 2026
ce705b4
(clean up) remove .env.test
mark-robustelli Jun 10, 2026
ad49766
fix(utilities/http-client): fix assumption
mark-robustelli Jun 10, 2026
a56036b
fix(line-tracker): correctly detect array boundaries in all valid JSO…
mark-robustelli Jun 10, 2026
fcc148a
Merge branch 'main' into mcr/kickstart-apply
mark-robustelli Jun 10, 2026
3857b01
fix(validator): fix special patterns for url extraction
mark-robustelli Jun 11, 2026
bde9d54
fix(validator): fix line number
mark-robustelli Jun 11, 2026
f86735b
fix(validator): fix allow object and only allow json
mark-robustelli Jun 11, 2026
b2e2a8e
fix(validator-substitution): fix prefix-matching vulnerability
mark-robustelli Jun 11, 2026
ab087ed
refactor(http-client): refactor timeout handling
mark-robustelli Jun 11, 2026
18da867
(update)made it more clear that test data was test.
mark-robustelli Jun 11, 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
12 changes: 5 additions & 7 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

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

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

- name: Install dependencies
run: npm install
Expand All @@ -26,11 +26,9 @@ jobs:

- 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
run: npm run test
timeout-minutes: 15
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,9 @@ dist
# Stores VSCode versions used for testing VSCode extensions
.vscode-test

# Integration test generated files
__tests__/integration/fixtures/kickstarts/fusionauth-integration-test-base/.env.test

# yarn v2
.yarn/cache
.yarn/unplugged
Expand Down
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 Tests

### Run All Tests (unit + integration)

```bash
npm test
```

### Run Unit Tests Only (excludes integration)

```bash
npm run test:unit
```

### Run Integration Tests Only

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

### Environment Variables

- `NODE_ENV=test` — Required for test mode (prevents `process.exit()` calls in executeAction)
- `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
npm run test:integration
```

### 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`.
147 changes: 147 additions & 0 deletions __tests__/integration/apply/apply.integration.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { describe, test } from "node:test"
import assert from "node:assert/strict"
import {
startFusionAuthContainer,
stopFusionAuthContainer,
getUser,
getTenant,
getEmailTemplateByName,
getMessageTemplateByName
} from "../setup.js"
import { executeAction } from "../../../src/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()}`)
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}`)
}
}
}
}
}
}

// 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: 'testfromemail@example.com'
}

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

describe('Apply Command Integration Tests', () => {
test('should properly process poc/kickstart.json test file.', async (t) => {
const container = await startFusionAuthContainer()
t.after(async () => {
await stopFusionAuthContainer()
})

const fusionAuthUrl = container.url
const apiKey = container.apiKey

// 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')

})
})
Loading