# Creating a KTestify Plugin

> Generate a complete ktestify plugin project in seconds using the Maven archetype, then implement your transport logic.

Source: https://docs.ktestify.xyz/docs/extend/plugins/create-plugin

This guide walks you through creating a ktestify plugin from start to finish using the **ktestify-plugin-archetype** Maven archetype. The archetype generates a complete, compilable project with all the boilerplate: plugin SPI class, config reader, transport layer, Cucumber step definitions, services, unit tests, and build configuration.

:::tip
The archetype is the **recommended way** to start a new plugin. It ensures your project follows the correct structure, naming conventions, and build setup from day one.
:::

---

## How the Archetype Works

The ktestify plugin archetype is a standard Maven archetype that uses [Velocity templating](https://velocity.apache.org/) to generate a project tailored to your plugin. When you run `mvn archetype:generate`, Maven prompts you for a set of parameters (plugin name, author, GitHub repo, etc.) and produces a complete project directory.

### What gets generated

```
ktestify-plugin-myplugin/
├── pom.xml                          ← Full POM with Spotless, JaCoCo, release profile
├── README.md                        ← Plugin README with step examples
├── LICENSE                          ← Apache 2.0
├── cliff.toml                       ← git-cliff changelog config
├── .gitignore
├── .spotless/
│   └── HEADER.txt                   ← Apache 2.0 license header (with your name)
└── src/
    ├── main/
    │   ├── java/.../
    │   │   ├── MyPluginPlugin.java              ← KtestifyPlugin SPI implementation
    │   │   ├── config/
    │   │   │   └── MyPluginConfig.java          ← Typed HOCON config reader
    │   │   ├── io/
    │   │   │   ├── MyPluginRecordFetcher.java   ← RecordFetcher<String> (transport layer)
    │   │   │   ├── MyPluginConsumer.java        ← Orchestration (fetch → match → result)
    │   │   │   └── MyPluginConsumerContext.java ← Immutable per-fetch context
    │   │   ├── entities/
    │   │   │   └── KtestifyMyPluginEntity.java  ← Resource entity
    │   │   ├── services/
    │   │   │   ├── MyPluginActionService.java   ← Send/upload service
    │   │   │   └── MyPluginValidationService.java ← Validation + timeout orchestration
    │   │   └── steps/
    │   │       ├── MyPluginBackgroundSteps.java ← @Given steps
    │   │       ├── MyPluginActionSteps.java     ← @When steps
    │   │       ├── MyPluginValidationSteps.java ← @Then / @And steps
    │   │       └── SharedMyPluginResources.java ← PicoContainer shared state
    │   └── resources/
    │       ├── reference.conf                   ← Default HOCON config
    │       ├── log4j2.properties                ← Logging config
    │       └── META-INF/services/
    │           └── io.github.ktestify.plugin.KtestifyPlugin
    └── test/
        └── java/.../
            ├── MyPluginPluginTest.java          ← Plugin lifecycle tests
            └── config/
                └── MyPluginConfigTest.java      ← Config loading tests
```

### Three-layer architecture

The generated project follows the same three-layer separation as ktestify-core:

```
┌──────────────────────────────────────────────────────────────┐
│  TRANSPORT                 │  ORCHESTRATION  │  ASSERTION    │
│  MyPluginRecordFetcher     │  MyPluginConsumer│ RecordMatcher │
│  implements RecordFetcher  │                 │ (from core)   │
└──────────────────────────────────────────────────────────────┘
```

- **Transport**: `MyPluginRecordFetcher` implements `RecordFetcher<String>` from ktestify-core. It returns `List<ConsumedRecord<String>>`, the only type crossing layer boundaries.
- **Orchestration**: `MyPluginConsumer` wires fetch to match to result. It uses `RecordMatcherFactory.forRaw()` to select the correct matcher.
- **Assertion**: all standard `RecordMatcher` implementations from ktestify-core are reused as-is. No matcher code is needed in your plugin.

---

## Phase 1: Generate the Project

### Step 1.1. Run the archetype

```bash
mvn archetype:generate \
  -DarchetypeGroupId=io.github.ktestify \
  -DarchetypeArtifactId=ktestify-plugin-archetype \
  -DarchetypeVersion=1.0-SNAPSHOT \
  -DgroupId=io.github.ktestify \
  -DartifactId=ktestify-plugin-s3 \
  -Dversion=1.0-SNAPSHOT \
  -Dpackage=io.github.ktestify.s3 \
  -DpluginShortName=s3 \
  -DpluginKebabId=s3 \
  -DpluginPascalName=S3 \
  -DauthorName="Your Name" \
  -DauthorEmail="your.email@example.com" \
  -DgithubOrg=your-github-username \
  -DgithubRepoName=ktestify-plugin-s3 \
  -DinteractiveMode=false
```

### Step 1.2. Archetype parameters

| Parameter | Description | Example |
|---|---|---|
| `groupId` | Maven groupId for the generated project | `io.github.ktestify` |
| `artifactId` | Maven artifactId (should start with `ktestify-plugin-`) | `ktestify-plugin-s3` |
| `version` | Initial Maven version | `1.0-SNAPSHOT` |
| `package` | Base Java package | `io.github.ktestify.s3` |
| `pluginShortName` | Short lowercase name (file names, log files) | `s3` |
| `pluginKebabId` | Kebab-case plugin ID (HOCON config paths) | `s3` |
| `pluginPascalName` | PascalCase name (Java class names) | `S3` |
| `authorName` | Author display name (startup banner + POM) | `Your Name` |
| `authorEmail` | Author contact email | `your.email@example.com` |
| `githubOrg` | GitHub username or organisation | `your-github-username` |
| `githubRepoName` | GitHub repository name | `ktestify-plugin-s3` |

> **ktestify-core version**: The generated project depends on the latest released version of `ktestify-core`. This version is managed by Dependabot in the archetype repository. When a new version of ktestify-core is released, Dependabot automatically opens a PR to bump it in the template POM.

### Step 1.3. Verify it compiles

```bash
cd ktestify-plugin-s3
mvn compile
```

If this succeeds, you have a valid plugin skeleton. The generated code compiles out of the box, and all TODO stubs are syntactically valid.

---

## Phase 2: Implement Your Transport

The generated code contains TODO markers where you need to add your transport-specific logic. Here's what to implement:

### Step 2.1. Add your transport SDK dependency

Add your transport SDK to `pom.xml`:

```xml
<!-- Example: AWS S3 SDK -->
<dependency>
    <groupId>software.amazon.awssdk</groupId>
    <artifactId>s3</artifactId>
    <version>2.25.0</version>
</dependency>
```

### Step 2.2. Implement the RecordFetcher

Open `src/main/java/.../io/S3RecordFetcher.java` and implement the `fetch()` method:

```java
@Override
public List<ConsumedRecord<String>> fetch() throws FetchException {
    String recordId = context.getRecordId();
    String resourceName = context.getResourceName();
    long deadlineMs = System.currentTimeMillis() + resolveReadTimeoutMs();
    long pollMs = resolvePollIntervalMs();

    while (System.currentTimeMillis() < deadlineMs) {
        // TODO: Replace with your SDK call
        boolean exists = s3Client.headObject(
            HeadObjectRequest.builder()
                .bucket(resourceName)
                .key(recordId)
                .build()
        ).sdkHttpResponse().isSuccessful();

        if (exists) {
            return List.of(downloadRecord(resourceName, recordId));
        }

        sleep(pollMs, recordId);
    }

    throw new FetchException("Timed out waiting for record '" + recordId + "'");
}
```

### Step 2.3. Implement the Action Service

Open `src/main/java/.../services/S3ActionService.java` and implement the `send()` method:

```java
public void send(KtestifyS3Entity resource, String recordId, String sourceFile) {
    String connStr = resolveConnectionString(resource);
    // TODO: Build your client and upload the file
    byte[] content = Files.readAllBytes(Path.of(sourceFile));
    s3Client.putObject(
        PutObjectRequest.builder()
            .bucket(resource.getResourceName())
            .key(recordId)
            .build(),
        RequestBody.fromBytes(content)
    );
}
```

### Step 2.4. Customize step wording (optional)

The generated steps use generic wording like `"S3 resource"` and `"expected S3 record from file"`. You can rename these to be more natural for your transport:

```java
// Before (generated)
@Given("S3 resource")

// After (customized)
@Given("S3 bucket")
```

---

## Phase 3: Configuration

### Step 3.1. Update reference.conf

The generated `reference.conf` has a basic connection-string pattern. Update it to match your transport's authentication model:

```hocon
ktestify.plugins.s3 {
  # AWS credentials (uses DefaultCredentialsProvider if not set)
  access-key-id     = ""
  access-key-id     = ${?AWS_ACCESS_KEY_ID}

  secret-access-key = ""
  secret-access-key = ${?AWS_SECRET_ACCESS_KEY}

  region            = "us-east-1"
  region            = ${?AWS_REGION}

  endpoint          = ""   # for LocalStack
  endpoint          = ${?AWS_ENDPOINT}

  read-timeout      = 30s
  poll-interval     = 500ms
}
```

### Step 3.2. Update the Config class

Add fields to `S3Config.java` to match your new config keys:

```java
private S3Config(Config cfg) {
    this.accessKeyId     = cfg.getString("access-key-id");
    this.secretAccessKey = cfg.getString("secret-access-key");
    this.region          = cfg.getString("region");
    this.endpoint        = cfg.getString("endpoint");
    this.readTimeoutMs   = cfg.getDuration("read-timeout").toMillis();
    this.pollIntervalMs  = cfg.getDuration("poll-interval").toMillis();
}
```

---

## Phase 4: Testing

### Step 4.1. Unit tests

The archetype generates unit tests for the plugin lifecycle and config loading. Run them:

```bash
mvn test
```

### Step 4.2. Integration tests

Add integration tests using Testcontainers. Create `src/test/java/.../S3PluginIT.java`:

```java
@Testcontainers
class S3PluginIT {

    @Container
    static final GenericContainer<?> localstack =
        new GenericContainer<>("localstack/localstack:3.0")
            .withEnv("SERVICES", "s3")
            .withExposedPorts(4566);

    @Test
    @DisplayName("upload and validate a blob")
    void uploadAndValidate() {
        // Point your plugin at LocalStack
        // Upload a file via the action service
        // Assert it appears via the validation service
    }
}
```

Run integration tests (requires Docker):

```bash
mvn verify
```

---

## Phase 5: Use Your Plugin

### Option A. As a Maven dependency

```xml
<dependency>
    <groupId>io.github.ktestify</groupId>
    <artifactId>ktestify-plugin-s3</artifactId>
    <version>1.0-SNAPSHOT</version>
    <scope>test</scope>
</dependency>
```

### Option B. Drop the JAR into `/workspace/plugins`

```bash
docker run --rm \
  -v $(pwd)/features:/workspace/features \
  -v $(pwd)/plugins:/workspace/plugins \
  ghcr.io/ktestify/ktestify-cucumber:latest \
  /workspace/features
```

### Example Gherkin scenario

```gherkin
Feature: S3 integration

  Background:
    Given S3 bucket
      | resourceName | resourceAlias |
      | test-bucket  | blobs          |

  Scenario: Upload and validate
    When S3 record is sent from file
      | resourceAlias | file         | recordId    |
      | blobs         | payload.json | data.json   |
    Then expected S3 record from file
      | resourceAlias | recordId  | file           | readTimeout |
      | blobs         | data.json | expected.json  | 30          |
```

---

## Checklist for Production Quality

Before publishing your plugin, ensure it meets these criteria:

- [ ] Plugin implements all methods of `KtestifyPlugin` interface
- [ ] Configuration is loaded from `ktestify.plugins.<id>` HOCON subtree
- [ ] `initialize()` validates required config and throws `PluginException` on failure
- [ ] `shutdown()` releases all resources (connections, threads, etc.)
- [ ] Plugin is registered in `META-INF/services/io.github.ktestify.plugin.KtestifyPlugin`
- [ ] Step definitions are in a discoverable package (returned by `getGluePackage()`)
- [ ] `RecordFetcher<V>` implementation returns `ConsumedRecord<V>` correctly
- [ ] `reference.conf` documents all configuration keys with sensible defaults
- [ ] Logging uses SLF4J via `@Slf4j` annotation
- [ ] Documentation includes configuration examples and usage scenarios
- [ ] README contains a clear description of what the plugin does
- [ ] Unit and integration tests pass
- [ ] Code is formatted with Maven Spotless (`mvn spotless:apply`)

---

## Reference Implementations

Looking for real-world examples? Check out the first-party plugins:

| Plugin | What it demonstrates |
|---|---|
| [ktestify-plugin-azureblob](azureblob) | Azure Blob Storage transport with Azurite integration tests |
| [ktestify-plugin-notifications](notifications) | Non-transport plugin: Teams/Slack/webhook notifications with its own SPI |
| [ktestify-plugin-skeleton](https://github.com/ktestify/ktestify-plugin-skeleton) | The reference template the archetype is derived from |

---

## Resources

- [ktestify-plugin-archetype](https://github.com/ktestify/ktestify-plugin-archetype): the archetype repository
- [Plugin System Documentation](plugin-system): how the plugin loading works
- [ktestify-core API](https://github.com/ktestify/ktestify-core): base classes and interfaces
- [Adding a Transport](../transports/adding-a-transport): transport layer deep dive

---

## Next Steps

Once your plugin is complete and tested:

1. **Publish to Maven Central**: follow [Sonatype's guide](https://central.sonatype.org/publishing/publish-maven/)
2. **Create a GitHub repository**: use `ktestify-plugin-*` naming convention
3. **Document on docs.ktestify.xyz**: add a page next to the Azure Blob example
4. **Announce in the ktestify community**: open a discussion thread

Happy plugin building!
