{"id":46310559,"url":"https://github.com/hungdv136/rio","last_synced_at":"2026-03-04T13:07:41.970Z","repository":{"id":165105763,"uuid":"640490062","full_name":"hungdv136/rio","owner":"hungdv136","description":"Flexible HTTP mocking in Golang","archived":false,"fork":false,"pushed_at":"2025-05-10T05:00:12.000Z","size":260,"stargazers_count":34,"open_issues_count":1,"forks_count":4,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-05-10T05:28:38.637Z","etag":null,"topics":["golang","http-mocking","microservices-testing","unit-test"],"latest_commit_sha":null,"homepage":"","language":"Go","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/hungdv136.png","metadata":{"files":{"readme":"README.md","changelog":null,"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":"2023-05-14T09:08:52.000Z","updated_at":"2025-05-10T05:00:15.000Z","dependencies_parsed_at":null,"dependency_job_id":"af9f9176-c68f-4d7a-8aa4-261f006ac26b","html_url":"https://github.com/hungdv136/rio","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/hungdv136/rio","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hungdv136%2Frio","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hungdv136%2Frio/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hungdv136%2Frio/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hungdv136%2Frio/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hungdv136","download_url":"https://codeload.github.com/hungdv136/rio/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hungdv136%2Frio/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30081262,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-04T12:28:08.313Z","status":"ssl_error","status_checked_at":"2026-03-04T12:27:28.210Z","response_time":59,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":["golang","http-mocking","microservices-testing","unit-test"],"created_at":"2026-03-04T13:07:41.422Z","updated_at":"2026-03-04T13:07:41.959Z","avatar_url":"https://github.com/hungdv136.png","language":"Go","readme":"# A flexible declarative HTTP mocking framework in Golang\n\n![ci](https://github.com/hungdv136/rio/workflows/ci/badge.svg)\n[![Go Report Card](https://goreportcard.com/badge/github.com/hungdv136/rio)](https://goreportcard.com/report/github.com/hungdv136/rio)\n[![Go Reference](https://pkg.go.dev/badge/github.com/hungdv136/rio.svg)](https://pkg.go.dev/github.com/hungdv136/rio)\n\n- [A lightweight declarative HTTP mocking framework in Golang](#a-lightweight-declarative-http-mocking-framework-in-golang)\n  - [Introduction](#introduction)\n  - [Features](#features)\n  - [How it works](#how-it-works)\n  - [How to use in unit test for Golang](#how-to-use-in-unit-test-for-golang)\n    - [Prerequisites](#prerequisites)\n    - [Install](#install)\n    - [Usage](#usage)\n  - [How to use in integration test](#how-to-use-in-integration-test)\n    - [Deploy `Rio` as a stand-alone service](#deploy-rio-as-a-stand-alone-service)\n    - [Change the root url configuration of the external to mock server](#change-the-root-url-configuration-of-the-external-to-mock-server)\n    - [Perform manual test case](#perform-manual-test-case)\n    - [Write an automation test cases](#write-an-automation-test-cases)\n  - [Request Matching](#request-matching)\n    - [Match by method and url](#match-by-method-and-url)\n    - [Match by query parameter](#match-by-query-parameter)\n    - [Match by cookies](#match-by-cookies)\n    - [Match by request body](#match-by-request-body)\n      - [JSON Path](#json-path)\n      - [XML Path](#xml-path)\n      - [Multipart](#multipart)\n      - [URL Encoded Form (application/x-www-form-urlencoded)](#url-encoded-form-applicationx-www-form-urlencoded)\n    - [Matching Operators](#matching-operators)\n  - [Response Definition](#response-definition)\n    - [Status Code, Cookies, Header](#status-code-cookies-header)\n    - [Response body](#response-body)\n      - [JSON](#json)\n      - [XML](#xml)\n      - [HTML](#html)\n      - [Stream/Binary](#streambinary)\n    - [Redirection](#redirection)\n    - [Reserve proxy and recording](#reserve-proxy-and-recording)\n    - [Mock a download API](#mock-a-download-api)\n  - [Create stubs using Postman](#create-stubs-using-postman)\n    - [JSON Format](#json-format)\n    - [YAML](#yaml)\n  - [Advance Features](#advance-features)\n    - [Support priority response](#support-priority-response)\n    - [Delay response](#delay-response)\n    - [Deactivate stub when matched](#deactivate-stub-when-matched)\n    - [Namespace](#namespace)\n    - [Dynamic response](#dynamic-response)\n  - [Mocking GRPC](#mocking-grpc)\n    - [Define a proto](#define-a-proto)\n    - [Define stub](#define-stub)\n    - [Mocking GRPC error response](#mocking-grpc-error-response)\n    - [Change the root url to rio](#change-the-root-url-to-rio)\n  - [How to deploy](#how-to-deploy)\n    - [Setup database](#setup-database)\n    - [Deploy file storage](#deploy-file-storage)\n      - [Use S3](#use-s3)\n      - [Use GCS](#use-gcs)\n    - [Deploy HTTP mock server](#deploy-http-mock-server)\n    - [Deploy GRPC mock server](#deploy-grpc-mock-server)\n    - [Configure cache](#configure-cache)\n  - [Contribution](#contribution)\n    - [Run test](#run-test)\n    - [Commit Changes](#commit-changes)\n\n## Introduction\n\nRio is a declarative HTTP mocking library for unit test in Golang and HTTP/gPRC mock server for integration test. Using the same framework for both kind of tests can help to share stub definition schema or codes between developers and testers easily. This framework has been used for thousands of test cases internally for a long time ago, but it just has been published recently (Rio is a variant of parrot)\n\n## Features\n\n- Fast, simple and fluent API for unit test in Golang \n- DSL in YAML/JSON format for stub declarations\n- Supports wide-range response types (html, xml, json and binary)\n- Can be deployed as mock server (HTTP and gRPC) for integration test\n- Supports persistent stubs to database with caching to improve performance\n- Flexible for matching request by method, URL params, headers, cookies and bodies\n- Dynamic response with go-template\n- Automatically generates stubs with reserve proxy mode\n- Ability to run tests in parallel to improve speed\n- Support SDK in Golang and TypeScript/Javascript\n\n## How it works\n\n![Workflow](docs/flow.png)\n\n## How to use in unit test for Golang\n\nSuppose that we want to test a function that calls API and parse the response data as the following example\n\n```go\nfunc CallAPI(ctx context.Context, rootURL string, input map[string]interface{}) (map[string]interface{}, error) {\n\tbodyBytes, err := json.Marshal(input)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, rootURL+\"/animal\", bytes.NewReader(bodyBytes))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tres, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdata := map[string]interface{}{}\n\tdecoder := json.NewDecoder(res.Body)\n\tif err := decoder.Decode(\u0026data); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn data, nil\n}\n```\n\n### Prerequisites\n\nGolang 1.18+\n\n### Install\n\n```bash\ngo get github.com/hungdv136/rio@latest\n```\n\nNo deployment is required for unit test\n\n### Usage \n\nWrite unit test with Golang\n\n```go \nfunc TestCallAPI(t *testing.T) {\n\tt.Parallel()\n\n\tctx := context.Background()\n\tserver := rio.NewLocalServerWithReporter(t)\n\n\tt.Run(\"success\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tanimalName := uuid.NewString()\n\t\treturnedBody := map[string]interface{}{\"id\": uuid.NewString()}\n\n\t\trequire.NoError(t, rio.NewStub().\n\t\t\t// Verify method and path\n\t\t\tFor(\"POST\", rio.EndWith(\"/animal\")).\n\t\t\t// Verify if the request body is composed correctly\n\t\t\tWithRequestBody(rio.BodyJSONPath(\"$.name\", rio.EqualTo(animalName))).\n\t\t\t// Response with 200 (default) and JSON\n\t\t\t// Body can be map, struct or JSON string\n\t\t\tWillReturn(rio.JSONResponse(returnedBody)).\n\t\t\t// Submit stub to mock server\n\t\t\tSend(ctx, server))\n\n\t\tinput := map[string]interface{}{\"name\": animalName}\n\t\tresData, err := CallAPI(ctx, server.GetURL(ctx), input)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, returnedBody, resData)\n\t})\n\n\tt.Run(\"bad_request\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tanimalName := uuid.NewString()\n\n\t\trequire.NoError(t, rio.NewStub().\n\t\t\t// Verify method and path\n\t\t\tFor(\"POST\", rio.EndWith(\"/animal\")).\n\t\t\t// Verify if the request body is composed correctly\n\t\t\tWithRequestBody(rio.BodyJSONPath(\"$.name\", rio.EqualTo(animalName))).\n\t\t\t// Response with status 400 and empty body JSON\n\t\t\tWillReturn(rio.NewResponse().WithStatusCode(400)).\n\t\t\t// Submit stub to mock server\n\t\t\tSend(ctx, server))\n\n\t\tinput := map[string]interface{}{\"name\": animalName}\n\t\tresData, err := CallAPI(ctx, server.GetURL(ctx), input)\n\t\trequire.Error(t, err)\n\t\trequire.Empty(t, resData)\n\t})\n}\n\n```\n\n[Examples](https://github.com/hungdv136/rio_examples)\n\n## How to use in integration test\n\nSuppose that we want to test (manual or automation) an API that calls an external API by simulating a mock response for that external API. It can help us to create stable tests by isolating our test suites with external systems\n\nGolang, TypeScript, Postman can be used to define and submit stubs to mock server. [This repository](https://github.com/hungdv136/rio-js) illustrates how to use Rio to write integration tests in Javascript/TypeScript\n\n### Deploy `Rio` as a stand-alone service\n\nSee [deploy](#how-to-deploy). After deployed, Rio can be accessed by other services via a domain, for example `http://rio-domain`\n\n### Change the root url configuration of the external to mock server\n\nGo to ENV management system to change the root URL to the mock server with the format: `http://rio-domain/echo` (Must include **`/echo`** at the end)\n\n### Perform manual test case \n\n1. [Use Postman to submit stubs](#create-stubs-using-postman)\n2. Use Postman to perform manual test with your API\n\n### Write an automation test cases\n\n1. Create a new server\n\nThis struct is used to connect with the remote server that we have deployed above, so we should provide the root url of that mock server when initializing the remote server struct\n\n```go \nserver := rio.NewRemoteServer(\"http://rio-server\")\n```\n\n```ts\nimport { Server } from 'rio-ts-sdk'\nserver := Server('http://rio-server')\n```\n\n2. Define a stub\n\n```go\nresData :=types.Map{\"data\": uuid.NewString(),\"verdict\": \"success\"}\nstub := rio.NewStub().\n\t\tFor(\"GET\", rio.Contains(\"animal/create\")).\n\t\tWithHeader(\"X-REQUEST-ID\", rio.Contains(\"\u003cx-request-id\u003e\")).\n\t\tWithQuery(\"search_term\", rio.EqualTo(\"\u003csearch-value\u003e\")).\n\t\tWithCookie(\"SESSION_ID\", rio.EqualTo(\"\u003ccookie-value\u003e\")).\n\t\tWillReturn(rio.JSONResponse(resData)).\n    Send(ctx, server)\n```\n\n```ts\nimport { Stub, Rule, JSONResponse } from 'rio-ts-sdk'\n\nresData :={data: uuidv4(), verdict: \"success\"};\nstub := new Stub(\"GET\", Rule.contains(\"animal/create\"))\n  .withHeader(\"X-REQUEST-ID\", Rule.contains('\u003cx-request-id\u003e'))\n  .withQuery(\"search_term\", Rule.equalsTo('\u003csearch-value\u003e'))\n  .withCookie(\"SESSION_ID\", Rule.equalsTo('\u003ccookie-value\u003e'))\n  .willReturn(JSONResponse(resData))\n  .send(ctx, server);\n```\n\nIn the above example, the stub will be pushed to remote server via `stub/create_many` API. This should be done before performing a request to the test target service. Since the root url of the external service is switched to `Rio` service, the request will be routed to `Rio` service. Once a request comes, a generic handler in remote server will validate the following information\n\n- Validate method GET\n- Validate whether request's path contains `animal/create`\n- Validate query string `search_term` whether its value contains a predefined value\n- Validate X-Request-ID whether its value equals to a predefined value\n- Validate cookie SESSION_ID whether its value equals to a predefined value\n- If these conditions are matched, then return with predefined response\n\n## Request Matching\n\nThis is to verify incoming requests against predefined stubs. If all rules are matched, then the predefined response of matched stub will be responded\n\n### Match by method and url\n\n```go\nNewStub().For(\"GET\", Contains(\"/helloworld\"))\n```\n\n```ts\nnew Stub(\"GET\", Rule.contains(\"/helloworld\"))\n```\n\n```json\n{\n  \"request\": {\n    \"method\": \"GET\",\n    \"url\": [{\n      \"name\": \"contains\",\n      \"value\": \"/helloworld\"\n    }]\n  }\n}\n```\n\n### Match by query parameter\n\n```go\nNewStub().WithQuery(\"search_term\", NotEmpty())\n```\n\n```ts\nnew Stub(\"GET\", Rule.contains(\"/helloworld\"))\n  .withQuery(\"search_term\", Rule.notEmpty())\n```\n\n```json\n{\n  \"request\": {\n    \"query\": [{\n      \"field_name\": \"search_term\",\n      \"operator\": {\n        \"name\": \"not_empty\"\n      }\n    }]\n  }\n}\n```\n\n### Match by cookies\n\n```go\nNewStub().WithCookie(\"SESSION_ID\", EqualTo(\"expected cookie value\"))\n```\n\n```ts\nnew Stub(\"GET\", Rule.contains(\"/helloworld\"))\n  .withCookie(\"SESSION_ID\", Rule.equalsTo(\"expected cookie value\"))\n```\n\n```json\n{\n  \"request\": {\n    \"cookie\": [{\n      \"field_name\": \"SESSION_ID\",\n      \"operator\": {\n        \"name\": \"equal_to\",\n        \"value\": \"expected cookie value\"\n      }\n    }]\n  }\n}\n```\n\n### Match by request body\n\n#### JSON Path\n\n```go\nNewStub().WithRequestBody(BodyJSONPath(\"$.name\"), NotEmpty())\n```\n\n```ts\nnew Stub('GET', Rule.endWith('/helloworld'))\n  .withRequestBody(\n    JSONPathRule(\"$.name\", Rule.notEmpty()),\n    JSONPathRule(\"$.count\", Rule.equalsTo(3000))\n  )\n```\n\n```json\n{\n  \"request\": {\n    \"body\": [{\n      \"content_type\":  \"application/json\",\n      \"operator\": {\n        \"name\": \"not_empty\"\n      },\n      \"key_path\": \"$.name\"\n    }] \n  }\n}\n```\n\n#### XML Path \n\n```go\nNewStub().WithRequestBody(BodyXMLPath(\"//book/title\"), NotEmpty())\n```\n\n```json\n{\n  \"request\": {\n    \"body\": [{\n      \"content_type\":  \"text/xml\",\n      \"operator\": {\n        \"name\": \"not_empty\"\n      },\n      \"key_path\": \"//book/title\"\n    }] \n  }\n}\n```\n\n#### Multipart \n\n```go\nNewStub().WithRequestBody(MultipartForm(\"field_name\"), NotEmpty())\n```\n\n```ts\nnew Stub('GET', Rule.endWith('/helloworld'))\n  .withRequestBody(\n    MultiPartFormRule(\"field_name\", Rule.notEmpty())\n  )\n```\n\n```json\n{\n  \"request\": {\n    \"body\": [{\n      \"content_type\":  \"multipart/form-data\",\n      \"operator\": {\n        \"name\": \"not_empty\"\n      },\n      \"key_path\": \"field_name\"\n    }] \n  }\n}\n```\n\n#### URL Encoded Form (application/x-www-form-urlencoded) \n\n```go\nNewStub().WithRequestBody(URLEncodedBody(\"CustomerID\", EqualTo(\"352461777\")))\n```\n\n```ts\nnew Stub('GET', Rule.endWith('/helloworld'))\n  .withRequestBody(\n    URLEncodedBodyRule(\"CustomerID\", Rule.equalsTo(\"352461777\"))\n  )\n```\n\n```json\n{\n  \"request\": {\n    \"body\": [{\n      \"content_type\":  \"application/x-www-form-urlencoded\",\n      \"operator\": {\n        \"name\": \"equal_to\",\n        \"value\": \"352461777\"\n      },\n      \"key_path\": \"CustomerID\"\n    }] \n  }\n}\n```\n\n### Matching Operators\n\nSee [operator](https://github.com/hungdv136/rio/blob/main/operator.go) for supported operators which can be used for any matching types including method, url, headers. cookies and bodies\n\n| DSL | Golang | TypeScript | Description |\n| --- | ------ | ---------- | ----------- |\n| contains | rio.Contains | Rule.contains | Checks whether actual value contains given value in parameter |\n| not_contains | rio.NotContains | Rule.notContains | Checks whether actual value contains given value in parameter |\n| regex | rio.Regex | Rule.regex | Checks whether actual value matches with given regex in parameter |\n| equal_to | rio.EqualTo | Rule.equalsTo | Determines if two objects are considered equal. Works as require.Equal |\n| start_with | rio.StartWith | Rule.startWith | Tests whether the string begins with prefix. Support string only |\n| end_with | rio.EndWith | Rule.endWith | Tests whether the string begins with prefix. Support string only |\n| length | rio.Length | Rule.withLength | Checks length of object. Support string or array |\n| empty | rio.Empty | Rule.empty | Check whether the specified object is considered empty. Works as require.Empty |\n| not_empty | rio.NotEmpty | Rule.notEmpty | Check whether the specified object is considered not empty. Works as require.NotEmpty |\n\n## Response Definition\n\nResponse can be defined using fluent functions WithXXX (Header, StatusCode, Cookie, Body) as the following example\n\n```go\nrio.NewResponse().WithStatusCode(400).WithHeader(\"KEY\", \"VALUE\")\n```\n\n```ts\nnew StubResponse().withStatusCode(400).withHeader(\"KEY\", \"VALUE\")\n```\n\nThe below are convenient functions to create response with common response content types\n\n```go\n// JSON \nrio.JSONReponse(body)\n\n// XML\nrio.XMLReponse(body)\n\n// HTML\nrio.HTMLReponse(body)\n```\n\n```ts\n// JSON \nJSONReponse({fieldName: 'value'})\n\n// XML\nXMLReponse(`\u003cxml\u003e\u003c/xml\u003e`)\n\n// HTML\nHTMLReponse(`\u003chtml\u003e\u003c/html\u003e`)\n```\n\n### Status Code, Cookies, Header\n\n```go\nresStub := NewResponse()\n  .WithHeader(\"X-REQUEST-HEADER\", \"HEADER_VALUE\")\n  .WithStatusCode(400)\n  .WithCookie(\"KEY\", \"VALUE\")\n\nNewStub().WithReturn(resStub)\n```\n\n```ts\nresStub := new StubResponse()\n  .withHeader(\"X-REQUEST-HEADER\", \"HEADER_VALUE\")\n  .withStatusCode(400)\n  .withCookie(\"KEY\", \"VALUE\")\n\nnew Stub('GET', Rule.contains('/path')).withReturn(resStub)\n```\n\n```json\n{\n  \"response\": {\n    \"body\": {\n      \"key\": \"value\"\n    },\n    \"cookies\": [{\n      \"name\": \"SESSION_ID\",\n      \"value\": \"4e1c0c4d-b7d4-449e-882e-f1be825f1d27\",\n      \"expired_at\": \"2023-01-07T12:26:01.59694+07:00\"\n    }],\n    \"header\": {\n      \"Content-Type\": \"application/json\"\n    },\n    \"status_code\": 200\n  }\n}\n```\n\n### Response body\n\n#### JSON\n\nUse JSONResponse to construct response with JSON (parameter can be map or struct)\n\n```go\nerr := NewStub().For(\"POST\", Contains(\"animal/create\")).\n    WillReturn(JSONResponse(types.Map{\"id\": animalID})).\n    Send(ctx, server)\n```\n\n```ts\nawait new Stub(\"POST\", Rule.contains(\"animal/create\"))\n  .willReturn(JSONResponse({\"id\": animalID})).\n  .send(server)\n```\n\n```json\n{\n  \"response\": {\n    \"status_code\": 200,\n    \"header\": {\n      \"Content-Type\": \"application/json\"\n    },\n    \"body\": {\n      \"key\": \"value\"\n    }\n  }\n}\n```\n\n#### XML \n\nUse XMLResponse to construct response with XML\n\n```go\nerr := NewStub().For(\"POST\", Contains(\"animal/create\")).\n    WillReturn(XMLResponse(structVar)).\n    Send(ctx, server)\n```\n\n```ts\nawait new Stub(\"POST\", Rule.contains(\"animal/create\"))\n  .willReturn(XMLResponse(`\u003cxml\u003e\u003canimal name=\"bird\"/\u003e\u003c/xml\u003e`)).\n  .send(server)\n```\n\n```json\n{\n  \"status_code\": 200,\n  \"body\": \"PGh0bWw+PGh0bWw+\",\n  \"header\": {\n    \"Content-Type\": \"text/xml\"\n  }\n}\n```\n\nWith XML data type, content must be encoded to base64 before submit stub as JSON directly to API. If you want to use raw string, submit with YAML format instead. See [YAML](testdata/stubs.yaml) for example\n\n#### HTML\n\n```go\nerr := NewStub().For(\"POST\", Contains(\"animal/create\")).\n    WillReturn(HTMLResponse(\"\u003chtml\u003e\u003c/html\u003e\")).\n    Send(ctx, server)\n```\n\n```ts\nawait new Stub(\"POST\", Rule.contains(\"animal/create\"))\n  .willReturn(HTMLResponse(`\u003chtml\u003e content \u003chtml\u003e`)).\n  .send(server)\n```\n\n```json\n{\n  \"status_code\": 200,\n  \"body\": \"PGh0bWw+PGh0bWw+\",\n  \"header\": {\n    \"Content-Type\": \"text/html\"\n  }\n}\n```\n\nWith HTML data type, content must be encoded to base64 before submit stub as JSON to mokc API. Go and TS SDK handles this out of the box. If you want to use raw string, submit with YAML format instead. See [YAML](testdata/stubs.yaml) for example\n\n#### Stream/Binary\n\nWe should upload file to server, then assign file id and appropriate content type to response. This also works for any other response types such as JSON, HTML, XML, ...\n\n```go\nserver.UploadFile(ctx, fileID, fileBody)\nNewStub().WithReturn(NewResponse().WithFileBody(fileID))\n```\n\n```ts\nconst server = Server('http://\u003cmock-server\u003e');\nconst fileID = await server.uploadFile('/\u003cpath/to/file\u003e');\n\nnew Stub().withReturn(new StubResponse().withFileBody(fileID))\n```\n\n```json\n{\n  \"response\": {\n    \"status_code\": 200,\n    \"body_file\": \"\u003cfile_id\u003e\",\n    \"header\": {\n      \"Content-Type\": \"\u003ccontent-type\u003e\"\n    }\n  }\n}\n```\n\n### Redirection\n\nThis is to redirect request to another url\n\n```go\nresStub := NewResponse().WithRedirect(\"https://redirect_url.com\")\nNewStub().WithReturn(resStub)\n```\n\n```ts\nresStub := NewResponse().withRedirect(\"https://redirect_url.com\");\nnew Stub().withReturn(resStub);\n```\n\n```json\n{\n  \"response\": {\n    \"status_code\": 307,\n    \"header\": {\n      \"Location\": \"https://redirect_url.com\"\n    }\n  }\n}\n```\n\n### Reserve proxy and recording\n\nIf we want to communicate with real service and record the request and response, then we can enable recording as the following\n\n- `target_url` is the root url of the real system\n- `target_path` is optional. If not provided, then the same relative path from incoming request is used\n\n```go\nrio.NewStub().\n\t\tForAny(rio.Contains(\"reverse_recording/animal/create\")).\n\t\tWithTargetURL(targetURL).\n\t\tWithEnableRecord(true)\n```\n\n```ts\nnew Stub('', Rule.contains(\"reverse_recording/animal/create\"))\n  .withTargetURL(targetURL)\n  .withEnableRecord(true)\n```\n\n```json\n{\n  \"proxy\": {\n    \"target_url\": \"https://destination\",\n    \"enable_record\": true\n  }\n}\n```\n\nThe server will create a new inactive stub into database as the recorded result. This is very helpful for the 1st time we want to simulate the response for a service\n\n### Mock a download API\n\n1. Create an appropriate server (local for unit test or remote for integration test)\n\n```go\nserver := NewRemoteServer(\"http://mock-server\")\n```\n\n2. Upload file \n\n```go\nb, err := os.ReadFile(filePath)\nrequire.NoError(t, err)\n\nfileID, err = server.UploadFile()\nrequire.NoError(t, err)\n```\n\nWe can upload file using rest for integration test `POST {rio-domain}/upload`\n\n3. Create a stub \n\n```go\nresStub := NewResponse().WithFileBody(\"image/jpeg\", fileID)\nerr := NewStub().For(\"GET\", Contains(\"animal/image/download\")).WillReturn(resStub).Send(ctx, server)\n```\n\n```json\n{\n  \"response\": {\n    \"body_file\": \"\u003cuploaded_file_id\",\n    \"status_code\": 200\n  }\n}\n```\n\n4. Perform download request\n\n```go\nreq := http.NewRequest(http.MethodGet, server.GetURL(ctx)+\"/animal/image/download\", nil)\nres, err := http.DefaultClient.Do(req)\nrequire.NoError(t, err)\nrequire.Equal(t, http.StatusOK, res.StatusCode)\ndefer res.Body.Close()\n// Read response body and assert\n```\n\n## Create stubs using Postman\n\nSee [Swagger](docs/swagger.yaml) for API specifications\n\n### JSON Format \n\nThe stubs (matching rules and the expected response) can be created through Rest API `stubs/create_many`, the below is example of body payload\n\n```json\n{\n  \"stubs\": [\n    {\n      \"active\": true,\n      \"id\": 1,\n      \"namespace\": \"\",\n      \"request\": {\n        \"body\": [\n          {\n            \"content_type\":\"application/json\",\n            \"key_path\": \"$.book.type\",\n             \"operator\": {\n              \"name\": \"equal_to\",\n              \"value\": \"How to write test in Golang\"\n            }\n          }\n        ],\n        \"cookie\": [\n          {\n            \"field_name\": \"SESSION_ID\",\n            \"operator\": {\n              \"name\": \"equal_to\",\n              \"value\": \"27a6c092-3bdc-4f46-b1fb-1c7c5eea39e0\"\n            }\n          }\n        ],\n        \"header\": [\n          {\n            \"field_name\": \"X-REQUEST-ID\",\n            \"operator\": {\n              \"name\": \"equal_to\",\n              \"value\": \"f5dcaabc-caac-4c5e-9e06-6b1e935b756d\"\n            }\n          }\n        ],\n        \"method\": \"GET\",\n        \"query\": [\n          {\n            \"field_name\": \"search_term\",\n            \"operator\": {\n              \"name\": \"equal_to\",\n              \"value\": \"4e1c0c4d-b7d4-449e-882e-f1be825f1d27\"\n            }\n          }\n        ],\n        \"url\": [\n          {\n            \"name\": \"contains\",\n            \"value\": \"animal/create\"\n          }\n        ]\n      },\n      \"response\": {\n        \"body\": {\n          \"key\": \"value\"\n        },\n        \"body_file\": \"\",\n        \"cookies\": [{\n          \"name\": \"SESSION_ID\",\n          \"value\": \"4e1c0c4d-b7d4-449e-882e-f1be825f1d27\",\n          \"expired_at\": \"2023-01-07T12:26:01.59694+07:00\"\n        }],\n        \"header\": {\n          \"Content-Type\": \"application/json\"\n        },\n        \"status_code\": 200\n      },\n      \"settings\": {\n        \"deactivate_when_matched\": false,\n        \"delay_duration\": 0\n      },\n      \"weight\": 0\n    }\n  ]\n}\n```\n\n### YAML \n\nIf the response body is not JSON such as XML, or HTML. It is hard to use submit stub with JSON format since JSON does not support multiple lines. In that case, we should use YAML as the following example. Remember to add `Content-Type=application/x-yaml` (This is header of submit request, it is not header of the expected response)\n\n```yaml\nstubs:\n  - active: true\n    namespace: \"\"\n    request:\n      body:\n        - content_type: application/json\n          key_path: $.book.type\n          operator:\n            name: equal_to\n            value: How to write test in Golang\n      cookie:\n        - field_name: SESSION_ID\n          operator:\n            name: equal_to\n            value: 27a6c092-3bdc-4f46-b1fb-1c7c5eea39e0\n      header:\n        - field_name: X-REQUEST-ID\n          operator:\n            name: equal_to\n            value: f5dcaabc-caac-4c5e-9e06-6b1e935b756d\n      method: GET\n      query:\n        - field_name: search_term\n          operator:\n            name: equal_to\n            value: 4e1c0c4d-b7d4-449e-882e-f1be825f1d27\n      url:\n        - name: contains\n          value: animal/create\n    response:\n      template:\n        status_code: 200\n        header: \n          Content-Type: text/html\n        body: \u003e\n            \u003chtml\u003e\n              This is HTML body type\n            \u003c/html\u003e\n    settings:\n      deactivate_when_matched: false\n      delay_duration: 0s\n    weight: 0\n```\n\n## Advance Features\n\nAll these features are supported in Go and TypeScript SDK with the same function names\n\n### Support priority response\n\nSometimes, we want the server to return a fallback response if there is no stub are fully matched with the expectation. In this case, we should submit two different stubs to the mock server. Rio will get the stub with highest weight first, if the weight is not specified, the latest stub will be used\n\n```go\nhighPriority := rio.NewStub().\n    For(\"GET\", rio.Contains(\"animal/create\")).\n    WithHeader(\"X-REQUEST-ID\", rio.Contains(uuid.NewString())).\n    WithQuery(\"search_term\", rio.EqualTo(uuid.NewString())).\n    WithCookie(\"SESSION_ID\", rio.EqualTo(uuid.NewString())).\n    WithWeight(10).\n    WillReturn(rio.NewResponse().WithBody(rio.MapToJSON(resData))).\n    Send(server)\n\nlowPriority := NewStub().\n    For(\"GET\", Contains(\"animal/image/download\")).\n    WithWeight(1).\n    WillReturn(resStub).\n    Send(ctx, server)\n```\n\n```json\n{\n   \"weight\": 10\n}\n```\n\n### Delay response\n\nIt is sometimes we want to simulate slow response API. Rio supports this feature by set delay duration\n\n```go\nNewStub().For(\"GET\", Contains(\"animal/create\")).ShouldDelay(3 * time.Second)\n```\n\n```ts\nnew Stub(\"GET\", Rule.contains(\"animal/create\")).shouldDelay(3000)\n```\n\n```json\n{\n  \"settings\": {\n    \"delay_duration\": \"3000000000\"\n  }\n}\n```\n\n### Deactivate stub when matched\n\nThis is to disable the matched stub, it is not used for the next request. In the following example, the first request will return the first stub with higher weight, then that stub is not available for the next request anymore\n\n```go\nNewStub().For(\"GET\", Contains(\"animal/create\")).ShouldDeactivateWhenMatched().WithWeight(2)\nNewStub().For(\"GET\", Contains(\"animal/create\")).ShouldDeactivateWhenMatched().WithWeight(1)\n```\n\n```json\n{\n  \"settings\": {\n    \"deactivate_when_matched\": true\n  }\n}\n```\n\n### Namespace\n\nThe namespace can be used to separate data between test case. This is helpful when a single mock server is used for many features and projects. Use this pattern as the root url `http://rio.mock.com/\u003cnamespace\u003e/echo`. For example, we want to separate test stubs for payment_service and lead service, then set the root url for those service as below\n\n- Payment Service Root URL: `http://rio.mock.com/payment_service/echo`\n  \n- Lead Service Root URL: `http://rio.mock.com/lead_service/echo`\n   \nIf this url is used `http://rio.mock.com.com/echo`, then default namespace (empty) will be used\n\n### Dynamic response\n\nThe dynamic response uses the Go template to generate the response body programatically. The template is a string in YAML format as the following example. Since the JSON does not support multiple lines input, we should submit stubs in YAML format by providing the request body as the following example. Also, we should set the `Content-Type` header to `application/x-yaml`\n\n**Notes:** While this is a powerful feature, we don't recommend to use this feature in the unit test and automation integration test. Because, it is more flexible and easier to debug when building the response using native language that we use to write the test. This template should use for manual test only\n\nFor supported function in Go template, see http://masterminds.github.io/sprig/\n\n**Avaliable Variables**\n\n- [Request](https://pkg.go.dev/net/http#Request), can be access as `{{ .Request.\u003cGo-Field-Name\u003e }}`\n- `JSONBody` is parsed body in JSON format, can be used in go template as `{{ .JSONBody.\u003cjson_field_parent\u003e.\u003cjson_field_child\u003e }}`\n\n```yaml\nstubs:\n  - active: true\n    namespace: \"\"\n    request:\n      body:\n        - content_type: application/json\n          key_path: $.book.type\n          operator:\n            name: equal_to\n            value: How to write test in Golang\n      cookie:\n        - field_name: SESSION_ID\n          operator:\n            name: equal_to\n            value: 27a6c092-3bdc-4f46-b1fb-1c7c5eea39e0\n      header:\n        - field_name: X-REQUEST-ID\n          operator:\n            name: equal_to\n            value: f5dcaabc-caac-4c5e-9e06-6b1e935b756d\n      method: GET\n      query:\n        - field_name: search_term\n          operator:\n            name: equal_to\n            value: 4e1c0c4d-b7d4-449e-882e-f1be825f1d27\n      url:\n        - name: contains\n          value: animal/create\n    response:\n      template:\n        script_schema_type: yaml\n        script: \u003e\n            status_code: 200\n            \n            cookies: \n                {{ range $cookie := .Request.Cookies }}\n                - name: {{ $cookie.Name }}\n                  value: {{ $cookie.Value }}\n                {{end}}\n\n            headers:\n                X-REQUEST-ID: {{ .Request.Header.Get \"X-REQUEST-ID\"}} \n            \n            body: \u003e\n                {\n                    \"encrypted_value\": \"{{ encryptAES \"e09b3cc3b4943e2558d1882c9ef999eb\" .JSONBody.naked_value}}\"\n                }\n    settings:\n      deactivate_when_matched: false\n      delay_duration: 0s\n    weight: 0\n```\n\nExample for template in TypeScript [this file](https://github.com/hungdv136/rio-js/blob/main/example/sdk-install.test.ts)\n \n## Mocking GRPC\n\nMocking grpc is mostly the same as mocking HTTP, the following are some minor differences. Currently, only Unary is supported. Even this gRPC mocking can be used with unit test, we recommend that we should not use it for unit test since it is not right way to do unit test with gPRC\n\n### Define a proto\n\n- Compress protos of a target service and its own proto dependencies into a single compressed file with the same package structure\n- Call API `POST proto/upload` to upload compressed file to the rio server. After uploaded proto file, the rest are the same as HTTP mocking\n\n### Define stub\n\nDefine stub for grpc the same as for HTTP mock with the following differences\n\n- `method` must be `grpc` as the following example\n- `status_code`: Must follow grpc code. Default = 0 for success response. For [details](https://grpc.github.io/grpc/core/md_doc_statuscodes.html)\n- `header`: will be matched with request metadata (For example: X-REQUEST-ID)\n- `cookie` and `query` are not supported in GRPC\n\n```json\n{\n  \"request\": {\n    \"method\": \"grpc\",\n    \"url\": [{\n      \"name\": \"equal_to\",\n      \"value\": \"/offers.v1.OfferService/ValidateOffer\"\n    }]\n  },\n  \"response\": {\n    \"status_code\": 0,\n    \"body\": {\n      \"key\": \"value\"\n    },\n    \"header\": {\n      \"Header-Name\": \"HEADER-VALUE\"\n    }\n  }\n}\n```\n\nThe response body is in JSON format. You can enable proxy with recording or look at the generated proto structure to know the response structure\n\n### Mocking GRPC error response \n\n```json\n{\n  \"request\": {\n    \"method\": \"grpc\",\n    \"url\": [{\n      \"name\": \"equal_to\",\n      \"value\": \"/offers.v1.OfferService/ValidateOffer\"\n    }]\n  },\n  \"response\": {\n    \"status_code\": 3,\n    \"error\": {\n      \"message\": \"This is error message\",\n      \"details\": [{\n        \"type\": \"common.v1.CommonError\",\n        \"value\": {\n          \"verdict\": \"record_not_found\"\n        }\n      }]\n    }\n  }\n}\n```\n\n`status_code`: Must be greater than 0\n`details`: Optional. This is to define detail of error. `type`: must be defined and its proto definitions must be included in the same compressed proto. `value` is a custom key value\n\n### Change the root url to rio\n\nNote that the root does not contains `/echo/` as HTTP mock, also namespace is not supported yet \n\n## How to deploy\n\nThis is to deploy remote mock server. These steps are not required for unit test\n\n### Setup database\n  \nSupported databases: MySQL or MariaDB\n\n```env\nDB_SERVER=0.0.0.0:3306\nDB_USER=\u003cuser\u003e\nDB_PASSWORD=\u003cpassword\u003e\n```\n\n### Deploy file storage \n\nIf LocalStorageType is used then `Rio` can only be deployed with single instance. The GRPC and HTTP services must access to the same directory that is defined in ENV `FILE_DIR`. If we want to deploy Rio as a cluster with multiple instances, then GCS or S3 must be used as file storage\n\n#### Use S3\n\n```env\nFILE_STORAGE_TYPE=s3\nS3_ACCESS_KEY_ID=\nS3_SECRET_ACCESS_KEY=\nS3_REGION=ap-southeast-1\n```\n\n#### Use GCS\n\n```env\nFILE_STORAGE_TYPE=gcs\nGCS_CREDENTIALS_FILE=\u003ccredential-file-path\u003e\n```\n\n### Deploy HTTP mock server\n\nThis is required even we want to use GRPC mock only because HTTP server is not only for serving mock requests, but also contains a set of API for submitting stubs\n\n### Deploy GRPC mock server\n\nThis is optional. The GRPC is to serve mock GRPC requests. If you just want to use HTTP mock, then can skip this step\n\n### Configure cache \n\nThe below are default configuration for cache. If we want to change cache TTL or change cache strategy, then adjust the following env. Otherwise, can ignore these configurations\n\n```env\nSTUB_CACHE_TTL=1h\nSTUB_CACHE_STRATEGY=default\n```\n\nThe default strategy cache stubs and protos in local memory and invalidate if there is any update/insert/delete in database. If we want to do performance testing, then can change `STUB_CACHE_STRATEGY` to `aside`\n\n- [Docker Compose](docker/compose.yaml): HTTP mock server\n\n- [Docker Compose GRPC](docker/compose.grpc.yaml.yaml): HTTP and gRPC mock servers\n\n## Contribution\n\n### Run test\n\nThere are few integration tests in these packages `internal/database`, `internal/api` and `internal/grpc` those are integrated with real database. Follow the following step to setup environment and run tests\n\n1. Install docker\n\n2. Run the below command to setup database for testing\n\n```bash\nmake dev-up\n```\n\n3. Run all tests\n\n```bash\nmake test\n```\n\n4. To cleanup testing environment\n\n```bash\nmake dev-down\n```\n\n### Commit Changes\n\nRun the below command to format codes, check lint and run tests before commit codes\n\n```bash\nmake all\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhungdv136%2Frio","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhungdv136%2Frio","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhungdv136%2Frio/lists"}