{"id":24062128,"url":"https://github.com/loremipsum/mocking-hans","last_synced_at":"2025-04-23T13:19:06.645Z","repository":{"id":80321517,"uuid":"189008709","full_name":"loremipsum/mocking-hans","owner":"loremipsum","description":"You don't mock the Hans, he's mocking you.","archived":false,"fork":false,"pushed_at":"2024-11-08T09:48:07.000Z","size":271,"stargazers_count":24,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-23T13:18:51.505Z","etag":null,"topics":["api","development","express","fake","graphql","http","mock","mocking","rest","websocket"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/loremipsum.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","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}},"created_at":"2019-05-28T10:42:19.000Z","updated_at":"2024-11-08T09:48:04.000Z","dependencies_parsed_at":null,"dependency_job_id":"d337746c-ee55-4ef7-a9d5-b4ab4cc08e4d","html_url":"https://github.com/loremipsum/mocking-hans","commit_stats":{"total_commits":93,"total_committers":2,"mean_commits":46.5,"dds":0.3548387096774194,"last_synced_commit":"4cb040532b8960973c57bff9abd87c4dd309286c"},"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loremipsum%2Fmocking-hans","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loremipsum%2Fmocking-hans/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loremipsum%2Fmocking-hans/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/loremipsum%2Fmocking-hans/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/loremipsum","download_url":"https://codeload.github.com/loremipsum/mocking-hans/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250439295,"owners_count":21430824,"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","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":["api","development","express","fake","graphql","http","mock","mocking","rest","websocket"],"created_at":"2025-01-09T08:20:02.413Z","updated_at":"2025-04-23T13:19:06.616Z","avatar_url":"https://github.com/loremipsum.png","language":"TypeScript","readme":"# Mocking Hans\n\n[![npm version](https://badge.fury.io/js/%40loremipsum%2Fmocking-hans.svg)](https://badge.fury.io/js/%40loremipsum%2Fmocking-hans)\n[![Build Status](https://travis-ci.org/loremipsum/mocking-hans.svg?branch=master)](https://travis-ci.org/loremipsum/mocking-hans)\n[![Coverage Status](https://coveralls.io/repos/github/loremipsum/mocking-hans/badge.svg?branch=master)](https://coveralls.io/github/loremipsum/mocking-hans?branch=master)\n\n\u003e You don't mock the Hans, he's mocking you.\n\n## Features\n\n- Multi-port/app API mocking\n- Express, Socket.io and native WebSocket support\n- Local and app-shared global state\n- Middleware support (for faking authentication, ...)\n- Common Response objects\n- [Faker](https://github.com/marak/Faker.js/) integration\n\n## Installation\n\n1. Install Hans via npm/yarn `npm i @loremipsum/mocking-hans` / `yarn add @loremipsum/mocking-hans`\n\n2. Create your APIs (recommended in an `apps/` directory):\n\n```typescript\n// apps/Example.ts\n\nimport {Get, App, Response} from '@loremipsum/mocking-hans';\n\n@App({\n  name: 'example',\n  port: 4999,\n})\nexport class Example {\n  /**\n   * A simple text response\n   */\n  @Get('/')\n  index() {\n    return new Response('Hello there!');\n  }\n}\n```\n\n3. Hans can be started with either `./node_modules/.bin/hans apps` or add to your `scripts` section in your `package.json`:\n\n```json\n\"scripts\": {\n  \"hans\": \"hans apps\"\n}\n```\n\n\u003e It's recommended to install the [ts-node-dev](https://www.npmjs.com/package/ts-node-dev) package which automatically \n  reloads Hans on changes. Install the package and extend your change your hans script to:\n\n```json\n\"scripts\": {\n  \"start\": \"ts-node-dev ./node_modules/.bin/hans apps --disable-compilation\"\n}\n```\n\n_Note:_ The `--disable-compilation` flag is required when using custom compilation like via `ts-node-dev`.\n\n4. Create a `tsconfig.json`\n\n```json\n{\n  \"compilerOptions\": {\n    \"target\": \"es6\",\n    \"moduleResolution\": \"node\",\n    \"types\": [\n      \"node\",\n      \"ws\"\n    ],\n    \"experimentalDecorators\": true\n  }\n}\n```\n\n5. **Done**!\n\n### Application coupling\n\nDue to its packaged nature it's possible to ship Hans directly within the application (and its corresponding repository) \nconsuming the API. This could additionally be useful for running functional testing within a CI environment.\n\nTo make use of this simply follow the instructions above within the repository of your application _and_ install \nHans with the `--save-dev` (for npm) / `--dev` (for yarn) \nflag. The additional flags prevents Hans from being shipped in production.\n\n\u003e Keep in mind that running Hans this way requires you to pass the `--project \u003ctsconfig path\u003e.json` flag when starting \nHans to ensure the proper tsconfig to be used.\n\nIn case of an Angular application consuming the Twitter API your structure might look like this:\n\n```\nsrc/\n  // your angular application \napi-mock/\n  Twitter.ts    \u003c- Mocked Twitter API\n  tsconfig.json \u003c- tsconfig for Hans\n.gitignore\nREADME.md\npackage.json\n```\n\n## Usage\n\nBasically all features covered by Hans are implemented as an example within the `examples` directory. To run this \nexamples simply clone this repository, install the dependencies and run `npm run example`.\n\n### Apps\n\nApps represent a *single* API. For example, if you'd like to mock the Facebook and Twitter API you'd create two \ndifferent applications.\n\n#### Implementing Apps\n\nApps **must** be decorated with the `@App` decorator:\n\n```typescript\n@App({\n  name: 'twitter',\n  port: 61000\n})\nexport class Twitter {\n  @Get(\"/1.1/search/tweets.json\")\n  getTweets() {\n    return new JsonResponse({\n      \"statuses\": [\n        {\n          \"created_at\": \"Sun Feb 25 18:11:01 +0000 2018\",\n          \"id\": 967824267948773377,\n          \"id_str\": \"967824267948773377\",\n          \"text\": \"From pilot to astronaut, Robert H. Lawrence was the first African-American to be selected as an astronaut by any na… https://t.co/FjPEWnh804\",\n          \"truncated\": true\n        }\n      ]\n    })\n  }\n}\n```\n\nWhen manually creating the Hans instance apps needs to be registered to Hans; registered apps are passed to Hans when \ninstantiating the class, e.g.:\n\n```typescript\n(new Hans([Twitter])).bootstrap().then(() =\u003e {\n  console.log(chalk.blue(chalk.bold('\\nAre you ready to ... MOCK?\\n')));\n});\n```\n\nStarting Hans will now result in:\n\n```\n\u003e npm start\n\n    ✔ Started twitter on localhost:61000\n```\n\nThe `@App` options are:\n\n- `name` (string): Application name\n- `port` (string): Application port\n- `middleware` (Array\u003cstring\u003e): Used middleware (**only works for http routes!**)\n- `publicDirectory` (string): Path to the public directory of this application (defaults to `public`)\n- `configure`: Callback (`(container) =\u003e void`) for configuring the application\n\n#### Implementing interfaces\n\nEven though your app is loaded, it doesn't expose any interfaces yet. Interfaces are \nrepresented as single methods and need to be decorated with a proper decorator which represents the request \nmethod (`@Get`, `@Post`, `@Put` or `@Delete`). In \ncase of our Twitter app you may be going for an implementation like:\n\n```typescript\n@App({\n  name: 'twitter',\n  port: 61000\n})\nexport class Twitter {\n  @Get(\"/1.1/search/tweets.json\")\n  getTweets() {\n    return new JsonResponse({\n      \"statuses\": [\n        {\n          \"created_at\": \"Sun Feb 25 18:11:01 +0000 2018\",\n          \"id\": 967824267948773377,\n          \"id_str\": \"967824267948773377\",\n          \"text\": \"From pilot to astronaut, Robert H. Lawrence was the first African-American to be selected as an astronaut by any na… https://t.co/FjPEWnh804\",\n          \"truncated\": true,\n          // even more fields\n        }\n      ]\n    })\n  }\n}\n```\n\nGoing to `http://localhost:61000/1.1/search/tweets.json` will now return valid JSON containing\nthe given tweet.\n\n#### Sockets with socket.io\n\nEvery app is started alongside a socket server. Socket interfaces are implemented the same way \nas HTTP routes, but with the `@Socket` decorator:\n\n```typescript\n@Socket('connection')\nonConnect() {\n  console.log(`Someone connected to Example.`);\n}\n```\n\nThe `@Socket` decorator accepts two parameters:\n\n1. The event (in this case `connection`)\n2. The namespace (by default `/`)\n\nUsing sockets on your client requires the `socket.io-client` library to be installed.\n\n```html\n\u003cscript src=\"node_modules/socket.io-client/dist/socket.io.js\"\u003e\u003c/script\u003e\n\u003cscript\u003e\n  let socket = io.connect('http://localhost:61000');\n\n  socket.on('news', data =\u003e {\n    console.log(data);\n  });\n\u003c/script\u003e\n```\n\nSee `client-socketio.html` for a more detailed example.\n\n#### WebSockets\n\nWebSocket interfaces are implemented much like Sockets, but with the `@Websocket` decorator:\n\n```typescript\n@Websocket('connection')\nonConnect() {\n  console.log(`Someone connected to / via Websocket.`);\n}\n```\n\nThe `@Websocket` decorator accepts two parameters:\n\n1. The event (in this case `connection`)\n2. The topic or path (by default `/`)\n\nSee `client-websocket.html` for an example of a client using the WebSocket API.\n\n#### Configuration\n\nThe `@App` decorator makes it possible to configure your app on its lowest level; think of using custom middleware for\nExpress.\n\n```typescript\n@App({\n  name: 'example',\n  port: 4999,\n  configure: container =\u003e {\n    const express = container.get('express_app');\n    express.use(/** your middleware **/);\n  }\n})\n```\n\nThe `Container` does keep track of all applications and adapters (e.g. Express, SocketIO, ...):\n\n- `Container.get('http_server')`: the used HTTP server for this application\n- `Container.get('express_app')'`: the express instance for this application\n- `Container.get('io')`: the io instance for this application\n\n### API\n\n#### `@Get`, `@Post`, `@Put` and `@Delete`\n\nThese decorators expose a method as an interface through the desired request method. Methods decorated with one of these \ndo have access to the following params:\n\n1. `req` - The request object ([reference](http://expressjs.com/de/api.html#req))\n2. `res` - The response object ([reference](http://expressjs.com/de/api.html#res))\n3. `next` - A function which tells the server to continue handling routes (see Troubleshooting).\n4. `io` - The Socket.IO object ([reference](https://socket.io/docs/server-api/))\n\nAn example of these params can be found within the example app:\n\n```typescript\n@Get(\"/broadcast\")\nbroadcast(req, res, next, io) {\n  io.emit('news', {message: req.query.message, time: +(new Date())});\n  return new JsonResponse({success: 1});\n}\n```\n\nSending a HTTP GET to `/broadcast?message=foobar` emits the message `foobar` to all connected sockets.\n\n#### Responses\n\nFor convenience Hans provides several `Response` objects:\n\n##### - `JsonResponse`\n\nJSON formatted response.\n\n```typescript\nreturn new JsonResponse({\n  message: 'foo'\n});\n\n// Outputs:\n// {\n//   message: 'foo'\n// }\n```\n\n##### - `XmlFromJsonResponse`\n\nXML formatted response based on JSON.\n\n```typescript\nreturn new XmlFromJsonResponse({\n  message: 'foo'\n});\n\n// Outputs:\n// \u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?\u003e\n// \u003cmessage\u003efoo\u003c/message\u003e\n```\n\n##### - `Response`\n\nBasic text response. Additionally parent to all response objects.\n\n```typescript\nreturn new Response('Hello there');\n\n// Outputs:\n// Hello there\n```\n\n##### - `FileResponse`\n\nResponse from file.\n\n```typescript\nreturn new FileResponse('filename.ext');\n\n// Outputs the given file\n```\n\n\u003e Files are served from your public directory which must be specified when bootstraping Hans:\n\n```typescript\n(new Hans(apps)).bootstrap({ publicDirectory: 'public' })\n```\n\n##### - `TemplateResponse`\n\nResponse from placeholders. Content can be either a `string` or a `json` file (via `require`).\n\n```typescript\nreturn new TemplateResponse('hello %name%', { name: 'John' });\n\n// Outputs:\n// Hello John\n```\n\n##### Response settings\n\nAll `Response` objects do allow setting the status code and/or headers via the constructor:\n\n```typescript\nreturn new JsonResponse({ error: 'nope' }, 400);\n```\n\n### Middleware\n\nHans allows the usage of *middleware*, which is executed before an API (or an entire app) is called. An example \nwould be pseudo-authentication where the API would require proper authorizations headers:\n\n```typescript\nexport function IsAuthenticated(req: express.Request, res: express.Response, next: express.NextFunction) {\n  if (!req.headers.authorization) {\n    return res.status(403).json({error: 'You are not logged in!'});\n  }\n  next();\n}\n```\n\nThis middleware can be applied to a method by:\n\n```typescript\n@Get('/authenticated')\n@Middleware([IsAuthenticated])\nauthenticated() {\n  return new JsonResponse({\n    message: 'Hello there'\n  });\n}\n```\n\nOr to the entire application by passing it to the `@App` options:\n\n```typescript\n@App({\n  name: 'example',\n  port: 4999,\n  middleware: [IsAuthenticated]\n})\n```\n\nTrying to access your application will now most likely return an error due to not having proper headers set. If accessed\nvia\n\n```bash\ncurl -H \"Authorization: Token abcd\" http://localhost:4999/authenticated \n``` \n\nthe API will return a friendly _\"Hello there\"_.\n\nSince middlewares are passed as an array it's possible to attach as many middlewares per app or method as you'd like to. \nExecution order is based on the order in the array.\n\n\u003e **Important:** Middleware does ___not___ evaluate `Response` objects, meaning it's __your__ responsibility to take care \nof sending a response. This can either be done by calling either `return res.send(your response)` or `next()`. Sending a \nresponse yourself will prevent further actions while `next` simply continues the request. Neither calling \n`res.send` nor `next` will result in a stuck request!\n\n### State\n\nHans does implement a _very_ simple basic state (which is basically just a `Map`). There's two different states \nwhich can be useful for you:\n\n#### Application / local state\n\nThe application or local state can simply be implemented with class properties:\n\n```typescript\nexport class Example {\n  private localState: State = new State();\n  \n  @Get('/state')\n  stateExample() {\n    let counter = this.localState.get('counter', 0);\n\n    this.localState.set('counter', counter + 1);\n\n    return new JsonResponse({\n      localState: this.localState.get('counter'),\n    });\n  }\n}\n```\n\nThis state will persevere throughout all requests as long as Hans is not restarted .\n\n#### Hans-wide state\n\nAdditionally to the local state Hans implements its own state which is available across _all_ registered applications. \nThis state is automatically injected into every applications constructor first argument:\n\n```typescript\nexport class Example {\n  constructor(private globalState: State) {\n  }\n    \n  @Get('/state')\n  stateExample() {\n    let globalCounter = this.globalState.get('counter', 0);\n    \n    this.globalState.set('counter', globalCounter + 1);\n\n    return new JsonResponse({\n      globalState: this.globalState.get('counter')\n    });\n  }\n}\n```\n\nThis state will persevere throughout all requests as long as Hans is not restarted and is shared across all applications.\n\n### Faker and Utility\n\nHans integrates [Faker](https://github.com/marak/Faker.js/) by default, providing useful methods for generating fake\ndata.\n\nAdditionally there's a custom utility helper for biased random elements based on a given probability:\n\n```typescript\nconst elements = [{\n  element: 'foo',\n  probability: 0.2\n}, {\n  element: 'bar',\n  probability: 0.7\n}, {\n  element: 'lorem',\n  probability: 0.1\n}];\n\nconst e = Helper.getRandomElementByProbability(elements);\n// 70% chance for 'bar', 20% chance for 'foo', 10% chance for 'lorem'\n```\n\n## Troubleshooting \n\n### Responses take forever\n\nFor API methods: most likely you've forgotten to wrap your response in a `Response` object. To prevent stuck requests \n\n- return a `Response` object\n- call `res.send` by yourself\n- call `next()` by yourself\n\nThe same also applies for middleware __but__ keep in mind that middleware does not support `Response` objects, which means \nreturning a `Response` object would not work and you'd call `res.send` or `next` by yourself.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Floremipsum%2Fmocking-hans","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Floremipsum%2Fmocking-hans","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Floremipsum%2Fmocking-hans/lists"}