Back to skills
SkillHub ClubShip Full StackFull StackBackendTesting

api-testing

Tool-based API testing with Postman and Bruno - collections, environments, test assertions, CI integration. Use when: postman, bruno, API testing, test API endpoint, API collection, HTTP request testing, endpoint validation.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
6
Hot score
82
Updated
March 20, 2026
Overall rating
C3.0
Composite score
3.0
Best-practice grade
B75.6

Install command

npx @skill-hub/cli install scientiacapital-skills-api-testing-skill

Repository

scientiacapital/skills

Skill path: active/api-testing-skill

Tool-based API testing with Postman and Bruno - collections, environments, test assertions, CI integration. Use when: postman, bruno, API testing, test API endpoint, API collection, HTTP request testing, endpoint validation.

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack, Backend, Testing, Integration.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: scientiacapital.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install api-testing into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/scientiacapital/skills before adding api-testing to shared team environments
  • Use api-testing for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: "api-testing"
description: "Tool-based API testing with Postman and Bruno - collections, environments, test assertions, CI integration. Use when: postman, bruno, API testing, test API endpoint, API collection, HTTP request testing, endpoint validation."
---

<objective>
Expert-level skill for tool-based API testing using Postman and Bruno. Covers collection organization, environment management, test scripting, response validation, and CI/CD integration.

This skill complements testing-skill (code-based tests) and api-design-skill (API structure). Use this when you need to test existing APIs with dedicated tools rather than writing programmatic tests.

Key distinction:
- testing-skill: Code-based tests (supertest, MSW, pytest requests)
- api-testing-skill: Tool-based tests (Postman, Bruno collections)
- api-design-skill: How to design APIs (structure, conventions)
</objective>

<quick_start>
**Postman Quick Test:**

```javascript
// Tests tab in Postman
pm.test("Status code is 200", function () {
    pm.response.to.have.status(200);
});

pm.test("Response has user data", function () {
    const json = pm.response.json();
    pm.expect(json).to.have.property("id");
    pm.expect(json).to.have.property("email");
});
```

**Bruno Quick Test:**

```javascript
// tests/get-user.bru
meta {
  name: Get User
  type: http
  seq: 1
}

get {
  url: {{baseUrl}}/api/users/{{userId}}
}

tests {
  test("should return 200", function() {
    expect(res.status).to.equal(200);
  });
}
```

**Environment Setup:**
```json
{
  "baseUrl": "https://api.example.com",
  "apiKey": "test_key_xxx"
}
```
</quick_start>

<success_criteria>
API testing is successful when:
- All endpoints have at least one happy path test
- Error cases tested (4xx, 5xx responses)
- Response schema validated (not just status codes)
- Environment variables used for all configurable values
- Collections organized by resource/domain
- Authentication flows tested end-to-end
- CI pipeline runs collections on every PR
- Test data is reproducible (fixtures or dynamic generation)
</success_criteria>

<tool_comparison>
## Postman vs Bruno

| Feature | Postman | Bruno |
|---------|---------|-------|
| Storage | Cloud/Local | Git-native (.bru files) |
| Collaboration | Team sync | Git branches |
| Pricing | Free tier + paid | Free and open source |
| Offline | Desktop app | Full offline |
| Scripting | JavaScript | JavaScript |
| CI/CD | Newman CLI | Bruno CLI |
| Schema | JSON | Plain text .bru |
| Best For | Teams, API documentation | Git workflows, privacy |

### When to Use Each

**Choose Postman when:**
- Team needs real-time collaboration
- API documentation is primary output
- Mock servers needed for frontend dev
- Complex OAuth flows with token refresh

**Choose Bruno when:**
- Git-native workflow preferred
- Privacy/self-hosting required
- Simpler test scenarios
- Developers prefer code-like syntax
</tool_comparison>

<collection_organization>
## Collection Structure

### Folder Hierarchy

```
my-api-tests/
├── auth/
│   ├── login.bru
│   ├── refresh-token.bru
│   └── logout.bru
├── users/
│   ├── create-user.bru
│   ├── get-user.bru
│   ├── update-user.bru
│   └── delete-user.bru
├── orders/
│   ├── create-order.bru
│   ├── get-orders.bru
│   └── cancel-order.bru
├── environments/
│   ├── local.bru
│   ├── staging.bru
│   └── production.bru
└── collection.bru
```

### Naming Conventions

| Element | Convention | Example |
|---------|------------|---------|
| Folders | kebab-case, plural | `users`, `auth-flows` |
| Requests | verb-noun | `create-user`, `get-orders` |
| Variables | camelCase | `{{baseUrl}}`, `{{authToken}}` |
| Environments | lowercase | `local`, `staging`, `production` |

### Request Ordering

Use sequence numbers for dependent requests:

```
1. auth/login.bru          (seq: 1)
2. users/create-user.bru   (seq: 2) - needs auth token
3. users/get-user.bru      (seq: 3) - uses created user ID
```
</collection_organization>

<test_patterns>
## Test Assertion Patterns

### Status Code Validation

```javascript
// Postman
pm.test("Success response", () => pm.response.to.have.status(200));
pm.test("Created response", () => pm.response.to.have.status(201));
pm.test("Not found", () => pm.response.to.have.status(404));

// Bruno
test("Success response", () => expect(res.status).to.equal(200));
```

### Response Body Validation

```javascript
// Postman
pm.test("Has required fields", function () {
    const json = pm.response.json();
    pm.expect(json).to.have.property("id");
    pm.expect(json.id).to.be.a("string");
    pm.expect(json.email).to.match(/^[\w-]+@[\w-]+\.\w+$/);
});

// Array validation
pm.test("Returns array of users", function () {
    const json = pm.response.json();
    pm.expect(json.users).to.be.an("array");
    pm.expect(json.users.length).to.be.greaterThan(0);
});
```

### Response Time Validation

```javascript
pm.test("Response time < 500ms", function () {
    pm.expect(pm.response.responseTime).to.be.below(500);
});
```

### Header Validation

```javascript
pm.test("Content-Type is JSON", function () {
    pm.response.to.have.header("Content-Type", /application\/json/);
});

pm.test("Has request ID", function () {
    pm.response.to.have.header("X-Request-Id");
});
```

### JSON Schema Validation (Postman)

```javascript
const schema = {
    type: "object",
    required: ["id", "name", "email"],
    properties: {
        id: { type: "string", format: "uuid" },
        name: { type: "string", minLength: 1 },
        email: { type: "string", format: "email" }
    }
};

pm.test("Schema is valid", function () {
    pm.response.to.have.jsonSchema(schema);
});
```
</test_patterns>

<environment_management>
## Environment Management

### Variable Scopes (Postman)

```
Global → Collection → Environment → Data → Local
(lowest priority)         (highest priority)
```

### Environment Files

**Postman environment.json:**
```json
{
    "id": "env-uuid",
    "name": "staging",
    "values": [
        { "key": "baseUrl", "value": "https://staging.api.com", "enabled": true },
        { "key": "apiKey", "value": "stg_key_xxx", "enabled": true, "type": "secret" }
    ]
}
```

**Bruno environment:**
```javascript
// environments/staging.bru
vars {
  baseUrl: https://staging.api.com
  apiKey: stg_key_xxx
}
```

### Dynamic Variables

