Skip to main content

Overview

Custom test templates allow you to:
  • Create reusable test patterns for your specific API security needs
  • Auto-generate tests from endpoint metadata
  • Standardize security testing across your organization
  • Extend Metlo’s built-in templates with custom logic

Template Structure

A test template is a TypeScript or JavaScript module that exports:
  • name - Template identifier
  • version - Template version number
  • builder - Function that generates a test configuration
import { GenTestEndpoint, TestBuilder, TemplateConfig } from "@metlo/testing"

export default {
  name: "CUSTOM_TEMPLATE",
  version: 1,
  builder: (endpoint: GenTestEndpoint, config: TemplateConfig) => {
    return new TestBuilder()
      .setMeta({
        name: `${endpoint.path} Custom Test`,
        severity: "HIGH",
        tags: ["CUSTOM"],
      })
      .addTestStep(
        // Define test steps
      )
  },
}

Setting Up Custom Templates

1

Initialize Template Project

Create a new custom template project:
metlo test init-custom-templates my-templates
This creates:
  • package.json with @metlo/testing dependency
  • templates/ directory for your templates
2

Create Template File

Create a new template in templates/:
touch templates/custom-auth.ts
3

Write Template Logic

Implement your template using the builder API
4

Generate Tests

Use your custom template:
metlo test generate \
  --test ./templates/custom-auth.ts \
  --host api.example.com \
  --endpoint /api/data \
  --method GET \
  --path output.yaml

Template Builder API

TestBuilder

The TestBuilder class helps construct test configurations:
import { TestBuilder, TestStepBuilder } from "@metlo/testing"

const test = new TestBuilder()
  .setMeta({
    name: "My Test",
    severity: "HIGH",
    tags: ["CUSTOM"],
  })
  .addEnv("BASE_URL", "https://api.example.com")
  .addTestStep(
    new TestStepBuilder()
      .setMethod("GET")
      .setUrl("{{BASE_URL}}/endpoint")
      .addHeader("Authorization", "Bearer token")
      .assert({
        key: "resp.status",
        value: 200,
      })
  )
  .getTest()

TestStepBuilder

Build individual test steps:
new TestStepBuilder()
  .setMethod("GET")
  .setUrl("https://api.example.com/users")
  .addHeader("Content-Type", "application/json")
  .assert({
    key: "resp.status",
    value: 200,
  })

Built-in Template Examples

Broken Authentication Template

Tests that authentication is properly enforced:
import { GenTestEndpoint, TestBuilder, TestStepBuilder } from "@metlo/testing"
import { AssertionType } from "@metlo/testing/dist/types/enums"
import { TemplateConfig } from "@metlo/testing/dist/types/resource_config"

export default {
  name: "BROKEN_AUTHENTICATION",
  version: 1,
  builder: (endpoint: GenTestEndpoint, config: TemplateConfig) => {
    if (!endpoint.authConfig) {
      throw new Error(`No auth config defined for host: "${endpoint.host}"`)
    }

    return new TestBuilder()
      .setMeta({
        name: `${endpoint.path} Broken Authentication`,
        severity: "HIGH",
        tags: ["BROKEN_AUTHENTICATION"],
      })
      .addTestStep(
        TestStepBuilder.sampleRequest(endpoint, config).assert({
          type: AssertionType.enum.JS,
          value: "resp.status < 300",
        })
      )
      .addTestStep(
        TestStepBuilder.sampleRequestWithoutAuth(endpoint, config).assert({
          type: AssertionType.enum.EQ,
          key: "resp.status",
          value: [401, 403],
        })
      )
  },
}

BOLA (Broken Object Level Authorization) Template

Tests that users can’t access other users’ resources:
import { GenTestEndpoint, TestBuilder, TestStepBuilder } from "@metlo/testing"
import { AssertionType } from "@metlo/testing/dist/types/enums"
import { TemplateConfig } from "@metlo/testing/dist/types/resource_config"

export default {
  name: "BOLA",
  version: 1,
  builder: (endpoint: GenTestEndpoint, config: TemplateConfig) => {
    if (!endpoint.authConfig) {
      throw new Error(`No auth config defined for host: "${endpoint.host}"`)
    }

    return new TestBuilder()
      .setMeta({
        name: `${endpoint.path} BOLA`,
        severity: "HIGH",
        tags: ["BOLA"],
      })
      // User A can access their own resources
      .addTestStep(
        TestStepBuilder.sampleRequest(endpoint, config, "USER_A").assert({
          type: AssertionType.enum.JS,
          value: "resp.status < 300",
        })
      )
      // User B cannot access User A's resources
      .addTestStep(
        TestStepBuilder.sampleRequestWithoutAuth(endpoint, config, "USER_A")
          .addAuth(endpoint, "USER_B")
          .assert({
            type: AssertionType.enum.EQ,
            key: "resp.status",
            value: [401, 403],
          })
      )
      // User A cannot access User B's resources
      .addTestStep(
        TestStepBuilder.sampleRequestWithoutAuth(endpoint, config, "USER_B")
          .addAuth(endpoint, "USER_A")
          .assert({
            type: AssertionType.enum.EQ,
            key: "resp.status",
            value: [401, 403],
          })
      )
  },
}

Custom Template Examples

Rate Limiting Test

import { GenTestEndpoint, TestBuilder, TestStepBuilder } from "@metlo/testing"
import { TemplateConfig } from "@metlo/testing/dist/types/resource_config"

