Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@
- Custom error reporting via `utils.reportError()` and `utils.errorAndExit()`
- Check response types with `isClientResponse()` and `isErrors()` utilities

### Confirmation and Risky Operations
- Commands that perform irreversible or potentially disruptive operations require `--yes` to proceed non-interactively
- Without `--yes`, these commands exit with an error in non-TTY contexts (agents, pipes, scripts)
- Always obtain user confirmation before passing `--yes`; never pass it autonomously for destructive operations
- Where available, prefer running with `--dry-run` first to preview changes before committing

### Code Structure
- Command definitions use Commander.js with fluent API
- JSDoc comments for function documentation
Expand Down
43 changes: 43 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Contributing

## Command Structure
Commands generally follow the form:

fusionauth namespace:command [--command-option] ...

Where
* Commands are grouped into a functional or domain namespace
* Option names use kebab-case (e.g. `--admin-email`, `--number-of-files`)
* Sensitive items can be passed via environment variable. In this case use `--option-name-env ENV_VAR` to indicate that the value is coming from the specified environment variable

## Risky Operations Policy

Commands that perform risky operations must gate execution behind user confirmation using `confirmOrExit()` from `src/utils.ts`. All such commands must expose a `--yes` flag.

## Testing

### Running the tests

```bash
# Unit tests (run these before every commit)
npm run test:unit

# Integration tests (requires a live FusionAuth instance)
npm run test:integration

# Full suite
npm run test
```

The integration tests manage a Docker container automatically. Several environment variables control their behaviour:

| Variable | Effect |
|---|---|
| `VERBOSE_CONTAINER=true` | Print each health-check attempt, elapsed time, and error reason; dump `docker compose logs` on failure |
| `REUSE_CONTAINER=true` | Skip container startup and use a FusionAuth instance already running on `localhost:9011` |
| `SKIP_TEARDOWN=true` | Leave the container running after the tests finish (useful for manual inspection) |

### Requirements

- **All new functionality must be covered by tests.** This includes new commands, new options on existing commands, and new utility functions.
- **All existing tests must pass cleanly before a PR is submitted.** A clean run means zero failures — `# fail 0` in the test output.
64 changes: 64 additions & 0 deletions __tests__/commands/import-generate.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { describe, test } from "node:test"
import assert from "node:assert/strict"
import { getDeprecatedFlagUsage, importGenerate } from "../../src/commands/import-generate.js"

describe('getDeprecatedFlagUsage()', () => {
test('returns empty array when no deprecated flags are used', () => {
const usage = getDeprecatedFlagUsage(['node', 'script', '--number-of-files', '5'])
assert.deepEqual(usage, [])
})

test('detects a bare deprecated flag (--flag value form)', () => {
const usage = getDeprecatedFlagUsage(['node', 'script', '--numberOfFiles', '5'])
assert.equal(usage.length, 1)
assert.deepEqual(usage[0], ['--numberOfFiles', '--number-of-files'])
})

test('detects a deprecated flag in --flag=value form', () => {
const usage = getDeprecatedFlagUsage(['node', 'script', '--numberOfFiles=5'])
assert.equal(usage.length, 1)
assert.deepEqual(usage[0], ['--numberOfFiles', '--number-of-files'])
})

test('detects multiple deprecated flags used together', () => {
const usage = getDeprecatedFlagUsage(['node', 'script', '--numberOfFiles', '5', '--groupId=abc'])
const oldFlags = usage.map(([old]) => old)
assert.ok(oldFlags.includes('--numberOfFiles'))
assert.ok(oldFlags.includes('--groupId'))
assert.equal(usage.length, 2)
})

test('does not flag the new kebab-case form as deprecated', () => {
const usage = getDeprecatedFlagUsage(['node', 'script', '--group-id', 'abc'])
assert.deepEqual(usage, [])
})
})

describe('import:generate option parsing', () => {
test('deprecated --numberOfFiles populates the same option as --number-of-files', async () => {
let capturedOptions
importGenerate.action((options) => { capturedOptions = options })

await importGenerate.parseAsync(['--numberOfFiles', '5'], { from: 'user' })

assert.equal(capturedOptions.numberOfFiles, '5')
})

test('--number-of-files populates the same numberOfFiles property', async () => {
let capturedOptions
importGenerate.action((options) => { capturedOptions = options })

await importGenerate.parseAsync(['--number-of-files', '7'], { from: 'user' })

assert.equal(capturedOptions.numberOfFiles, '7')
})

test('deprecated --groupId populates the same option as --group-id', async () => {
let capturedOptions
importGenerate.action((options) => { capturedOptions = options })

await importGenerate.parseAsync(['--groupId', 'abc-123'], { from: 'user' })

assert.equal(capturedOptions.groupId, 'abc-123')
})
})
Loading