```javascript
// Pre-request script - set dynamic values
const timestamp = Date.now();
const uniqueEmail = `test_${timestamp}@example.com`;

// Postman
pm.environment.set("uniqueEmail", uniqueEmail);
pm.environment.set("timestamp", timestamp);

// Bruno (in pre-request)
bru.setVar("uniqueEmail", uniqueEmail);
```

### Chaining Requests

```javascript
// Request 1: Login - save token
pm.test("Save auth token", function () {
    const json = pm.response.json();
    pm.environment.set("authToken", json.accessToken);
    pm.environment.set("userId", json.user.id);
});

// Request 2: Use saved token
// Headers: Authorization: Bearer {{authToken}}
// URL: {{baseUrl}}/users/{{userId}}
```
</environment_management>

<authentication_testing>
## Authentication Testing

### Bearer Token Flow

```javascript
// 1. Login request - Pre-request or separate request
// 2. Save token to environment
pm.environment.set("accessToken", pm.response.json().accessToken);

// 3. Use in subsequent requests
// Header: Authorization: Bearer {{accessToken}}
```

### API Key Authentication

```javascript
// Header-based
// X-API-Key: {{apiKey}}

// Query param
// GET {{baseUrl}}/endpoint?api_key={{apiKey}}
```

### OAuth 2.0 with Refresh

```javascript
// Pre-request script for token refresh
const tokenExpiry = pm.environment.get("tokenExpiry");
const now = Date.now();

if (!tokenExpiry || now > tokenExpiry) {
    pm.sendRequest({
        url: pm.environment.get("authUrl") + "/token",
        method: "POST",
        header: { "Content-Type": "application/x-www-form-urlencoded" },
        body: {
            mode: "urlencoded",
            urlencoded: [
                { key: "grant_type", value: "refresh_token" },
                { key: "refresh_token", value: pm.environment.get("refreshToken") },
                { key: "client_id", value: pm.environment.get("clientId") }
            ]
        }
    }, (err, res) => {
        if (!err) {
            const json = res.json();
            pm.environment.set("accessToken", json.access_token);
            pm.environment.set("tokenExpiry", now + (json.expires_in * 1000));
        }
    });
}
```

### Testing Auth Failures

```javascript
// Test unauthorized access
pm.test("Returns 401 without token", function () {
    pm.response.to.have.status(401);
});

// Test forbidden access
pm.test("Returns 403 for wrong role", function () {
    pm.response.to.have.status(403);
    pm.expect(pm.response.json().error).to.include("permission");
});
```
</authentication_testing>

<error_testing>
## Error Response Testing

### Validation Errors (400)

```javascript
pm.test("Returns validation error", function () {
    pm.response.to.have.status(400);

    const json = pm.response.json();
    pm.expect(json.error).to.equal("VALIDATION_ERROR");
    pm.expect(json.details).to.be.an("array");
    pm.expect(json.details[0]).to.have.property("field");
    pm.expect(json.details[0]).to.have.property("message");
});
```

### Not Found (404)

```javascript
pm.test("Returns 404 for missing resource", function () {
    pm.response.to.have.status(404);
    pm.expect(pm.response.json().error).to.equal("NOT_FOUND");
});
```

### Rate Limiting (429)

```javascript
pm.test("Rate limit headers present", function () {
    pm.response.to.have.header("X-RateLimit-Limit");
    pm.response.to.have.header("X-RateLimit-Remaining");
    pm.response.to.have.header("X-RateLimit-Reset");
});
```

### Server Errors (5xx)

```javascript
// Test graceful error handling
pm.test("Error response has request ID", function () {
    pm.expect(pm.response.json()).to.have.property("requestId");
});
```
</error_testing>

<ci_integration>
## CI/CD Integration

### Newman (Postman CLI)

```bash
# Install
npm install -g newman

# Run collection
newman run collection.json -e environment.json

# With reporters
newman run collection.json \
  -e staging.json \
  --reporters cli,junit \
  --reporter-junit-export results.xml

# Specific folder
newman run collection.json --folder "users"
```

### Bruno CLI

```bash
# Install
npm install -g @usebruno/cli

# Run collection
bru run --env staging

# Specific file
bru run users/create-user.bru --env local
```

### GitHub Actions Example

```yaml
name: API Tests

on:
  pull_request:
    branches: [main]

jobs:
  api-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Install Newman
        run: npm install -g newman newman-reporter-htmlextra

      - name: Run API Tests
        run: |
          newman run tests/api/collection.json \
            -e tests/api/ci.json \
            --reporters cli,htmlextra \
            --reporter-htmlextra-export report.html

      - name: Upload Report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: api-test-report
          path: report.html
```

### Environment Secrets in CI

```yaml
# Use GitHub Secrets for sensitive values
- name: Run API Tests
  env:
    API_KEY: ${{ secrets.STAGING_API_KEY }}
  run: |
    newman run collection.json \
      --env-var "apiKey=$API_KEY" \
      -e staging.json
```
</ci_integration>

<data_management>
## Test Data Management

### Data Files (Newman)

```json
// test-data.json
[
    { "email": "[email protected]", "name": "User One" },
    { "email": "[email protected]", "name": "User Two" },
    { "email": "[email protected]", "name": "User Three" }
]
```

```bash
# Run with data iterations
newman run collection.json -d test-data.json -n 3
```

### Dynamic Data Generation

```javascript
// Pre-request script
const faker = require("faker"); // Postman has built-in faker

pm.environment.set("randomEmail", pm.variables.replaceIn("{{$randomEmail}}"));
pm.environment.set("randomName", pm.variables.replaceIn("{{$randomFullName}}"));
pm.environment.set("randomUUID", pm.variables.replaceIn("{{$guid}}"));
```

### Built-in Dynamic Variables (Postman)

| Variable | Example Output |
|----------|----------------|
| `{{$guid}}` | `a8b2c3d4-e5f6-7890-abcd-ef1234567890` |
| `{{$timestamp}}` | `1612345678` |
| `{{$randomEmail}}` | `[email protected]` |
| `{{$randomInt}}` | `42` |
| `{{$randomFullName}}` | `John Smith` |

### Cleanup Scripts

```javascript
// Post-request script - cleanup created resources
if (pm.response.code === 201) {
    const createdId = pm.response.json().id;

    pm.sendRequest({
        url: pm.environment.get("baseUrl") + "/users/" + createdId,
        method: "DELETE",
        header: { "Authorization": "Bearer " + pm.environment.get("authToken") }
    }, (err, res) => {
        console.log("Cleanup: deleted user " + createdId);
    });
}
```
</data_management>

<checklist>
## API Testing Checklist

Before creating collection:
- [ ] API documentation reviewed
- [ ] Authentication method identified
- [ ] Base URLs for all environments defined
- [ ] Test data strategy determined

For each endpoint:
- [ ] Happy path test (expected input, expected output)
- [ ] Required field validation (400 errors)
- [ ] Authentication test (401 without token)
- [ ] Authorization test (403 wrong permissions)
- [ ] Not found test (404 invalid ID)
- [ ] Response schema validated
- [ ] Response time asserted

Collection organization:
- [ ] Requests grouped by resource
- [ ] Sequence numbers for dependent requests
- [ ] Environments for local/staging/production
- [ ] Sensitive values marked as secrets