export default {
  name: "RATE_LIMIT",
  version: 1,
  builder: (endpoint: GenTestEndpoint, config: TemplateConfig) => {
    const builder = new TestBuilder()
      .setMeta({
        name: `${endpoint.path} Rate Limiting`,
        severity: "MEDIUM",
        tags: ["RATE_LIMIT"],
      })

    // Make 100 requests rapidly
    for (let i = 0; i < 100; i++) {
      builder.addTestStep(
        TestStepBuilder.sampleRequest(endpoint, config)
          .assert({
            type: "JS",
            value: "resp.status === 200 || resp.status === 429",
            description: `Request ${i + 1} should succeed or be rate limited`,
          })
      )
    }

    // Verify at least some requests were rate limited
    builder.addTestStep(
      new TestStepBuilder()
        .setMethod("GET")
        .setUrl("{{BASE_URL}}/dummy")
        .assert({
          type: "JS",
          value: "true", // Placeholder for checking rate limit was hit
          description: "At least one request should have been rate limited",
        })
    )

    return builder
  },
}

Input Validation Test

import { GenTestEndpoint, TestBuilder, TestStepBuilder } from "@metlo/testing"
import { TemplateConfig } from "@metlo/testing/dist/types/resource_config"

export default {
  name: "INPUT_VALIDATION",
  version: 1,
  builder: (endpoint: GenTestEndpoint, config: TemplateConfig) => {
    const invalidInputs = [
      { name: "empty string", value: "" },
      { name: "very long string", value: "a".repeat(10000) },
      { name: "null", value: "null" },
      { name: "special characters", value: "<script>alert('xss')</script>" },
      { name: "SQL injection", value: "'; DROP TABLE users; --" },
    ]

    const builder = new TestBuilder()
      .setMeta({
        name: `${endpoint.path} Input Validation`,
        severity: "HIGH",
        tags: ["INPUT_VALIDATION"],
      })

    for (const input of invalidInputs) {
      builder.addTestStep(
        new TestStepBuilder()
          .setMethod(endpoint.method)
          .setUrl(`${endpoint.host}${endpoint.path}`)
          .setData(JSON.stringify({ input: input.value }))
          .addHeader("Content-Type", "application/json")
          .assert({
            type: "JS",
            value: "resp.status === 400 || resp.status === 422",
            description: `Should reject ${input.name}`,
          })
      )
    }

    return builder
  },
}

Custom Header Validation

import { GenTestEndpoint, TestBuilder, TestStepBuilder } from "@metlo/testing"
import { TemplateConfig } from "@metlo/testing/dist/types/resource_config"

export default {
  name: "SECURITY_HEADERS",
  version: 1,
  builder: (endpoint: GenTestEndpoint, config: TemplateConfig) => {
    const requiredHeaders = [
      { name: "X-Content-Type-Options", value: "nosniff" },
      { name: "X-Frame-Options", value: "DENY" },
      { name: "X-XSS-Protection", value: "1; mode=block" },
      { name: "Strict-Transport-Security", pattern: /max-age=/ },
    ]

    const builder = new TestBuilder()
      .setMeta({
        name: `${endpoint.path} Security Headers`,
        severity: "MEDIUM",
        tags: ["SECURITY_HEADERS"],
      })
      .addTestStep(
        TestStepBuilder.sampleRequest(endpoint, config)
          .assert({
            key: "resp.status",
            value: 200,
          })
      )

    for (const header of requiredHeaders) {
      builder.addTestStep(
        TestStepBuilder.sampleRequest(endpoint, config)
          .assert({
            type: header.pattern ? "REGEXP" : "EQ",
            key: `resp.headers['${header.name.toLowerCase()}']`,
            value: header.pattern?.source || header.value,
            description: `Should include ${header.name} header`,
          })
      )
    }

    return builder
  },
}

Using Templates

Generate Test from Template

metlo test generate \
  --test ./templates/rate-limit.ts \
  --host api.example.com \
  --endpoint /api/search \
  --method POST \
  --path tests/rate-limit-test.yaml

Generate Without Saving

Print the generated test to stdout:
metlo test generate \
  --test ./templates/input-validation.ts \
  --host api.example.com \
  --endpoint /api/users \
  --method POST

Generate with Specific Version

metlo test generate \
  --test BOLA \
  --version 1 \
  --host api.example.com \
  --endpoint /api/users/123 \
  --method GET

Template Configuration

The TemplateConfig object provides access to:
  • authConfig - Authentication configuration per host
  • userConfig - User credentials for testing
  • entityMapping - Entity ID mappings for BOLA tests
interface TemplateConfig {
  authConfig?: Record<string, AuthConfig>
  userConfig?: Record<string, UserConfig>
  // ... other configuration
}

Validation

Metlo validates custom templates to ensure they:
  • Export a default object
  • Include name, version, and builder properties
  • Return a valid TestConfig from the builder

Best Practices

Increment version numbers when making changes:
export default {
  name: "CUSTOM_TEMPLATE",
  version: 2, // Incremented from 1
  builder: (endpoint, config) => {
    // Updated logic
  },
}
Choose clear template names that indicate what they test:
name: "API_RATE_LIMITING"
name: "SENSITIVE_DATA_EXPOSURE"
name: "CORS_MISCONFIGURATION"
Include descriptions to make failures clear:
.assert({
  type: "EQ",
  key: "resp.status",
  value: 403,
  description: "Non-admin users should be denied access",
})
Check for required configuration and throw helpful errors:
if (!endpoint.authConfig) {
  throw new Error(
    `No auth config defined for host: "${endpoint.host}". ` +
    `Add authentication configuration to use this template.`
  )
}
TypeScript catches errors before runtime:
# templates/custom-auth.ts instead of custom-auth.js
touch templates/custom-auth.ts

Sharing Templates

Share templates with your team:
  1. Version Control - Commit templates to your repository
  2. NPM Package - Publish as an npm package for easy distribution
  3. Documentation - Document what each template tests and when to use it

Next Steps

Writing Tests

Learn the YAML test format in detail

Running Tests

Execute your generated tests