{"id":32532577,"url":"https://github.com/cobbzilla/zilla-script","last_synced_at":"2026-07-04T19:31:20.765Z","repository":{"id":318098158,"uuid":"1070019480","full_name":"cobbzilla/zilla-script","owner":"cobbzilla","description":"JSON-driven REST API test framework with built-in session management and state capture","archived":false,"fork":false,"pushed_at":"2025-10-05T05:05:45.000Z","size":0,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-10-05T05:28:27.658Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cobbzilla.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-05T05:05:19.000Z","updated_at":"2025-10-05T05:05:56.000Z","dependencies_parsed_at":"2025-10-05T05:28:37.359Z","dependency_job_id":"d0427f9e-5b9f-4ed8-9e65-c8d5f4b6662d","html_url":"https://github.com/cobbzilla/zilla-script","commit_stats":null,"previous_names":["cobbzilla/zilla-script"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/cobbzilla/zilla-script","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cobbzilla%2Fzilla-script","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cobbzilla%2Fzilla-script/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cobbzilla%2Fzilla-script/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cobbzilla%2Fzilla-script/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cobbzilla","download_url":"https://codeload.github.com/cobbzilla/zilla-script/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cobbzilla%2Fzilla-script/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":281441023,"owners_count":26501758,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-28T02:00:06.022Z","response_time":60,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-10-28T12:53:25.883Z","updated_at":"2026-07-04T19:31:20.757Z","avatar_url":"https://github.com/cobbzilla.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# How to Write API Tests That Don't Suck\n\n## Introduction\nI tried everything it all sucks. None of the API testing frameworks did what I wanted in all cases.\n\nI need something that can run both in development environments and in CI.\n\nI need a test suite that's easily versioned and committed to source control.\n\nI need something that *just works*.\n\nI couldn't find anything that scratched my itches just right, so **zilla-script** is my answer to these questions.\n\n### Why Not Just Use [X]?\n\n**Postman/Insomnia**: Great for manual testing, terrible for CI/CD. Version control is painful, no programmatic control.\n\n**Supertest/Axios**: Imperative code. Every test becomes 50% boilerplate, 50% intent. State management is manual.\n\n**GraphQL/gRPC test tools**: Domain-specific. zilla-script works with any HTTP/REST API.\n\n**Cucumber/Gherkin**: Natural language is great for stakeholders, terrible for developers. BDD adds ceremony without adding value for API tests.\n\n**Raw test code**: Maximum flexibility, maximum pain. You end up reinventing zilla-script badly.\n\n### Philosophy\n\n1. **Tests are documentation**: Your test suite should read like API documentation\n2. **Declare intent, not implementation**: Describe what you're testing, not how to test it\n3. **State is explicit**: Variables and sessions are first-class concepts\n4. **Composition over inheritance**: Build complex tests from simple, reusable pieces\n5. **Escape hatches everywhere**: Custom handlers for when declarative isn't enough\n6. **Simplicity first**: Sensible defaults everywhere, facilitate minimalistic tests\n\n### Community\n\n- **GitHub**: [https://github.com/cobbzilla/zilla-script](https://github.com/cobbzilla/zilla-script)\n- **Report [Issues](https://github.com/cobbzilla/zilla-script/issues)**\n- **License**: [Apache License](LICENSE.txt)\n\n# zilla-script\nThe rest of this README is a high-level overview of what zilla-script is trying to\nsolve and how it does it. There is a [full guide document](GUIDE.md) that describes all the features\nof zilla-script in detail.\n\n## The Problem\n\nYou're testing a REST API. You need to:\n1. Make requests and validate things about the responses\n2. Create/read/update/delete some resources\n3. Authenticate, establish sessions, make requests from multiple sessions\n4. Chain requests using captured session tokens and IDs\n5. Upload files\n\nYour options:\n- **Postman collections**: Click-fest UI, version control nightmare, no real programming\n- **Imperative test code**: 200 lines of boilerplate for what should be 20 lines of intent\n- **Raw curl + bash**: Works until you need state management, then becomes spaghetti\n\nThere's a better way.\n\n## The Solution: Declarative API Testing\n\n**zilla-script** lets you write API tests as data structures. Your test describes *what* you want, not *how* to do it.\n\n### Before (Imperative and Ugly)\n\n```typescript\nit('should authenticate and fetch user data', async () =\u003e {\n  // Request auth code\n  const authRes = await fetch('http://localhost:3030/api/auth', {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify({ email: 'user@example.com' })\n  });\n  expect(authRes.status).to.equal(200);\n\n  // Simulate getting token from email (in real test: check mock mailbox)\n  const token = await getMockEmailToken('user@example.com');\n\n  // Verify token\n  const verifyRes = await fetch('http://localhost:3030/api/auth/verify', {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify({ token })\n  });\n  expect(verifyRes.status).to.equal(200);\n  const { session } = await verifyRes.json();\n  expect(session).to.exist;\n\n  // Use session to fetch account data\n  const accountRes = await fetch('http://localhost:3030/api/account', {\n    method: 'GET',\n    headers: {\n      'Content-Type': 'application/json',\n      'Cookie': `session=${session}`\n    }\n  });\n  expect(accountRes.status).to.equal(200);\n  const account = await accountRes.json();\n  expect(account.email).to.equal('user@example.com');\n});\n```\n\n### After (Declarative and Beautiful)\n\n```typescript\nexport const AuthFlow: ZillaScript = {\n  script: \"auth-flow\",\n  init: { servers: [{ base: \"http://localhost:3030/api/\" }] },\n  steps: [\n    {\n      step: \"request auth code\",\n      request: { post: \"auth\", body: { email: \"user@example.com\" } },\n      handlers: [{ handler: \"get_email_token\", params: { tokenVar: \"token\" } }]\n    },\n    {\n      step: \"verify and start session\",\n      request: { post: \"auth/verify\", body: { token: \"{{token}}\" } },\n      response: { session: { name: \"userSession\", from: { body: \"session\" } } }\n    },\n    {\n      step: \"fetch account data\",\n      request: { session: \"userSession\", get: \"account\" },\n      response: {\n        validate: [{ id: \"correct email\", check: [\"eq body.email 'user@example.com'\"] }]\n      }\n    }\n  ]\n};\n\n// Run it\nawait runZillaScript(AuthFlow);\n\n// Or, if you're using a test framework like mocha:\ndescribe(\"my API test\", async () =\u003e {\n    it(\"hits some endpoint and we get what we expect\", async () =\u003e runZillaScript(AuthFlow))\n})\n```\n\n**Result**: Half the code, zero boilerplate, 100% intent.\n\n## Why This Rocks\n\n### 1. **State Management Is Built-In**\n\nCapture values from responses, use them in subsequent requests:\n\n```typescript\n{\n  step: \"create post\",\n  request: { post: \"posts\", body: { title: \"Hello World\" } },\n  response: {\n    capture: { postId: { body: \"id\" } }  // JSONPath with implied $.\n  }\n},\n{\n  step: \"add comment\",\n  request: {\n    post: \"posts/{{postId}}/comments\",  // Use captured value\n    body: { text: \"Great post!\" }\n  }\n}\n```\n\n### 2. **Sessions Are Automatic**\n\nCapture a session once, use it everywhere:\n\n```typescript\nresponse: {\n  session: {\n    name: \"adminSession\",\n    from: { body: \"session.token\" }  // or header/cookie\n  }\n}\n\n// Later...\nrequest: {\n  session: \"adminSession\",  // Automatically sent in header/cookie\n  get: \"admin/users\"\n}\n```\n\n### 3. **Validations Are Data**\n\n```typescript\nvalidate: [\n    { id: \"status is active\", check: [\"eq body.status 'active'\"] },\n    { id: \"created recently\", check: [`gt body.createdAt ${Date.now() - 5000}`] },\n    { id: \"has items\", check: [\"notEmpty body.items\"] }\n]\n```\n\nAvailable checks: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `empty`, `notEmpty`, `null`, `notNull`, `undefined`, `notUndefined`, `startsWith`, `endsWith`, `includes`\n\n### 4. **Composition via Include**\n\nBreak complex flows into reusable modules:\n\n```typescript\nconst SignUp: ZillaScript = {\n  script: \"sign-up\",\n  sets: {\n      vars: [\"user\"],\n      sessions: [\"userSession\"],\n  },\n  steps: [/* ... signup steps that establish userSession and set user var ... */]\n};\n\nconst FullWorkflow: ZillaScript = {\n  script: \"full-workflow\",\n  steps: [\n    {\n        step: \"sign up user\",\n        include: SignUp,\n        params: { email: \"test@example.com\" }\n    },\n    { step: \"do stuff\", /* ... you can now send requests witht session: \"userSession\" and use the newly-defined 'user' variable */ }\n  ]\n};\n```\n\n### 5. **Custom Handlers for Complex Logic**\n\nWhen you need programmatic control:\n\n```typescript\nhandlers: [{\n  handler: \"check_database\",\n  params: {\n    query: \"SELECT count(*) FROM users WHERE email = ?\",\n    args: [\"{{userEmail}}\"],\n    expectedCount: 1\n  }\n}]\n```\n\nRegister handlers in your test setup:\n\n```typescript\nconst options: ZillaScriptOptions = {\n  init: {\n    handlers: {\n      check_database: async (ctx, params) =\u003e {\n        const count = await db.query(params.query, params.args);\n        if (count !== params.expectedCount) {\n          throw new Error(`Expected ${params.expectedCount}, got ${count}`);\n        }\n      }\n    }\n  }\n};\n```\n\n### 6. **Real-World Example: Guest Upload Flow**\n\nThis is a real test from our production API (simplified):\n\n```typescript\nexport const VisitGuestScript: ZillaScript = {\n  script: \"visit-guest\",\n  steps: [\n    {\n      step: \"visit location (scan QR code)\",\n      request: { get: \"visit/location/{{locationShortName}}\" },\n      handlers: [{\n        handler: \"new_appGuest_key\",\n        params: { var: \"guestKey\", authVar: \"guestAuth\", location: \"{{locationShortName}}\" }\n      }],\n      response: {\n        capture: { orgInfo: { body: null } },  // Capture entire body\n        validate: [\n          { id: \"org found\", check: [\"eq body.org.id orgId\"] },\n          { id: \"has logo\", check: [\"notEmpty body.assets.logo\"] }\n        ]\n      }\n    },\n    {\n      step: \"start guest session as minor (under 13)\",\n      request: {\n        post: \"visit/location/{{locationShortName}}\",\n        body: {\n          under13: true,\n          publicKey: \"{{guestAuth.publicKey}}\",\n          nonce: \"{{guestAuth.nonce}}\",\n          token: \"{{guestAuth.token}}\"\n        }\n      },\n      response: {\n        session: { name: \"guestSession\", from: { body: \"id\" } }\n      }\n    },\n    {\n      step: \"upload 3 photos as guest\",\n      loop: {\n        items: [\"photo1.jpg\", \"photo2.jpg\", \"photo3.jpg\"],\n        varName: \"filename\",\n        steps: [{\n          step: \"upload {{filename}}\",\n          request: {\n            session: \"guestSession\",\n            post: \"visit/location/{{locationShortName}}/asset\",\n            contentType: \"multipart/form-data\",\n            body: { file: \"{{filename}}\" }\n          }\n        }]\n      }\n    },\n    {\n      step: \"list uploaded photos\",\n      request: {\n        session: \"guestSession\",\n        get: \"visit/location/{{locationShortName}}/asset\"\n      },\n      response: {\n        capture: { photos: { body: null } },\n        validate: [{ id: \"3 photos uploaded\", check: [\"eq body.length 3\"] }]\n      }\n    },\n    {\n      step: \"delete first photo\",\n      request: {\n        session: \"guestSession\",\n        delete: \"visit/location/{{locationShortName}}/asset/{{photos.[0].id}}\"\n      }\n    },\n    {\n      step: \"verify deletion\",\n      request: {\n        session: \"guestSession\",\n        get: \"visit/location/{{locationShortName}}/asset\"\n      },\n      response: {\n        validate: [{ id: \"2 photos remain\", check: [\"eq body.length 2\"] }]\n      }\n    }\n  ]\n};\n```\n\nThis test:\n- Simulates scanning a QR code\n- Creates cryptographically signed guest credentials\n- Starts a session for a minor\n- Uploads files\n- Lists and deletes assets\n- Validates state throughout\n\nTry writing this imperatively. I'll wait.\n\n## Getting Started\n\n### Installation\n\n```bash\nnpm install zilla-script\n```\n\n### Basic Script\n\n```typescript\nimport { ZillaScript, runZillaScript } from \"zilla-script\";\n\nconst MyTest: ZillaScript = {\n  script: \"my-first-test\",\n  init: {\n    servers: [{\n      base: \"http://localhost:3000/api\",\n      session: { cookie: \"sessionId\" }\n    }],\n    vars: { username: \"testuser\", password: \"{{env.TEST_PASSWORD}}\" }\n  },\n  steps: [\n    {\n      step: \"login\",\n      request: {\n        post: \"auth/login\",\n        body: { username: \"{{username}}\", password: \"{{password}}\" }\n      },\n      response: {\n        session: { name: \"userSession\", from: { body: \"token\" } },\n        validate: [{ id: \"login success\", check: [\"eq body.success true\"] }]\n      }\n    },\n    {\n      step: \"get profile\",\n      request: { session: \"userSession\", get: \"user/profile\" },\n      response: {\n        validate: [{ id: \"correct username\", check: [\"eq body.username username\"] }]\n      }\n    }\n  ]\n};\n\n// Run with Mocha\ndescribe(\"API tests\", () =\u003e {\n  it(\"should login and fetch profile\", async () =\u003e {\n    await runZillaScript(MyTest, { env: process.env });\n  });\n});\n```\n\n## Advanced Features\n\n**Read the [full guide](GUIDE.md) for an exhaustive review.**\n\n### Multiple Servers\n\n```typescript\ninit: {\n  servers: [\n    { name: \"api\", base: \"http://localhost:3000/api\", session: { cookie: \"sid\" } },\n    { name: \"cdn\", base: \"http://localhost:4000\", session: { header: \"X-Token\" } }\n  ]\n}\n\n// Use in steps\nrequest: { server: \"cdn\", get: \"images/logo.png\" }\n```\n\n### Environment Variables in URLs\n\n```typescript\nservers: [{\n  base: \"http://{{env.API_HOST}}:{{env.API_PORT}}/api\"\n}]\n```\n\n### Extract from Headers/Cookies\n\n```typescript\nresponse: {\n  capture: {\n    rateLimitRemaining: { header: { name: \"X-RateLimit-Remaining\" } },\n    sessionCookie: { cookie: { name: \"connect.sid\" } }\n  }\n}\n```\n\n### Validation with Variables\n\n```typescript\nresponse: {\n  capture: { userId: { body: \"id\" } },\n  validate: [\n    { id: \"user id matches\", check: [\"eq body.owner.id userId\"] },\n    { id: \"header check\", check: [\"eq header.content_type 'application/json'\"] }\n  ]\n}\n```\n\n### Error Validation\n\n```typescript\nresponse: {\n  status: 422,\n  validate: [\n    { id: \"validation error\", check: [\"includes body.error 'invalid email'\"] }\n  ]\n}\n```\n\n### Loops\n\n```typescript\n{\n  step: \"create multiple users\",\n  loop: {\n    items: [\n      { name: \"Alice\", email: \"alice@example.com\" },\n      { name: \"Bob\", email: \"bob@example.com\" }\n    ],\n    varName: \"user\",\n    steps: [{\n      step: \"create {{user.name}}\",\n      request: {\n        post: \"users\",\n        body: { name: \"{{user.name}}\", email: \"{{user.email}}\" }\n      }\n    }]\n  }\n}\n```\n\n### Edit Variables\n\n```typescript\n{\n  step: \"update user object\",\n  edits: {\n    user: {\n      status: \"active\",\n      lastLogin: \"{{now}}\"\n    }\n  },\n  request: {\n    post: \"users/{{user.id}}\",\n    bodyVar: \"user\"  // Send entire modified user object\n  }\n}\n\n```\n```bash\nnpm install zilla-script\n```\n\nYour future self will thank you.\n\n---\n\n*Built by developers who got tired of API test boilerplate. Used in production to test a multi-tenant social platform with millions of API calls per day.*\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcobbzilla%2Fzilla-script","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcobbzilla%2Fzilla-script","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcobbzilla%2Fzilla-script/lists"}