CI integration:
- [ ] Newman/Bruno CLI configured
- [ ] GitHub Actions workflow created
- [ ] Test reports uploaded as artifacts
- [ ] Secrets stored in CI environment
</checklist>

<references>
For detailed patterns, load the appropriate reference:

| Topic | Reference File | When to Load |
|-------|----------------|--------------|
| Postman advanced patterns | `reference/postman-patterns.md` | Collections, scripting, monitors |
| Bruno workflow | `reference/bruno-patterns.md` | .bru files, git integration |
| Test case design | `reference/test-design.md` | Coverage strategies, edge cases |
| Test data strategies | `reference/data-management.md` | Fixtures, dynamic data, cleanup |
| CI/CD pipelines | `reference/ci-integration.md` | Newman, GitHub Actions, reporting |

**To load:** Ask for the specific topic or check if context suggests it.
</references>


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### reference/postman-patterns.md

```markdown
# Postman Advanced Patterns

## Collection Structure

### Folder Organization

```
My API Collection/
├── Setup/
│   ├── Health Check
│   └── Get Auth Token
├── Users/
│   ├── CRUD/
│   │   ├── Create User
│   │   ├── Get User
│   │   ├── Update User
│   │   └── Delete User
│   └── Search/
│       └── Search Users
├── Orders/
│   ├── Create Order
│   ├── Get Order
│   └── Cancel Order
└── Cleanup/
    └── Delete Test Data
```

### Collection Variables

```javascript
// Set at collection level
pm.collectionVariables.set("collectionId", "abc123");

// Access
const id = pm.collectionVariables.get("collectionId");
```

---

## Pre-request Scripts

### Token Refresh Pattern

```javascript
const tokenUrl = pm.environment.get("tokenUrl");
const refreshToken = pm.environment.get("refreshToken");
const tokenExpiry = pm.environment.get("tokenExpiry");

// Check if token needs refresh (1 minute buffer)
if (!tokenExpiry || Date.now() > (tokenExpiry - 60000)) {
    pm.sendRequest({
        url: tokenUrl,
        method: 'POST',
        header: {
            'Content-Type': 'application/json'
        },
        body: {
            mode: 'raw',
            raw: JSON.stringify({
                grant_type: 'refresh_token',
                refresh_token: refreshToken
            })
        }
    }, (err, response) => {
        if (err) {
            console.error('Token refresh failed:', err);
            return;
        }

        const json = response.json();
        pm.environment.set("accessToken", json.access_token);
        pm.environment.set("tokenExpiry", Date.now() + (json.expires_in * 1000));

        if (json.refresh_token) {
            pm.environment.set("refreshToken", json.refresh_token);
        }
    });
}
```

### Generate Unique Test Data

```javascript
// Using built-in dynamic variables
const uniqueId = pm.variables.replaceIn('{{$guid}}');
const timestamp = pm.variables.replaceIn('{{$timestamp}}');
const randomEmail = pm.variables.replaceIn('{{$randomEmail}}');

pm.environment.set("testUserId", uniqueId);
pm.environment.set("testEmail", `test_${timestamp}@example.com`);

// Using JavaScript
const randomString = Math.random().toString(36).substring(7);
pm.environment.set("testSlug", `test-${randomString}`);
```

### Conditional Execution

```javascript
// Skip request based on condition
const shouldSkip = pm.environment.get("skipAuthTests") === "true";

if (shouldSkip) {
    pm.execution.skipRequest();
}
```

---

## Test Patterns

### Comprehensive Response Validation

```javascript
pm.test("Status code is 200", function () {
    pm.response.to.have.status(200);
});

pm.test("Response time is acceptable", function () {
    pm.expect(pm.response.responseTime).to.be.below(2000);
});

pm.test("Response has correct content type", function () {
    pm.response.to.have.header("Content-Type");
    pm.expect(pm.response.headers.get("Content-Type")).to.include("application/json");
});

pm.test("Response body structure is valid", function () {
    const json = pm.response.json();

    pm.expect(json).to.be.an("object");
    pm.expect(json).to.have.property("data");
    pm.expect(json).to.have.property("meta");
});

pm.test("Data array is not empty", function () {
    const json = pm.response.json();
    pm.expect(json.data).to.be.an("array").that.is.not.empty;
});
```

### JSON Schema Validation

```javascript
const userSchema = {
    type: "object",
    required: ["id", "email", "name", "createdAt"],
    properties: {
        id: {
            type: "string",
            pattern: "^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$"
        },
        email: {
            type: "string",
            format: "email"
        },
        name: {
            type: "string",
            minLength: 1,
            maxLength: 255
        },
        createdAt: {
            type: "string",
            format: "date-time"
        },
        metadata: {
            type: "object",
            additionalProperties: true
        }
    },
    additionalProperties: false
};

pm.test("Response matches user schema", function () {
    const json = pm.response.json();
    pm.expect(tv4.validate(json, userSchema)).to.be.true;
});
```

### Chained Request Validation

```javascript
// In Create User request tests
pm.test("Save created user ID", function () {
    const json = pm.response.json();
    pm.expect(json).to.have.property("id");

    // Save for next request
    pm.environment.set("createdUserId", json.id);
    pm.environment.set("createdUserEmail", json.email);
});

// In Get User request tests
pm.test("Retrieved user matches created user", function () {
    const json = pm.response.json();
    const expectedEmail = pm.environment.get("createdUserEmail");

    pm.expect(json.email).to.equal(expectedEmail);
});
```

---

## Workflows

### Collection Runner Setup

```javascript
// Run specific folders in order
// 1. Setup (auth)
// 2. Create resources
// 3. Test CRUD operations
// 4. Cleanup

// In Setup/Get Auth Token - tests:
pm.test("Auth token saved", function () {
    const json = pm.response.json();
    pm.environment.set("authToken", json.token);
});

// In Cleanup request - tests:
pm.test("Cleanup successful", function () {
    pm.environment.unset("createdUserId");
    pm.environment.unset("testEmail");
});
```

### Error Recovery

```javascript
// Handle failed prerequisite
pm.test("Handle missing auth token", function () {
    if (!pm.environment.get("authToken")) {
        pm.execution.setNextRequest("Get Auth Token");
        return;
    }
    // Continue with normal tests
});
```

---

## Monitors

### Health Check Monitor

```javascript
// Scheduled to run every 5 minutes
pm.test("API is healthy", function () {
    pm.response.to.have.status(200);

    const json = pm.response.json();
    pm.expect(json.status).to.equal("healthy");
});

pm.test("Response time SLA", function () {
    pm.expect(pm.response.responseTime).to.be.below(500);
});

// Alert on failure (configure in Postman UI)
```

### Performance Baseline

```javascript
pm.test("Establish performance baseline", function () {
    const responseTime = pm.response.responseTime;

    // Log for trend analysis
    console.log(`Response time: ${responseTime}ms`);

    // Alert if degradation
    const baseline = 200; // ms
    const threshold = 1.5; // 50% degradation

    pm.expect(responseTime).to.be.below(baseline * threshold);
});
```

---

## Mock Servers

### Creating Mocks

1. Create collection with example responses
2. Enable mock server in Postman
3. Use mock URL for frontend development

### Example Response Setup

```javascript
// Save as example response for mock
{
    "id": "mock-user-123",
    "email": "[email protected]",
    "name": "Mock User",
    "createdAt": "2024-01-01T00:00:00Z"
}
```

### Dynamic Mock Responses

```javascript
// In mock server's example, use variables
{
    "id": "{{$guid}}",
    "email": "{{$randomEmail}}",
    "createdAt": "{{$isoTimestamp}}"
}
```

---

## Best Practices

### Variables Naming

```
// Environment-specific
baseUrl, apiKey, authToken

// Test data
testUserId, testEmail, createdResourceId

// Configuration
timeout, retryCount, skipCleanup
```

### Secret Management

```json
// Mark sensitive variables
{
    "key": "apiKey",
    "value": "secret_xxx",
    "type": "secret",  // Masks in UI
    "enabled": true
}
```

### Documentation in Tests

```javascript
pm.test("User creation returns 201 with user object", function () {
    // Verify:
    // 1. Status code indicates resource created
    // 2. Response includes auto-generated ID
    // 3. Email matches input (case-insensitive)
    // 4. Timestamps are set

    pm.response.to.have.status(201);

    const json = pm.response.json();
    pm.expect(json).to.have.property("id");
    pm.expect(json.email.toLowerCase())
        .to.equal(pm.environment.get("testEmail").toLowerCase());
    pm.expect(json).to.have.property("createdAt");
});
```

```

### reference/bruno-patterns.md

```markdown
# Bruno Patterns

Bruno is a Git-native API client. All requests are stored as `.bru` files that can be version controlled.

## File Structure

### Basic .bru File

```javascript
meta {
  name: Get User
  type: http
  seq: 1
}

get {
  url: {{baseUrl}}/api/users/{{userId}}
  body: none
  auth: bearer
}

auth:bearer {
  token: {{accessToken}}
}

headers {
  Accept: application/json
  X-Request-Id: {{$guid}}
}

tests {
  test("Status is 200", function() {
    expect(res.status).to.equal(200);
  });

  test("Has user data", function() {
    expect(res.body).to.have.property('id');
    expect(res.body).to.have.property('email');
  });
}
```

### POST Request with Body

```javascript
meta {
  name: Create User
  type: http
  seq: 2
}

post {
  url: {{baseUrl}}/api/users
  body: json
  auth: bearer
}

auth:bearer {
  token: {{accessToken}}
}

headers {
  Content-Type: application/json
}

body:json {
  {
    "email": "{{testEmail}}",
    "name": "Test User",
    "role": "member"
  }
}

script:pre-request {
  const timestamp = Date.now();
  bru.setVar("testEmail", `test_${timestamp}@example.com`);
}

tests {
  test("User created", function() {
    expect(res.status).to.equal(201);
  });

  test("Save user ID", function() {
    bru.setVar("createdUserId", res.body.id);
  });
}
```

---

## Project Structure

### Recommended Layout

```
my-api/
├── bruno.json           # Collection config
├── environments/
│   ├── local.bru
│   ├── staging.bru
│   └── production.bru
├── auth/
│   ├── login.bru
│   ├── refresh-token.bru
│   └── logout.bru
├── users/
│   ├── create-user.bru
│   ├── get-user.bru
│   ├── update-user.bru
│   ├── delete-user.bru
│   └── search-users.bru
├── orders/
│   ├── create-order.bru
│   ├── get-orders.bru
│   └── cancel-order.bru
└── _data/
    └── test-users.json
```

### bruno.json Configuration

```json
{
  "version": "1",
  "name": "My API Tests",
  "type": "collection",
  "ignore": [
    "node_modules",
    ".git"
  ]
}
```

---

## Environments

### Environment File (.bru)

```javascript
// environments/staging.bru
vars {
  baseUrl: https://staging.api.example.com
  apiVersion: v1
}

vars:secret [
  accessToken,
  apiKey
]
```

### Multiple Environments

```javascript
// environments/local.bru
vars {
  baseUrl: http://localhost:3000
  apiVersion: v1
  debug: true
}

// environments/production.bru
vars {
  baseUrl: https://api.example.com
  apiVersion: v1
  debug: false
}
```

### Using Environment Variables

```bash
# CLI with environment
bru run --env staging

# Override variables
bru run --env staging --env-var "baseUrl=https://custom.api.com"
```

---

## Scripting

### Pre-request Scripts

```javascript
script:pre-request {
  // Generate unique test data
  const uuid = require('uuid');
  bru.setVar("testId", uuid.v4());

  // Set timestamp
  bru.setVar("timestamp", Date.now());

  // Conditional logic
  if (!bru.getVar("accessToken")) {
    console.log("Warning: No access token set");
  }
}
```

### Post-response Scripts

```javascript
script:post-response {
  // Save response data
  if (res.status === 200) {
    bru.setVar("lastResponseId", res.body.id);
  }

  // Log for debugging
  console.log(`Response time: ${res.responseTime}ms`);
}
```

### Test Scripts

```javascript
tests {
  test("Status code is 200", function() {
    expect(res.status).to.equal(200);
  });

  test("Response has required fields", function() {
    expect(res.body).to.have.property('id');
    expect(res.body).to.have.property('email');
    expect(res.body.email).to.be.a('string');
  });

  test("Array response", function() {
    expect(res.body.users).to.be.an('array');
    expect(res.body.users.length).to.be.greaterThan(0);
  });

  test("Response time acceptable", function() {
    expect(res.responseTime).to.be.below(1000);
  });
}
```

---

## Authentication

### Bearer Token

```javascript
auth:bearer {
  token: {{accessToken}}
}
```

### API Key (Header)

```javascript
headers {
  X-API-Key: {{apiKey}}
}
```

### API Key (Query Param)

```javascript
get {
  url: {{baseUrl}}/api/data?api_key={{apiKey}}
}
```

### Basic Auth

```javascript
auth:basic {
  username: {{username}}
  password: {{password}}
}
```

### OAuth 2.0 Flow

```javascript
// auth/get-token.bru
meta {
  name: Get OAuth Token
  type: http
  seq: 1
}

post {
  url: {{authUrl}}/oauth/token
  body: formUrlEncoded
}

body:form-urlencoded {
  grant_type: client_credentials
  client_id: {{clientId}}
  client_secret: {{clientSecret}}
  scope: read write
}

tests {
  test("Save access token", function() {
    expect(res.status).to.equal(200);
    bru.setVar("accessToken", res.body.access_token);
    bru.setVar("tokenExpiry", Date.now() + (res.body.expires_in * 1000));
  });
}
```

---

## Request Chaining

### Sequential Execution

```javascript
// 1-login.bru (seq: 1)
meta {
  name: Login
  seq: 1
}

// Saves accessToken

// 2-create-user.bru (seq: 2)
meta {
  name: Create User
  seq: 2
}

// Uses {{accessToken}}, saves {{createdUserId}}

// 3-get-user.bru (seq: 3)
meta {
  name: Get User
  seq: 3
}

// Uses {{createdUserId}}
```

### Variable Passing

```javascript
// First request - tests section
tests {
  test("Save for next request", function() {
    bru.setVar("orderId", res.body.id);
    bru.setVar("orderTotal", res.body.total);
  });
}

// Second request - URL
get {
  url: {{baseUrl}}/api/orders/{{orderId}}/details
}
```

---

## CLI Usage

### Basic Commands

```bash
# Run all requests in collection
bru run

# Run with specific environment
bru run --env staging

# Run specific file
bru run users/create-user.bru

# Run specific folder
bru run users/

# Run with environment variable override
bru run --env staging --env-var "baseUrl=http://localhost:3000"
```

### CI/CD Integration

```bash
# Run and output results
bru run --env ci --output results.json

# Fail on first error
bru run --env ci --bail

# Run with timeout
bru run --env ci --timeout 30000
```

### Example GitHub Action

```yaml
name: API Tests

on: [push, pull_request]

jobs:
  api-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

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

      - name: Install Bruno CLI
        run: npm install -g @usebruno/cli

      - name: Run API Tests
        env:
          API_KEY: ${{ secrets.API_KEY }}
        run: |
          bru run --env ci \
            --env-var "apiKey=$API_KEY"
```

---

## Best Practices

### File Naming

```
# Good
create-user.bru
get-user-by-id.bru
search-users.bru
delete-user.bru

# Avoid
user1.bru
test.bru
new.bru
```

### Sequence Numbers

```javascript
// Use seq for dependent requests
meta { seq: 1 }  // Setup/auth
meta { seq: 2 }  // Create resources
meta { seq: 3 }  // Read/verify
meta { seq: 10 } // Cleanup (high number = runs last)
```

### Git Workflow

```bash
# .gitignore for Bruno
environments/local.bru    # Local overrides
*.local.bru               # Local files
.bruno/                   # Cache directory
```

### Secret Management

```javascript
// environments/staging.bru

// Public variables
vars {
  baseUrl: https://staging.api.com
  apiVersion: v1
}

// Secret variables (not committed to git)
vars:secret [
  accessToken,
  apiKey,
  clientSecret
]
```

### Documentation in Files

```javascript
meta {
  name: Create Order
  type: http
  seq: 3
}

docs {
  Creates a new order for the authenticated user.

  Prerequisites:
  - Valid access token (run login.bru first)
  - User must have "create_order" permission

  Expected response: 201 Created with order object
}

post {
  url: {{baseUrl}}/api/orders
  body: json
}
```

```

### reference/test-design.md

```markdown
# API Test Design

## Test Coverage Strategy

### Coverage Pyramid for APIs

```
          ┌─────────────────────┐
          │   E2E Workflows     │  ~10% - Full user journeys
          │   (Login → Action)  │
          ├─────────────────────┤
          │   Integration       │  ~30% - Multi-endpoint flows
          │   (Create → Get)    │
          ├─────────────────────┤
          │   Endpoint Tests    │  ~60% - Individual endpoints
          │   (CRUD per entity) │
          └─────────────────────┘
```

### Test Types

| Type | What It Tests | Example |
|------|---------------|---------|
| Smoke | API is alive | `GET /health` returns 200 |
| Functional | Correct behavior | `POST /users` creates user |
| Contract | Response structure | Response matches schema |
| Performance | Speed | Response < 500ms |
| Security | Auth/AuthZ | 401 without token |

---

## Test Case Design

### Happy Path Tests

For each endpoint, test the expected successful flow:

```javascript
// POST /api/users - Happy path
pm.test("Creates user with valid data", function () {
    pm.response.to.have.status(201);

    const json = pm.response.json();
    pm.expect(json).to.have.property("id");
    pm.expect(json.email).to.equal(pm.environment.get("testEmail"));
    pm.expect(json.createdAt).to.exist;
});
```

### Error Path Tests

| Error Type | HTTP Status | Test Focus |
|------------|-------------|------------|
| Validation | 400 | Missing/invalid fields |
| Authentication | 401 | Missing/invalid token |
| Authorization | 403 | Wrong permissions |
| Not Found | 404 | Invalid resource ID |
| Conflict | 409 | Duplicate creation |
| Rate Limit | 429 | Too many requests |
| Server Error | 5xx | Graceful error handling |

### Boundary Tests

```javascript
// Test string length limits
pm.test("Name max length enforced", function () {
    // Request body has 256 char name (limit is 255)
    pm.response.to.have.status(400);
    pm.expect(pm.response.json().details[0].field).to.equal("name");
});

// Test numeric limits
pm.test("Quantity must be positive", function () {
    // Request body has quantity: 0
    pm.response.to.have.status(400);
});

// Test array limits
pm.test("Max 100 items per request", function () {
    // Request body has 101 items
    pm.response.to.have.status(400);
});
```

---

## CRUD Test Matrix

### Standard Tests per Endpoint

| Operation | Method | Tests |
|-----------|--------|-------|
| Create | POST | Valid data → 201, Invalid → 400, Duplicate → 409, Unauthorized → 401 |
| Read | GET | Exists → 200, Not found → 404, Unauthorized → 401 |
| Update | PUT/PATCH | Valid → 200, Invalid → 400, Not found → 404, Unauthorized → 401 |
| Delete | DELETE | Exists → 204, Not found → 404, Unauthorized → 401 |
| List | GET | Empty → 200 (empty array), With data → 200 (array), Pagination works |

### Example: User CRUD Tests

```
users/
├── create-user-happy.bru       # 201 with valid data
├── create-user-invalid.bru     # 400 missing email
├── create-user-duplicate.bru   # 409 email exists
├── get-user-happy.bru          # 200 existing user
├── get-user-not-found.bru      # 404 invalid ID
├── update-user-happy.bru       # 200 with valid changes
├── update-user-invalid.bru     # 400 invalid email format
├── delete-user-happy.bru       # 204 existing user
└── delete-user-not-found.bru   # 404 invalid ID
```

---

## Response Validation

### Status Code Assertions

```javascript
// Success codes
pm.response.to.have.status(200);  // OK
pm.response.to.have.status(201);  // Created
pm.response.to.have.status(204);  // No Content

// Client errors
pm.response.to.have.status(400);  // Bad Request
pm.response.to.have.status(401);  // Unauthorized
pm.response.to.have.status(403);  // Forbidden
pm.response.to.have.status(404);  // Not Found
pm.response.to.have.status(409);  // Conflict
pm.response.to.have.status(422);  // Unprocessable Entity
pm.response.to.have.status(429);  // Too Many Requests
```

### Body Structure Assertions

```javascript
// Object structure
pm.test("Response has expected structure", function () {
    const json = pm.response.json();

    // Required fields
    pm.expect(json).to.have.all.keys("id", "email", "name", "createdAt");

    // Field types
    pm.expect(json.id).to.be.a("string");
    pm.expect(json.email).to.be.a("string");
    pm.expect(json.metadata).to.be.an("object");

    // Field formats
    pm.expect(json.id).to.match(/^[a-f0-9-]{36}$/);  // UUID
    pm.expect(json.email).to.match(/^[\w-]+@[\w-]+\.\w+$/);
});

// Array structure
pm.test("List response structure", function () {
    const json = pm.response.json();

    pm.expect(json).to.have.property("data");
    pm.expect(json.data).to.be.an("array");
    pm.expect(json).to.have.property("meta");
    pm.expect(json.meta).to.have.property("total");
    pm.expect(json.meta).to.have.property("page");
});
```

### Error Response Validation

```javascript
pm.test("Error response format", function () {
    const json = pm.response.json();

    // Standard error structure
    pm.expect(json).to.have.property("error");
    pm.expect(json).to.have.property("message");
    pm.expect(json).to.have.property("requestId");

    // Validation error specifics
    if (pm.response.code === 400) {
        pm.expect(json).to.have.property("details");
        pm.expect(json.details).to.be.an("array");

        json.details.forEach(detail => {
            pm.expect(detail).to.have.property("field");
            pm.expect(detail).to.have.property("message");
        });
    }
});
```

---

## Edge Cases

### Empty/Null Values

```javascript
// Test empty string
{ "name": "" }  // Should fail validation

// Test null
{ "name": null }  // Should fail or be ignored

// Test whitespace only
{ "name": "   " }  // Should fail validation
```

### Special Characters

```javascript
// Test Unicode
{ "name": "用户名" }  // Chinese characters

// Test emoji
{ "name": "Test 🎉" }  // Emoji in string

// Test injection attempts
{ "name": "'; DROP TABLE users; --" }  // SQL injection
{ "name": "<script>alert(1)</script>" }  // XSS
```

### Large Payloads

```javascript
// Test large strings
{ "description": "x".repeat(100000) }  // 100KB string

// Test many array items
{ "tags": Array(1000).fill("tag") }  // 1000 items

// Test deeply nested objects
{ "data": { "level1": { "level2": { ... } } } }
```

---

## Security Testing

### Authentication Tests

```javascript
// No token
pm.test("401 without token", function () {
    // Remove Authorization header
    pm.response.to.have.status(401);
});

// Invalid token
pm.test("401 with invalid token", function () {
    // Authorization: Bearer invalid_token_xxx
    pm.response.to.have.status(401);
});

// Expired token
pm.test("401 with expired token", function () {
    // Use pre-expired token
    pm.response.to.have.status(401);
});
```

### Authorization Tests

```javascript
// Wrong user's resource
pm.test("403 accessing other user's data", function () {
    // User A trying to access User B's resource
    pm.response.to.have.status(403);
});

// Wrong role
pm.test("403 without admin role", function () {
    // Regular user accessing admin endpoint
    pm.response.to.have.status(403);
});
```

### Input Validation Security

```javascript
// SQL injection attempt
pm.test("SQL injection blocked", function () {
    // Body: { "search": "'; DROP TABLE users; --" }
    pm.response.to.not.have.status(500);
    // Should either 400 or process safely
});

// XSS attempt
pm.test("XSS sanitized", function () {
    // Body: { "name": "<script>alert(1)</script>" }
    const json = pm.response.json();
    pm.expect(json.name).to.not.include("<script>");
});
```

---

## Performance Testing

### Response Time Thresholds

```javascript
// Individual endpoint
pm.test("Response under 500ms", function () {
    pm.expect(pm.response.responseTime).to.be.below(500);
});

// Tiered thresholds
pm.test("Response time acceptable", function () {
    const endpoint = pm.request.url.path.join("/");
    const thresholds = {
        "/health": 100,
        "/api/users": 500,
        "/api/reports": 2000
    };

    const threshold = thresholds[endpoint] || 1000;
    pm.expect(pm.response.responseTime).to.be.below(threshold);
});
```

### Load Testing Patterns

```javascript
// Data file iteration for load testing
// Run with: newman run collection.json -d users.json -n 100

// users.json
[
    { "email": "[email protected]" },
    { "email": "[email protected]" },
    // ... 100 users
]
```

---

## Test Naming Conventions

### Good Test Names

```javascript
// Pattern: [method] [endpoint] - [scenario] → [expected result]

pm.test("POST /users - valid data → 201 with user object", ...);
pm.test("POST /users - missing email → 400 validation error", ...);
pm.test("GET /users/:id - valid ID → 200 with user data", ...);
pm.test("GET /users/:id - invalid ID → 404 not found", ...);
pm.test("DELETE /users/:id - no auth → 401 unauthorized", ...);
```

### Folder/File Naming

```
users/
├── post-create-user-success.bru
├── post-create-user-invalid-email.bru
├── post-create-user-duplicate.bru
├── get-user-success.bru
├── get-user-not-found.bru
└── delete-user-success.bru
```

```

### reference/data-management.md

```markdown
# Test Data Management

## Data Strategies

### Strategy Selection

| Strategy | When to Use | Pros | Cons |
|----------|-------------|------|------|
| Static fixtures | Deterministic tests | Reproducible | Stale data risk |
| Dynamic generation | Uniqueness needed | Fresh data | Non-deterministic |
| API seeding | Complex relationships | Realistic | Slower setup |
| Database snapshots | Integration tests | Fast reset | Environment coupling |

---

## Static Fixtures

### JSON Data Files

```json
// fixtures/users.json
[
    {
        "email": "[email protected]",
        "name": "Test Admin",
        "role": "admin"
    },
    {
        "email": "[email protected]",
        "name": "Test User",
        "role": "member"
    },
    {
        "email": "[email protected]",
        "name": "Read Only User",
        "role": "viewer"
    }
]
```

### Using with Newman

```bash
# Run collection with data file
newman run collection.json -d fixtures/users.json

# Each iteration uses one data row
# Iteration 1: email = [email protected]
# Iteration 2: email = [email protected]
# Iteration 3: email = [email protected]
```

### Accessing Data Variables

```javascript
// In request body
{
    "email": "{{email}}",
    "name": "{{name}}",
    "role": "{{role}}"
}

// In tests
pm.test("Created user matches input", function () {
    const json = pm.response.json();
    pm.expect(json.email).to.equal(pm.iterationData.get("email"));
});
```

---

## Dynamic Data Generation

### Postman Built-in Variables

```javascript
// UUID
{{$guid}}  // "a1b2c3d4-e5f6-7890-abcd-ef1234567890"

// Timestamp
{{$timestamp}}  // "1612345678"
{{$isoTimestamp}}  // "2024-01-15T10:30:00.000Z"

// Random data
{{$randomInt}}  // 42
{{$randomEmail}}  // "[email protected]"
{{$randomFullName}}  // "John Smith"
{{$randomFirstName}}  // "John"
{{$randomLastName}}  // "Smith"
{{$randomPhoneNumber}}  // "+1-555-123-4567"
{{$randomCity}}  // "New York"
{{$randomCountry}}  // "United States"

// Random strings
{{$randomAlphaNumeric}}  // "a1b2c3"
{{$randomWords}}  // "lorem ipsum dolor"
```

### Pre-request Script Generation

```javascript
// Generate unique test data
const timestamp = Date.now();
const randomSuffix = Math.random().toString(36).substring(7);

pm.environment.set("testEmail", `test_${timestamp}@example.com`);
pm.environment.set("testUsername", `user_${randomSuffix}`);
pm.environment.set("testOrderId", `ORD-${timestamp}`);

// Generate realistic data
const names = ["Alice", "Bob", "Charlie", "Diana", "Eve"];
const randomName = names[Math.floor(Math.random() * names.length)];
pm.environment.set("testName", randomName);

// Generate valid phone number
const areaCode = Math.floor(Math.random() * 900) + 100;
const exchange = Math.floor(Math.random() * 900) + 100;
const subscriber = Math.floor(Math.random() * 9000) + 1000;
pm.environment.set("testPhone", `+1${areaCode}${exchange}${subscriber}`);
```

### Bruno Dynamic Variables

```javascript
script:pre-request {
  const uuid = require('uuid');

  bru.setVar("testId", uuid.v4());
  bru.setVar("testEmail", `test_${Date.now()}@example.com`);
  bru.setVar("testTimestamp", new Date().toISOString());
}
```

---

## Data Seeding

### API-Based Seeding

```javascript
// seed/create-test-user.bru
meta {
  name: Seed Test User
  seq: 1
}

post {
  url: {{baseUrl}}/api/admin/seed
  body: json
}

body:json {
  {
    "users": [
      {
        "email": "[email protected]",
        "password": "Test123!",
        "role": "admin"
      }
    ],
    "organizations": [
      {
        "name": "Test Org",
        "plan": "enterprise"
      }
    ]
  }
}

tests {
  test("Seeding successful", function() {
    expect(res.status).to.equal(200);
    bru.setVar("seededUserId", res.body.users[0].id);
    bru.setVar("seededOrgId", res.body.organizations[0].id);
  });
}
```

### Setup Collection Pattern

```
setup/
├── 01-reset-database.bru     # Clear test data
├── 02-create-admin.bru       # Create admin user
├── 03-create-test-org.bru    # Create organization
├── 04-create-test-users.bru  # Create test users
└── 05-verify-setup.bru       # Verify setup complete
```

---

## Environment-Specific Data

### Environment Variables

```javascript
// environments/local.bru
vars {
  baseUrl: http://localhost:3000
  adminEmail: admin@localhost
  testPassword: localtest123
}

// environments/staging.bru
vars {
  baseUrl: https://staging.api.com
  adminEmail: [email protected]
  testPassword: stagingtest123
}

// environments/ci.bru
vars {
  baseUrl: http://api:3000
  adminEmail: [email protected]
  testPassword: citest123
}
```

### Secret Management

```javascript
// Don't commit secrets - use CI environment variables
// environments/ci.bru
vars {
  baseUrl: http://api:3000
}

vars:secret [
  apiKey,
  adminPassword,
  testUserToken
]
```

```bash
# Set secrets via CLI
bru run --env ci \
  --env-var "apiKey=$API_KEY" \
  --env-var "adminPassword=$ADMIN_PASSWORD"
```

---

## Data Cleanup

### Post-Test Cleanup

```javascript
// In test's post-response script
script:post-response {
  // Cleanup created resource
  if (res.status === 201 && res.body.id) {
    const deleteUrl = `${bru.getVar('baseUrl')}/api/users/${res.body.id}`;

    // Mark for cleanup (actual deletion in cleanup request)
    const toDelete = bru.getVar('resourcesToDelete') || [];
    toDelete.push({ type: 'user', id: res.body.id });
    bru.setVar('resourcesToDelete', JSON.stringify(toDelete));
  }
}
```

### Cleanup Collection

```javascript
// cleanup/delete-test-resources.bru
meta {
  name: Delete Test Resources
  seq: 999  // Run last
}

script:pre-request {
  const resources = JSON.parse(bru.getVar('resourcesToDelete') || '[]');
  bru.setVar('cleanupResources', JSON.stringify(resources));
}

// Multiple DELETE requests in loop (Postman)
const resources = JSON.parse(pm.environment.get('resourcesToDelete') || '[]');

resources.forEach(resource => {
    pm.sendRequest({
        url: `${pm.environment.get('baseUrl')}/api/${resource.type}s/${resource.id}`,
        method: 'DELETE',
        header: {
            'Authorization': `Bearer ${pm.environment.get('authToken')}`
        }
    }, (err, res) => {
        if (err) {
            console.log(`Failed to delete ${resource.type} ${resource.id}:`, err);
        } else {
            console.log(`Deleted ${resource.type} ${resource.id}`);
        }
    });
});
```

### Database Reset (CI)

```yaml
# GitHub Actions
- name: Reset Test Database
  run: |
    docker exec postgres psql -U postgres -d test -c "TRUNCATE users, orders CASCADE;"

- name: Run API Tests
  run: newman run collection.json -e ci.json
```

---

## Data Isolation

### Test User Prefixes

```javascript
// Use consistent prefix for test data
const TEST_PREFIX = "test_";

pm.environment.set("testEmail", `${TEST_PREFIX}${Date.now()}@example.com`);
pm.environment.set("testUsername", `${TEST_PREFIX}user_${Date.now()}`);

// Easy to identify and clean up
// DELETE FROM users WHERE email LIKE 'test_%'
```

### Dedicated Test Tenant

```javascript
// environments/ci.bru
vars {
  tenantId: test-tenant-001
  baseUrl: https://api.example.com/test-tenant-001
}

// All test data isolated to this tenant
```

### Request Tagging

```javascript
// Add header to identify test requests
headers {
  X-Test-Request: true
  X-Test-Run-Id: {{testRunId}}
}

// Server can log/track test requests separately
```

---

## Best Practices

### Data Independence

```javascript
// BAD - Tests depend on each other
// Test 1 creates user
// Test 2 expects user from Test 1

// GOOD - Each test creates its own data
script:pre-request {
  // Create fresh test user for this test
  bru.setVar("testEmail", `test_${Date.now()}@example.com`);
}
```

### Deterministic IDs for Debugging

```javascript
// Use predictable IDs in specific tests
const testCaseId = "TC001";
const timestamp = Date.now();
pm.environment.set("testEmail", `${testCaseId}_${timestamp}@example.com`);

// Easy to trace: "[email protected]"
```

### Data Validation Before Use

```javascript
// Verify environment setup
pm.test("Environment configured correctly", function () {
    pm.expect(pm.environment.get("baseUrl")).to.exist;
    pm.expect(pm.environment.get("apiKey")).to.exist;
    pm.expect(pm.environment.get("testUserEmail")).to.exist;
});
```

### Idempotent Setup

```javascript
// Create-if-not-exists pattern
script:pre-request {
  const existingUserId = bru.getVar('testUserId');

  if (!existingUserId) {
    // User will be created by this request
    bru.setVar('shouldCreateUser', 'true');
  } else {
    // Skip creation, user already exists
    bru.setVar('shouldCreateUser', 'false');
  }
}
```

```

### reference/ci-integration.md

```markdown
# CI/CD Integration for API Testing

## Newman (Postman CLI)

### Installation

```bash
# Global install
npm install -g newman

# Project dependency
npm install --save-dev newman

# With reporters
npm install -g newman-reporter-htmlextra newman-reporter-junit
```

### Basic Usage

```bash
# Run collection
newman run collection.json

# With environment
newman run collection.json -e staging.json

# With data file
newman run collection.json -d test-data.json

# Multiple iterations
newman run collection.json -n 5

# Specific folder only
newman run collection.json --folder "Users"

# Delay between requests (ms)
newman run collection.json --delay-request 100

# Timeout settings
newman run collection.json --timeout-request 30000 --timeout-script 5000
```

### Reporters

```bash
# CLI output (default)
newman run collection.json

# JUnit for CI
newman run collection.json \
  --reporters junit \
  --reporter-junit-export results.xml

# HTML report
newman run collection.json \
  --reporters htmlextra \
  --reporter-htmlextra-export report.html

# Multiple reporters
newman run collection.json \
  --reporters cli,junit,htmlextra \
  --reporter-junit-export results.xml \
  --reporter-htmlextra-export report.html
```

### Exit Codes

| Code | Meaning |
|------|---------|
| 0 | All tests passed |
| 1 | Test failures |
| 2 | Invalid collection |
| 3 | Runtime error |

---

## Bruno CLI

### Installation

```bash
# Global install
npm install -g @usebruno/cli

# Via npx (no install)
npx @usebruno/cli run
```

### Basic Usage

```bash
# Run all requests
bru run

# With environment
bru run --env staging

# Specific file
bru run users/create-user.bru --env local

# Specific folder
bru run users/ --env staging

# Environment variable override
bru run --env staging --env-var "baseUrl=http://localhost:3000"

# Output results
bru run --env ci --output results.json
```

---

## GitHub Actions

### Basic Workflow

```yaml
name: API Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

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

    steps:
      - name: Checkout
        uses: actions/checkout@v4

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

      - name: Install Newman
        run: npm install -g newman newman-reporter-htmlextra

      - name: Run API Tests
        run: |
          newman run tests/api/collection.json \
            -e tests/api/ci.json \
            --reporters cli,htmlextra \
            --reporter-htmlextra-export report.html

      - name: Upload Report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: api-test-report
          path: report.html
          retention-days: 7
```

### With Secrets

```yaml
- name: Run API Tests
  env:
    API_KEY: ${{ secrets.STAGING_API_KEY }}
    AUTH_TOKEN: ${{ secrets.TEST_AUTH_TOKEN }}
  run: |
    newman run collection.json \
      -e ci.json \
      --env-var "apiKey=$API_KEY" \
      --env-var "authToken=$AUTH_TOKEN"
```

### With Service Container

```yaml
jobs:
  api-tests:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
          POSTGRES_DB: test
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

      api:
        image: my-api:test
        env:
          DATABASE_URL: postgres://test:test@postgres:5432/test
        ports:
          - 3000:3000

    steps:
      - uses: actions/checkout@v4

      - name: Wait for API
        run: |
          timeout 60 bash -c 'until curl -s http://localhost:3000/health; do sleep 2; done'

      - name: Run Tests
        run: |
          newman run collection.json \
            --env-var "baseUrl=http://localhost:3000"
```

### Matrix Testing (Multiple Environments)

```yaml
jobs:
  api-tests:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        environment: [staging, production-readonly]

    steps:
      - uses: actions/checkout@v4

      - name: Run Tests - ${{ matrix.environment }}
        run: |
          newman run collection.json \
            -e environments/${{ matrix.environment }}.json \
            --reporters cli,junit \
            --reporter-junit-export results-${{ matrix.environment }}.xml
```

---

## GitLab CI

### Basic Pipeline

```yaml
# .gitlab-ci.yml
stages:
  - test

api-tests:
  stage: test
  image: node:20

  before_script:
    - npm install -g newman newman-reporter-junit

  script:
    - newman run collection.json
        -e ci.json
        --reporters cli,junit
        --reporter-junit-export results.xml

  artifacts:
    when: always
    reports:
      junit: results.xml
    paths:
      - results.xml
    expire_in: 1 week
```

### With Variables

```yaml
api-tests:
  stage: test
  variables:
    API_KEY: $STAGING_API_KEY  # From GitLab CI/CD settings

  script:
    - newman run collection.json
        -e ci.json
        --env-var "apiKey=$API_KEY"
```

---

## CircleCI

### Basic Config

```yaml
# .circleci/config.yml
version: 2.1

jobs:
  api-tests:
    docker:
      - image: cimg/node:20.0

    steps:
      - checkout

      - run:
          name: Install Newman
          command: npm install -g newman

      - run:
          name: Run API Tests
          command: |
            newman run collection.json \
              -e ci.json \
              --reporters cli,junit \
              --reporter-junit-export results.xml

      - store_test_results:
          path: results.xml

      - store_artifacts:
          path: results.xml

workflows:
  test:
    jobs:
      - api-tests
```

---

## Jenkins

### Pipeline Script

```groovy
pipeline {
    agent any

    tools {
        nodejs 'node-20'
    }

    stages {
        stage('Setup') {
            steps {
                sh 'npm install -g newman newman-reporter-junit'
            }
        }

        stage('API Tests') {
            steps {
                withCredentials([string(credentialsId: 'api-key', variable: 'API_KEY')]) {
                    sh '''
                        newman run collection.json \
                            -e ci.json \
                            --env-var "apiKey=$API_KEY" \
                            --reporters cli,junit \
                            --reporter-junit-export results.xml
                    '''
                }
            }
            post {
                always {
                    junit 'results.xml'
                }
            }
        }
    }
}
```

---

## Reporting

### HTML Report (htmlextra)

```bash
newman run collection.json \
  --reporters htmlextra \
  --reporter-htmlextra-export report.html \
  --reporter-htmlextra-title "API Test Report" \
  --reporter-htmlextra-browserTitle "API Tests" \
  --reporter-htmlextra-showEnvironmentData \
  --reporter-htmlextra-showGlobalData
```

### JUnit for CI Integration

```bash
newman run collection.json \
  --reporters junit \
  --reporter-junit-export results.xml
```

### Custom JSON Output

```bash
newman run collection.json \
  --reporters json \
  --reporter-json-export results.json
```

### Combining Reporters

```bash
newman run collection.json \
  --reporters cli,junit,htmlextra \
  --reporter-junit-export junit-results.xml \
  --reporter-htmlextra-export detailed-report.html
```

---

## Best Practices

### Environment Configuration

```javascript
// ci.json - Minimal CI environment
{
    "name": "CI",
    "values": [
        {
            "key": "baseUrl",
            "value": "http://localhost:3000",
            "enabled": true
        },
        {
            "key": "timeout",
            "value": "30000",
            "enabled": true
        }
    ]
}
```

### Fail Fast

```bash
# Stop on first failure
newman run collection.json --bail
```

### Parallel Execution

```yaml
# GitHub Actions - parallel jobs
jobs:
  api-tests:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        folder: [users, orders, payments]

    steps:
      - name: Run ${{ matrix.folder }} tests
        run: |
          newman run collection.json \
            --folder "${{ matrix.folder }}" \
            -e ci.json
```

### Retry on Flaky Tests

```yaml
# GitHub Actions retry
- name: Run API Tests
  uses: nick-fields/retry@v2
  with:
    timeout_minutes: 10
    max_attempts: 3
    command: newman run collection.json -e ci.json
```

### Scheduled Health Checks

```yaml
# Run API health checks every hour
name: API Health Check

on:
  schedule:
    - cron: '0 * * * *'  # Every hour

jobs:
  health-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run Health Checks
        run: |
          newman run collection.json \
            --folder "Health Checks" \
            -e production.json
```

### Notifications

```yaml
# Slack notification on failure
- name: Notify Slack on Failure
  if: failure()
  uses: slackapi/slack-github-action@v1
  with:
    payload: |
      {
        "text": "API Tests Failed",
        "blocks": [
          {
            "type": "section",
            "text": {
              "type": "mrkdwn",
              "text": "API tests failed on `${{ github.ref_name }}`\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Run>"
            }
          }
        ]
      }
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
```

```

api-testing | SkillHub