rio

package module
v0.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 3, 2023 License: Apache-2.0 Imports: 33 Imported by: 0

README

A flexible declarative HTTP mocking framework in Golang

ci Go Report Card Go Reference

Introduction

Rio 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)

Features

  • Fast, simple and fluent API for unit test in Golang
  • DSL in YAML/JSON format for stub declarations
  • Supports wide-range response types (html, xml, json and binary)
  • Can be deployed as mock server (HTTP and gRPC) for integration test
  • Supports persistent stubs to database with caching to improve performance
  • Flexible for matching request by method, URL params, headers, cookies and bodies
  • Dynamic response with go-template
  • Automatically generates stubs with reserve proxy mode
  • Ability to run tests in parallel to improve speed
  • Support SDK in Golang and TypeScript/Javascript

How it works

Workflow

How to use in unit test for Golang

Suppose that we want to test a function that calls API and parse the response data as the following example

func CallAPI(ctx context.Context, rootURL string, input map[string]interface{}) (map[string]interface{}, error) {
	bodyBytes, err := json.Marshal(input)
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, rootURL+"/animal", bytes.NewReader(bodyBytes))
	if err != nil {
		return nil, err
	}

	req.Header.Set("Content-Type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}

	data := map[string]interface{}{}
	decoder := json.NewDecoder(res.Body)
	if err := decoder.Decode(&data); err != nil {
		return nil, err
	}

	return data, nil
}
Prerequisites

Golang 1.18+

Install
go get github.com/kien-manabie/rio-mocker@latest

No deployment is required for unit test

Usage

Write unit test with Golang

func TestCallAPI(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	server := rio.NewLocalServerWithReporter(t)

	t.Run("success", func(t *testing.T) {
		t.Parallel()

		animalName := uuid.NewString()
		returnedBody := map[string]interface{}{"id": uuid.NewString()}

		require.NoError(t, rio.NewStub().
			// Verify method and path
			For("POST", rio.EndWith("/animal")).
			// Verify if the request body is composed correctly
			WithRequestBody(rio.BodyJSONPath("$.name", rio.EqualTo(animalName))).
			// Response with 200 (default) and JSON
			// Body can be map, struct or JSON string
			WillReturn(rio.JSONResponse(returnedBody)).
			// Submit stub to mock server
			Send(ctx, server))

		input := map[string]interface{}{"name": animalName}
		resData, err := CallAPI(ctx, server.GetURL(ctx), input)
		require.NoError(t, err)
		require.Equal(t, returnedBody, resData)
	})

	t.Run("bad_request", func(t *testing.T) {
		t.Parallel()

		animalName := uuid.NewString()

		require.NoError(t, rio.NewStub().
			// Verify method and path
			For("POST", rio.EndWith("/animal")).
			// Verify if the request body is composed correctly
			WithRequestBody(rio.BodyJSONPath("$.name", rio.EqualTo(animalName))).
			// Response with status 400 and empty body JSON
			WillReturn(rio.NewResponse().WithStatusCode(400)).
			// Submit stub to mock server
			Send(ctx, server))

		input := map[string]interface{}{"name": animalName}
		resData, err := CallAPI(ctx, server.GetURL(ctx), input)
		require.Error(t, err)
		require.Empty(t, resData)
	})
}

Examples

How to use in integration test

Suppose 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

Golang, TypeScript, Postman can be used to define and submit stubs to mock server. This repository illustrates how to use Rio to write integration tests in Javascript/TypeScript

Deploy Rio as a stand-alone service

See deploy. After deployed, Rio can be accessed by other services via a domain, for example http://rio-domain

Change the root url configuration of the external to mock server

Go 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)

Perform manual test case
  1. Use Postman to submit stubs
  2. Use Postman to perform manual test with your API
Write an automation test cases
  1. Create a new server

This 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

server := rio.NewRemoteServer("http://rio-server")
import { Server } from 'rio-ts-sdk'
server := Server('http://rio-server')
  1. Define a stub
resData :=types.Map{"data": uuid.NewString(),"verdict": "success"}
stub := rio.NewStub().
		For("GET", rio.Contains("animal/create")).
		WithHeader("X-REQUEST-ID", rio.Contains("<x-request-id>")).
		WithQuery("search_term", rio.EqualTo("<search-value>")).
		WithCookie("SESSION_ID", rio.EqualTo("<cookie-value>")).
		WillReturn(rio.JSONResponse(resData)).
    Send(ctx, server)
import { Stub, Rule, JSONResponse } from 'rio-ts-sdk'

resData :={data: uuidv4(), verdict: "success"};
stub := new Stub("GET", Rule.contains("animal/create"))
  .withHeader("X-REQUEST-ID", Rule.contains('<x-request-id>'))
  .withQuery("search_term", Rule.equalsTo('<search-value>'))
  .withCookie("SESSION_ID", Rule.equalsTo('<cookie-value>'))
  .willReturn(JSONResponse(resData))
  .send(ctx, server);

In 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

  • Validate method GET
  • Validate whether request's path contains animal/create
  • Validate query string search_term whether its value contains a predefined value
  • Validate X-Request-ID whether its value equals to a predefined value
  • Validate cookie SESSION_ID whether its value equals to a predefined value
  • If these conditions are matched, then return with predefined response

Request Matching

This is to verify incoming requests against predefined stubs. If all rules are matched, then the predefined response of matched stub will be responded

Match by method and url
NewStub().For("GET", Contains("/helloworld"))
new Stub("GET", Rule.contains("/helloworld"));
{
  "request": {
    "method": "GET",
    "url": [
      {
        "name": "contains",
        "value": "/helloworld"
      }
    ]
  }
}
Match by query parameter
NewStub().WithQuery("search_term", NotEmpty())
new Stub("GET", Rule.contains("/helloworld")).withQuery(
  "search_term",
  Rule.notEmpty()
);
{
  "request": {
    "query": [
      {
        "field_name": "search_term",
        "operator": {
          "name": "not_empty"
        }
      }
    ]
  }
}
Match by cookies
NewStub().WithCookie("SESSION_ID", EqualTo("expected cookie value"))
new Stub("GET", Rule.contains("/helloworld")).withCookie(
  "SESSION_ID",
  Rule.equalsTo("expected cookie value")
);
{
  "request": {
    "cookie": [
      {
        "field_name": "SESSION_ID",
        "operator": {
          "name": "equal_to",
          "value": "expected cookie value"
        }
      }
    ]
  }
}
Match by request body
JSON Path
NewStub().WithRequestBody(BodyJSONPath("$.name"), NotEmpty())
new Stub("GET", Rule.endWith("/helloworld")).withRequestBody(
  JSONPathRule("$.name", Rule.notEmpty()),
  JSONPathRule("$.count", Rule.equalsTo(3000))
);
{
  "request": {
    "body": [
      {
        "content_type": "application/json",
        "operator": {
          "name": "not_empty"
        },
        "key_path": "$.name"
      }
    ]
  }
}
XML Path
NewStub().WithRequestBody(BodyXMLPath("//book/title"), NotEmpty())
{
  "request": {
    "body": [
      {
        "content_type": "text/xml",
        "operator": {
          "name": "not_empty"
        },
        "key_path": "//book/title"
      }
    ]
  }
}
Multipart
NewStub().WithRequestBody(MultipartForm("field_name"), NotEmpty())
new Stub("GET", Rule.endWith("/helloworld")).withRequestBody(
  MultiPartFormRule("field_name", Rule.notEmpty())
);
{
  "request": {
    "body": [
      {
        "content_type": "multipart/form-data",
        "operator": {
          "name": "not_empty"
        },
        "key_path": "field_name"
      }
    ]
  }
}
URL Encoded Form (application/x-www-form-urlencoded)
NewStub().WithRequestBody(URLEncodedBody("CustomerID", EqualTo("352461777")))
new Stub("GET", Rule.endWith("/helloworld")).withRequestBody(
  URLEncodedBodyRule("CustomerID", Rule.equalsTo("352461777"))
);
{
  "request": {
    "body": [
      {
        "content_type": "application/x-www-form-urlencoded",
        "operator": {
          "name": "equal_to",
          "value": "352461777"
        },
        "key_path": "CustomerID"
      }
    ]
  }
}
Matching Operators

See operator for supported operators which can be used for any matching types including method, url, headers. cookies and bodies

DSL Golang TypeScript Description
contains rio.Contains Rule.contains Checks whether actual value contains given value in parameter
not_contains rio.NotContains Rule.notContains Checks whether actual value contains given value in parameter
regex rio.Regex Rule.regex Checks whether actual value matches with given regex in parameter
equal_to rio.EqualTo Rule.equalsTo Determines if two objects are considered equal. Works as require.Equal
start_with rio.StartWith Rule.startWith Tests whether the string begins with prefix. Support string only
end_with rio.EndWith Rule.endWith Tests whether the string begins with prefix. Support string only
length rio.Length Rule.withLength Checks length of object. Support string or array
empty rio.Empty Rule.empty Check whether the specified object is considered empty. Works as require.Empty
not_empty rio.NotEmpty Rule.notEmpty Check whether the specified object is considered not empty. Works as require.NotEmpty

Response Definition

Response can be defined using fluent functions WithXXX (Header, StatusCode, Cookie, Body) as the following example

rio.NewResponse().WithStatusCode(400).WithHeader("KEY", "VALUE")
new StubResponse().withStatusCode(400).withHeader("KEY", "VALUE");

The below are convenient functions to create response with common response content types

// JSON
rio.JSONReponse(body)

// XML
rio.XMLReponse(body)

// HTML
rio.HTMLReponse(body)
// JSON
JSONReponse({ fieldName: "value" });

// XML
XMLReponse(`<xml></xml>`);

// HTML
HTMLReponse(`<html></html>`);
Status Code, Cookies, Header
resStub := NewResponse()
  .WithHeader("X-REQUEST-HEADER", "HEADER_VALUE")
  .WithStatusCode(400)
  .WithCookie("KEY", "VALUE")

NewStub().WithReturn(resStub)
resStub := new StubResponse()
  .withHeader("X-REQUEST-HEADER", "HEADER_VALUE")
  .withStatusCode(400)
  .withCookie("KEY", "VALUE")

new Stub('GET', Rule.contains('/path')).withReturn(resStub)
{
  "response": {
    "body": {
      "key": "value"
    },
    "cookies": [
      {
        "name": "SESSION_ID",
        "value": "4e1c0c4d-b7d4-449e-882e-f1be825f1d27",
        "expired_at": "2023-01-07T12:26:01.59694+07:00"
      }
    ],
    "header": {
      "Content-Type": "application/json"
    },
    "status_code": 200
  }
}
Response body
JSON

Use JSONResponse to construct response with JSON (parameter can be map or struct)

err := NewStub().For("POST", Contains("animal/create")).
    WillReturn(JSONResponse(types.Map{"id": animalID})).
    Send(ctx, server)
await new Stub("POST", Rule.contains("animal/create"))
  .willReturn(JSONResponse({"id": animalID})).
  .send(server)
{
  "response": {
    "status_code": 200,
    "header": {
      "Content-Type": "application/json"
    },
    "body": {
      "key": "value"
    }
  }
}
XML

Use XMLResponse to construct response with XML

err := NewStub().For("POST", Contains("animal/create")).
    WillReturn(XMLResponse(structVar)).
    Send(ctx, server)
await new Stub("POST", Rule.contains("animal/create"))
  .willReturn(XMLResponse(`<xml><animal name="bird"/></xml>`)).
  .send(server)
{
  "status_code": 200,
  "body": "PGh0bWw+PGh0bWw+",
  "header": {
    "Content-Type": "text/xml"
  }
}

With 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 for example

HTML
err := NewStub().For("POST", Contains("animal/create")).
    WillReturn(HTMLResponse("<html></html>")).
    Send(ctx, server)
await new Stub("POST", Rule.contains("animal/create"))
  .willReturn(HTMLResponse(`<html> content <html>`)).
  .send(server)
{
  "status_code": 200,
  "body": "PGh0bWw+PGh0bWw+",
  "header": {
    "Content-Type": "text/html"
  }
}

With 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 for example

Stream/Binary

We 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, ...

server.UploadFile(ctx, fileID, fileBody)
NewStub().WithReturn(NewResponse().WithFileBody(fileID))
const server = Server("http://<mock-server>");
const fileID = await server.uploadFile("/<path/to/file>");

new Stub().withReturn(new StubResponse().withFileBody(fileID));
{
  "response": {
    "status_code": 200,
    "body_file": "<file_id>",
    "header": {
      "Content-Type": "<content-type>"
    }
  }
}
Redirection

This is to redirect request to another url

resStub := NewResponse().WithRedirect("https://redirect_url.com")
NewStub().WithReturn(resStub)
resStub := NewResponse().withRedirect("https://redirect_url.com");
new Stub().withReturn(resStub);
{
  "response": {
    "status_code": 307,
    "header": {
      "Location": "https://redirect_url.com"
    }
  }
}
Reserve proxy and recording

If we want to communicate with real service and record the request and response, then we can enable recording as the following

  • target_url is the root url of the real system
  • target_path is optional. If not provided, then the same relative path from incoming request is used
rio.NewStub().
		ForAny(rio.Contains("reverse_recording/animal/create")).
		WithTargetURL(targetURL).
		WithEnableRecord(true)
new Stub("", Rule.contains("reverse_recording/animal/create"))
  .withTargetURL(targetURL)
  .withEnableRecord(true);
{
  "proxy": {
    "target_url": "https://destination",
    "enable_record": true
  }
}

The 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

Mock a download API
  1. Create an appropriate server (local for unit test or remote for integration test)
server := NewRemoteServer("http://mock-server")
  1. Upload file
b, err := os.ReadFile(filePath)
require.NoError(t, err)

fileID, err = server.UploadFile()
require.NoError(t, err)

We can upload file using rest for integration test POST {rio-domain}/upload

  1. Create a stub
resStub := NewResponse().WithFileBody("image/jpeg", fileID)
err := NewStub().For("GET", Contains("animal/image/download")).WillReturn(resStub).Send(ctx, server)
{
  "response": {
    "body_file": "<uploaded_file_id",
    "status_code": 200
  }
}
  1. Perform download request
req := http.NewRequest(http.MethodGet, server.GetURL(ctx)+"/animal/image/download", nil)
res, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, res.StatusCode)
defer res.Body.Close()
// Read response body and assert

Create stubs using Postman

See Swagger for API specifications

JSON Format

The stubs (matching rules and the expected response) can be created through Rest API stubs/create_many, the below is example of body payload

{
  "stubs": [
    {
      "active": true,
      "id": 1,
      "namespace": "",
      "request": {
        "body": [
          {
            "content_type": "application/json",
            "key_path": "$.book.type",
            "operator": {
              "name": "equal_to",
              "value": "How to write test in Golang"
            }
          }
        ],
        "cookie": [
          {
            "field_name": "SESSION_ID",
            "operator": {
              "name": "equal_to",
              "value": "27a6c092-3bdc-4f46-b1fb-1c7c5eea39e0"
            }
          }
        ],
        "header": [
          {
            "field_name": "X-REQUEST-ID",
            "operator": {
              "name": "equal_to",
              "value": "f5dcaabc-caac-4c5e-9e06-6b1e935b756d"
            }
          }
        ],
        "method": "GET",
        "query": [
          {
            "field_name": "search_term",
            "operator": {
              "name": "equal_to",
              "value": "4e1c0c4d-b7d4-449e-882e-f1be825f1d27"
            }
          }
        ],
        "url": [
          {
            "name": "contains",
            "value": "animal/create"
          }
        ]
      },
      "response": {
        "body": {
          "key": "value"
        },
        "body_file": "",
        "cookies": [
          {
            "name": "SESSION_ID",
            "value": "4e1c0c4d-b7d4-449e-882e-f1be825f1d27",
            "expired_at": "2023-01-07T12:26:01.59694+07:00"
          }
        ],
        "header": {
          "Content-Type": "application/json"
        },
        "status_code": 200
      },
      "settings": {
        "deactivate_when_matched": false,
        "delay_duration": 0
      },
      "weight": 0
    }
  ]
}
YAML

If 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)

stubs:
  - active: true
    namespace: ""
    request:
      body:
        - content_type: application/json
          key_path: $.book.type
          operator:
            name: equal_to
            value: How to write test in Golang
      cookie:
        - field_name: SESSION_ID
          operator:
            name: equal_to
            value: 27a6c092-3bdc-4f46-b1fb-1c7c5eea39e0
      header:
        - field_name: X-REQUEST-ID
          operator:
            name: equal_to
            value: f5dcaabc-caac-4c5e-9e06-6b1e935b756d
      method: GET
      query:
        - field_name: search_term
          operator:
            name: equal_to
            value: 4e1c0c4d-b7d4-449e-882e-f1be825f1d27
      url:
        - name: contains
          value: animal/create
    response:
      template:
        status_code: 200
        header:
          Content-Type: text/html
        body: >
          <html>
            This is HTML body type
          </html>
    settings:
      deactivate_when_matched: false
      delay_duration: 0s
    weight: 0

Advance Features

All these features are supported in Go and TypeScript SDK with the same function names

Support priority response

Sometimes, 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

highPriority := rio.NewStub().
    For("GET", rio.Contains("animal/create")).
    WithHeader("X-REQUEST-ID", rio.Contains(uuid.NewString())).
    WithQuery("search_term", rio.EqualTo(uuid.NewString())).
    WithCookie("SESSION_ID", rio.EqualTo(uuid.NewString())).
    WithWeight(10).
    WillReturn(rio.NewResponse().WithBody(rio.MapToJSON(resData))).
    Send(server)

lowPriority := NewStub().
    For("GET", Contains("animal/image/download")).
    WithWeight(1).
    WillReturn(resStub).
    Send(ctx, server)
{
  "weight": 10
}
Delay response

It is sometimes we want to simulate slow response API. Rio supports this feature by set delay duration

NewStub().For("GET", Contains("animal/create")).ShouldDelay(3 * time.Second)
new Stub("GET", Rule.contains("animal/create")).shouldDelay(3000);
{
  "settings": {
    "delay_duration": "3000000000"
  }
}
Deactivate stub when matched

This 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

NewStub().For("GET", Contains("animal/create")).ShouldDeactivateWhenMatched().WithWeight(2)
NewStub().For("GET", Contains("animal/create")).ShouldDeactivateWhenMatched().WithWeight(1)
{
  "settings": {
    "deactivate_when_matched": true
  }
}
Namespace

The 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/<namespace>/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

  • Payment Service Root URL: http://rio.mock.com/payment_service/echo
  • Lead Service Root URL: http://rio.mock.com/lead_service/echo

If this url is used http://rio.mock.com.com/echo, then default namespace (empty) will be used

Dynamic response

The 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

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

For supported function in Go template, see http://masterminds.github.io/sprig/

Avaliable Variables

  • Request, can be access as {{ .Request.<Go-Field-Name> }}
  • JSONBody is parsed body in JSON format, can be used in go template as {{ .JSONBody.<json_field_parent>.<json_field_child> }}
stubs:
  - active: true
    namespace: ""
    request:
      body:
        - content_type: application/json
          key_path: $.book.type
          operator:
            name: equal_to
            value: How to write test in Golang
      cookie:
        - field_name: SESSION_ID
          operator:
            name: equal_to
            value: 27a6c092-3bdc-4f46-b1fb-1c7c5eea39e0
      header:
        - field_name: X-REQUEST-ID
          operator:
            name: equal_to
            value: f5dcaabc-caac-4c5e-9e06-6b1e935b756d
      method: GET
      query:
        - field_name: search_term
          operator:
            name: equal_to
            value: 4e1c0c4d-b7d4-449e-882e-f1be825f1d27
      url:
        - name: contains
          value: animal/create
    response:
      template:
        script_schema_type: yaml
        script: >
          status_code: 200

          cookies: 
              {{ range $cookie := .Request.Cookies }}
              - name: {{ $cookie.Name }}
                value: {{ $cookie.Value }}
              {{end}}

          headers:
              X-REQUEST-ID: {{ .Request.Header.Get "X-REQUEST-ID"}} 

          body: >
              {
                  "encrypted_value": "{{ encryptAES "e09b3cc3b4943e2558d1882c9ef999eb" .JSONBody.naked_value}}"
              }
    settings:
      deactivate_when_matched: false
      delay_duration: 0s
    weight: 0

Example for template in TypeScript this file

Mocking GRPC

Mocking 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

Define a proto
  • Compress protos of a target service and its own proto dependencies into a single compressed file with the same package structure
  • 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
Define stub

Define stub for grpc the same as for HTTP mock with the following differences

  • method must be grpc as the following example
  • status_code: Must follow grpc code. Default = 0 for success response. For details
  • header: will be matched with request metadata (For example: X-REQUEST-ID)
  • cookie and query are not supported in GRPC
{
  "request": {
    "method": "grpc",
    "url": [
      {
        "name": "equal_to",
        "value": "/offers.v1.OfferService/ValidateOffer"
      }
    ]
  },
  "response": {
    "status_code": 0,
    "body": {
      "key": "value"
    },
    "header": {
      "Header-Name": "HEADER-VALUE"
    }
  }
}

The response body is in JSON format. You can enable proxy with recording or look at the generated proto structure to know the response structure

Mocking GRPC error response
{
  "request": {
    "method": "grpc",
    "url": [
      {
        "name": "equal_to",
        "value": "/offers.v1.OfferService/ValidateOffer"
      }
    ]
  },
  "response": {
    "status_code": 3,
    "error": {
      "message": "This is error message",
      "details": [
        {
          "type": "common.v1.CommonError",
          "value": {
            "verdict": "record_not_found"
          }
        }
      ]
    }
  }
}

status_code: Must be greater than 0 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

Change the root url to rio

Note that the root does not contains /echo/ as HTTP mock, also namespace is not supported yet

How to deploy

This is to deploy remote mock server. These steps are not required for unit test

Setup database

Supported databases: MySQL or MariaDB

DB_SERVER=0.0.0.0:3306
DB_USER=<user>
DB_PASSWORD=<password>
Deploy file storage

If 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

Use S3
FILE_STORAGE_TYPE=s3
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
S3_REGION=ap-southeast-1
Use GCS
FILE_STORAGE_TYPE=gcs
GCS_CREDENTIALS_FILE=<credential-file-path>
Deploy HTTP mock server

This 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

Deploy GRPC mock server

This is optional. The GRPC is to serve mock GRPC requests. If you just want to use HTTP mock, then can skip this step

Configure cache

The 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

STUB_CACHE_TTL=1h
STUB_CACHE_STRATEGY=default

The 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

Contribution

Run test

There 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

  1. Install docker

  2. Run the below command to setup database for testing

make dev-up
  1. Run all tests
make test
  1. To cleanup testing environment
make dev-down
Commit Changes

Run the below command to format codes, check lint and run tests before commit codes

make all

Documentation

Index

Constants

View Source
const (
	Debug   = "debug"
	Release = "release"
)

Defines commone mode

View Source
const (
	ContentTypeJSON      = "application/json"
	ContentTypeXML       = "text/xml"
	ContentTypeHTML      = "text/html"
	ContentTypeText      = "text/plain"
	ContentTypeMultipart = "multipart/form-data"
	ContentTypeForm      = "application/x-www-form-urlencoded"
)

Defines request content types

View Source
const (
	HeaderContentType   = "Content-Type"
	HeaderContentLength = "Content-Length"
	HeaderLocation      = "Location"
	HeaderXRequestID    = "X-Request-Id"
)

Defines request header

View Source
const (
	MethodGrpc   = "grpc"
	ProtocolHTTP = "http"
	ProtocolGrpc = "grpc"
)
View Source
const ResetAll = "reset_all"
View Source
const TagRecordedStub = "recorded_stub"

Used for recorded stub when recording is enabled

Variables

View Source
var ReleaseMode = Release

ReleaseMode defines application mode This should be set on start up to avoid race condition

Functions

func HTMLContent

func HTMLContent(html string) (string, []byte)

HTMLContent response html

func Match

func Match(ctx context.Context, op Operator, value interface{}) (bool, error)

Match compares input value with predefined operator

func MustToJSON

func MustToJSON(v interface{}) (string, []byte)

MustToJSON converts a struct to JSON. Panic if error

func MustToXML

func MustToXML(v interface{}) (string, []byte)

MustToXML converts a struct to XML. Panic if error

Types

type ArrayStubs

type ArrayStubs struct {
	Stubs []*Stub `json:"stubs" yaml:"stubs"`
}

type Body

type Body []byte

Body is a custom encoded body value to support submit body with base64 encoded or raw string

func (*Body) UnmarshalJSON

func (m *Body) UnmarshalJSON(data []byte) error

Support submit body with base64 encoded or raw string

func (*Body) UnmarshalYAML

func (m *Body) UnmarshalYAML(unmarshal func(interface{}) error) error

It is more convenient to submit data with raw string in YAML

type BodyOperator

type BodyOperator struct {
	// The content type of the request body which is one of the following values
	//  - "application/json"
	//  - "text/xml"
	//  - "text/html"
	//  - "text/plain"
	//  - "multipart/form-data"
	//  - "application/x-www-form-urlencoded"
	ContentType string `json:"content_type" yaml:"content_type"`

	Operator Operator `json:"operator" yaml:"operator"`

	// KeyPath is json or xml path
	// Refer to this document for json path syntax https://goessner.net/articles/JsonPath/
	KeyPath string `json:"key_path" yaml:"key_path"`
}

BodyOperator define operator for matching body

type Cookie struct {
	Name      string    `json:"name" yaml:"name"`
	Value     string    `json:"value" yaml:"value"`
	ExpiredAt time.Time `json:"expired_at" yaml:"expired_at"`
}

Cookie defines cookie

type CreateBodyOperator

type CreateBodyOperator func() BodyOperator

CreateBodyOperator is alias function for creating a body operator

func BodyJSONPath

func BodyJSONPath(jsonPath string, createOperator CreateOperator) CreateBodyOperator

BodyJSONPath matches request body by the json path Refer to this document for json path syntax https://goessner.net/articles/JsonPath/

func MultiPartForm

func MultiPartForm(key string, createOperator CreateOperator) CreateBodyOperator

MultiPartForm to verify form value in multiple parts request

func URLEncodedBody

func URLEncodedBody(key string, createOperator CreateOperator) CreateBodyOperator

URLEncodedBody to verify form value in url encoded request

type CreateOperator

type CreateOperator func() Operator

CreateOperator is alias for creating an operator

func Contains

func Contains(v interface{}) CreateOperator

Contains checks actual value should contain given value in parameter

func Empty

func Empty() CreateOperator

Empty checks object is empty

func EndWith

func EndWith(v string) CreateOperator

EndWith sets the start with matching logic This operator is applied for string only

func EqualTo

func EqualTo(v interface{}) CreateOperator

EqualTo checks actual value should equal to given value in parameter Engine will convert actual value to same type with predefined parameter

func Length

func Length(v int) CreateOperator

Length check length operator Supported data types: map, string and array of map, int, string, float, ...

func NotContains

func NotContains(v interface{}) CreateOperator

NotContains checks actual value should not contain given value in parameter

func NotEmpty

func NotEmpty() CreateOperator

NotEmpty checks object is not empty

func Regex

func Regex(v string) CreateOperator

Regex checks actual value should match with given regex in parameter

func StartWith

func StartWith(v string) CreateOperator

StartWith sets the start with matching logic This operator is applied for string only

type ErrorDetail

type ErrorDetail struct {
	// Type defines message type of error detail. For example: common.v1.CommonError
	// This is to get message descriptor to encode/decode message
	// The proto of defined type must be included in proto compressed file
	Type string `json:"type,omitempty" yaml:"type"`

	// Value holds the payload of the error
	Value types.Map `json:"value,omitempty" yaml:"value"`
}

ErrorDetail is to define details for error

type FieldOperator

type FieldOperator struct {
	// FieldName is header name, cookie name or parameter name
	FieldName string   `json:"field_name" yaml:"field_name"`
	Operator  Operator `json:"operator" yaml:"operator"`
}

FieldOperator defines operator with field name

func (FieldOperator) String

func (o FieldOperator) String() string

String returns string

type GrpcRequest

type GrpcRequest struct {
	FullMethod string    `json:"full_method" yaml:"full_method"`
	InputData  types.Map `json:"input_data" yaml:"input_data"`
}

GrpcRequest defines grpc request

type Handler

type Handler struct {
	// contains filtered or unexported fields
}

Handler handles mocking for http request

func NewHandler

func NewHandler(stubStore StubStore, fileStorage fs.FileStorage) *Handler

NewHandler handles request

func (*Handler) Handle

func (h *Handler) Handle(w http.ResponseWriter, r *http.Request)

Handle handles http request

func (*Handler) WithBodyStoreThreshold

func (h *Handler) WithBodyStoreThreshold(v int) *Handler

func (*Handler) WithNamespace

func (h *Handler) WithNamespace(namespace string) *Handler

WithNamespace sets namespace

type IncomingQueryOption

type IncomingQueryOption struct {
	Namespace string  `json:"namespace" yaml:"namespace"`
	Ids       []int64 `json:"ids" yaml:"ids"`
	Limit     int     `json:"limit" yaml:"limit"`
}

IncomingQueryOption incoming query option

type IncomingRequest

type IncomingRequest struct {
	ID        int64     `json:"id" yaml:"id"`
	Namespace string    `json:"namespace" yaml:"namespace"`
	Tag       string    `json:"tag" yaml:"tag"`
	URL       string    `json:"url" yaml:"url"`
	Method    string    `json:"method" yaml:"method"`
	Header    types.Map `json:"header" yaml:"header"`
	Body      []byte    `json:"body" yaml:"body"`
	CURL      string    `json:"curl" gorm:"column:curl" yaml:"curl"`
	StubID    int64     `json:"stub_id" yaml:"stub_id"`
}

IncomingRequest capture the incoming request

func Capture

func Capture(r *http.Request, bodyThreshold int) *IncomingRequest

Capture capture the request from http request Ignore body if the given request is multiparts or its body exceeds the threshold

func (*IncomingRequest) Replay

func (i *IncomingRequest) Replay(ctx context.Context, server Server) (*http.Response, error)

Replay replays the http request to a server This is to debug with a stub from a remote server using IDE

func (*IncomingRequest) ReserveRequest

func (i *IncomingRequest) ReserveRequest(ctx context.Context) (*http.Request, error)

ReserveRequest converts the saved data to request

func (*IncomingRequest) WithNamespace

func (i *IncomingRequest) WithNamespace(v string) *IncomingRequest

WithNamespace sets namespace

type IncomingRequests

type IncomingRequests struct {
	Requests []*IncomingRequest `json:"requests" yaml:"requests"`
}

type LastUpdatedRecord

type LastUpdatedRecord struct {
	ID        int64     `json:"id"`
	UpdatedAt time.Time `json:"updated_at"`
}

LastUpdatedRecord holds the id and updated at

type LocalServer

type LocalServer struct {
	// contains filtered or unexported fields
}

LocalServer is local server for unit test

func NewLocalServer

func NewLocalServer() *LocalServer

NewLocalServer returns a new instance

func NewLocalServerWithReporter

func NewLocalServerWithReporter(t *testing.T) *LocalServer

NewLocalServerWithReporter inititial a server Automatically clean up data when test is completed

func (*LocalServer) Close

func (s *LocalServer) Close(ctx context.Context)

Close clean up

func (*LocalServer) Create

func (s *LocalServer) Create(ctx context.Context, stubs ...*Stub) error

Create creates stubs in local server

func (*LocalServer) GetIncomingRequests

func (s *LocalServer) GetIncomingRequests(ctx context.Context, option *IncomingQueryOption) ([]*IncomingRequest, error)

GetIncomingRequests gets recorded incoming requests

func (*LocalServer) GetURL

func (s *LocalServer) GetURL(ctx context.Context) string

GetURL gets root url of server

func (*LocalServer) SetNamespace

func (s *LocalServer) SetNamespace(v string)

SetNamespace sets namespace which can be used for isolating test data for each testing

func (*LocalServer) UploadFile

func (s *LocalServer) UploadFile(ctx context.Context, fileID string, file []byte) (string, error)

UploadFile upload file to server

func (*LocalServer) WithNamespace

func (s *LocalServer) WithNamespace(namespace string) *LocalServer

WithNamespace sets namespace with chaining style

type Mode

type Mode string

Mode alias for env mode

type Operator

type Operator struct {
	// OperatorName is the name of operator which is one of the following values
	//  - "contains"
	//  - "not_contains"
	//  - "regex"
	//  - "equal_to"
	//  - "start_with"
	//  - "end_with"
	//  - "length"
	//  - "empty"
	//  - "not_empty"
	Name OperatorName `json:"name" yaml:"name"`

	// Value the expected value, which will be compared with value from incoming request
	Value interface{} `json:"value" yaml:"value"`
}

Operator defines operator name and expected value

func (Operator) IsValid

func (o Operator) IsValid() bool

func (Operator) String

func (o Operator) String() string

String returns string

type OperatorName

type OperatorName string

OperatorName is alias for operator name

const (
	OpContaining    OperatorName = "contains"
	OpNotContaining OperatorName = "not_contains"
	OpRegex         OperatorName = "regex"
	OpEqualTo       OperatorName = "equal_to"
	OpStartWith     OperatorName = "start_with"
	OpEndWith       OperatorName = "end_with"
	OpLength        OperatorName = "length"
	OpEmpty         OperatorName = "empty"
	OpNotEmpty      OperatorName = "not_empty"
)

Defines common operator name Remember to add to AllSupportedOperators

type Proto

type Proto struct {
	ID        int64     `json:"id" yaml:"id"`
	Name      string    `json:"name" yaml:"name"`
	FileID    string    `json:"file_id" yaml:"file_id"`
	Methods   []string  `json:"methods" yaml:"methods" gorm:"serializer:json"`
	Types     []string  `json:"types" yaml:"types" gorm:"serializer:json"`
	CreatedAt time.Time `json:"created_at,omitempty" yaml:"created_at"`
	UpdatedAt time.Time `json:"updated_at,omitempty" yaml:"updated_at"`
}

Proto db model for proto

type Proxy

type Proxy struct {
	// TargetURL is the root url of the target server
	// The relative path will be parsed from incoming request
	TargetURL string `json:"target_url,omitempty" yaml:"target_url"`

	// TargetPath is the relative path of the target API
	// This is optional field, if not provided, it will be the same as original request path
	TargetPath string `json:"target_path,omitempty" yaml:"target_path"`

	// EnableRecord is to enable/disable recording response from remote server
	// A stub will be automatically created in stub store
	EnableRecord bool `json:"enable_record,omitempty" yaml:"enable_record"`
}

Proxy defines proxy settings

func (*Proxy) Scan

func (r *Proxy) Scan(val interface{}) error

Scan implements sqlx JSON scan method

func (Proxy) Value

func (r Proxy) Value() (driver.Value, error)

Value implements sqlx JSON value method

type RemoteServer

type RemoteServer struct {
	// contains filtered or unexported fields
}

RemoteServer communicates with remote mock server

func NewRemoteServer

func NewRemoteServer(rootURL string) *RemoteServer

NewRemoteServer returns a new instance

func NewRemoteServerWithReporter

func NewRemoteServerWithReporter(t *testing.T, rootURL string) *RemoteServer

NewRemoteServerWithReporter inititial a server Automatically clean up data when test is completed

func (*RemoteServer) Close

func (s *RemoteServer) Close(ctx context.Context)

Close clean up server data

func (*RemoteServer) Create

func (s *RemoteServer) Create(ctx context.Context, stubs ...*Stub) error

Create creates stubs in remote server

func (*RemoteServer) GetIncomingRequests

func (s *RemoteServer) GetIncomingRequests(ctx context.Context, option *IncomingQueryOption) ([]*IncomingRequest, error)

GetIncomingRequests gets recorded incoming requests

func (*RemoteServer) GetURL

func (s *RemoteServer) GetURL(ctx context.Context) string

GetURL gets root url of server

func (*RemoteServer) ReplayOnShadowServer

func (s *RemoteServer) ReplayOnShadowServer(ctx context.Context, options ...ReplayOptionFunc) error

ReplayOnShadowServer replays incoming requests (from remote server) to a shadow server (local server) By default, only the last request will be replayed. Use option to change replay option This is to debug the stub on a remote server using IDE

func (*RemoteServer) SetNamespace

func (s *RemoteServer) SetNamespace(v string)

SetNamespace sets namespace which can be used for isolating test data for each testing

func (*RemoteServer) UploadFile

func (s *RemoteServer) UploadFile(ctx context.Context, fileID string, fileBody []byte) (string, error)

UploadFile upload file to server

func (*RemoteServer) WithNamespace

func (s *RemoteServer) WithNamespace(namespace string) *RemoteServer

WithNamespace sets namespace which can be used for isolating test data for each testing

func (*RemoteServer) WithShadowServer

func (s *RemoteServer) WithShadowServer(server Server) *RemoteServer

WithShadowServer sets shadow server

type ReplayOption

type ReplayOption struct {
	// contains filtered or unexported fields
}

ReplayOption defines replaying parameter

type ReplayOptionFunc

type ReplayOptionFunc func(*ReplayOption)

ReplayOptionFunc is function to modify replay option

func ReplayWithLimit

func ReplayWithLimit(limit int) ReplayOptionFunc

ReplayWithLimit sets a limit

func ReplayWithRequestID

func ReplayWithRequestID(ids ...int64) ReplayOptionFunc

ReplayWithRequestID replay with a set of captured request id

type RequestMatching

type RequestMatching struct {
	// Rules to match the request method GET, POST, PUT, DELETE, PATCH
	Method string `json:"method,omitempty" yaml:"method"`

	// Rules to match the url
	URL []Operator `json:"url,omitempty" yaml:"url"`

	// Rules to match header name
	Header []FieldOperator `json:"header,omitempty" yaml:"header"`

	// Rules to match cookie
	Cookie []FieldOperator `json:"cookie,omitempty" yaml:"cookie"`

	// Rules to match request query
	Query []FieldOperator `json:"query,omitempty" yaml:"query"`

	// Rules to match request body by xml or json path
	Body []BodyOperator `json:"body,omitempty" yaml:"body"`
}

RequestMatching defines request matching

func (*RequestMatching) Scan

func (r *RequestMatching) Scan(val interface{}) error

Scan implements sqlx JSON scan method

func (*RequestMatching) Validate

func (r *RequestMatching) Validate(ctx context.Context) error

func (RequestMatching) Value

func (r RequestMatching) Value() (driver.Value, error)

Value implements sqlx JSON value method

type ResetQueryOption

type ResetQueryOption struct {
	Namespace string `json:"namespace" yaml:"namespace"`
	Tag       string `json:"tag" yaml:"tag"`
}

type Response

type Response struct {
	// Required. Define the response status code
	// GRPC. Default 0 OK: https://grpc.github.io/grpc/core/md_doc_statuscodes.html
	// HTTP. Default 200 OK: https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
	StatusCode int `json:"status_code,omitempty" yaml:"status_code"`

	// Body defines response body
	// Client can submit raw JSON object or base64 encoded string for HTML, XML, ... via API
	// If client submits stubs using YAML format via API, then raw string can be used for any text based content
	Body Body `json:"body,omitempty" yaml:"body"`

	// This is the id of uploaded file that can be used to simulate the download
	// Or can be used to respond a large data payload which is not suitable to save in database
	BodyFile string `json:"body_file,omitempty" yaml:"body_file"`

	// Optional. Define response cookies
	// This is not applied for GRPC
	Cookies []Cookie `json:"cookies,omitempty" yaml:"cookies"`

	// Optional. Define response http headers
	// This is equivalent to response metadata in GRPC
	Header map[string]string `json:"header,omitempty" yaml:"header"`

	// Error is optional. Defines response error for grpc
	// This is not applied for HTTP since body and status code can be used
	Error *ResponseError `json:"error,omitempty" yaml:"error"`

	// Optional. If defined, then executed template will override response data
	Template *Template `json:"template,omitempty" yaml:"template"`
}

Response defines a response

func HTMLResponse

func HTMLResponse(html string) *Response

HTMLResponse is convenient constructor to initialize response with htnl body

func JSONResponse

func JSONResponse(body interface{}) *Response

JSONResponse is convenient constructor to initialize response with JSON body The input parameter will be decoded to JSON

func NewResponse

func NewResponse() *Response

NewResponse creates new response

func NewResponseFromHTTP

func NewResponseFromHTTP(res *http.Response) *Response

NewResponseFromHTTP parses from http response

func XMLResponse

func XMLResponse(body interface{}) *Response

XMLResponse is convenient constructor to initialize response with XML body The input parameter will be decoded to XML

func (*Response) Clone

func (r *Response) Clone() *Response

Clone clones response and its properties

func (*Response) LoadBodyFromFile

func (r *Response) LoadBodyFromFile(ctx context.Context, fileStorage fs.FileStorage) error

LoadBodyFromFile loads body from file storage

func (*Response) LoadBodyFromTemplate

func (r *Response) LoadBodyFromTemplate(ctx context.Context, data *TemplateData) error

LoadBodyFromTemplate parses dynamic response from template

func (*Response) Scan

func (r *Response) Scan(val interface{}) error

Scan implements sqlx JSON scan method

func (*Response) Validate

func (r *Response) Validate(ctx context.Context) error

Validate returns a non-nil error if invalid

func (Response) Value

func (r Response) Value() (driver.Value, error)

Value implements sqlx JSON value method

func (*Response) WithBody

func (r *Response) WithBody(contentType string, body []byte) *Response

WithBody sets body

func (*Response) WithCookie

func (r *Response) WithCookie(name string, value string) *Response

WithCookie set cookies

func (*Response) WithError

func (r *Response) WithError(msg string, details ...*ErrorDetail) *Response

func (*Response) WithFileBody

func (r *Response) WithFileBody(contentType string, fileID string) *Response

WithFileBody sets file id for response body This should be used for large data response such as images, pdf, ... which are too large to store at database This can also be used to simulate the download request Handler will download file by this id and assign to body

func (*Response) WithHeader

func (r *Response) WithHeader(name string, value string) *Response

WithHeader sets header

func (*Response) WithRedirect

func (r *Response) WithRedirect(url string) *Response

WithRedirect sets redirect url Use WithStatusCode if want to customize the redirect code

func (*Response) WithStatusCode

func (r *Response) WithStatusCode(s int) *Response

WithStatusCode sets the status code

func (*Response) WriteTo

func (r *Response) WriteTo(ctx context.Context, w http.ResponseWriter) error

WriteTo writes response

type ResponseError

type ResponseError struct {
	Message string         `json:"message,omitempty" yaml:"message"`
	Details []*ErrorDetail `json:"details,omitempty" yaml:"details"`
}

ResponseError defines the response error. Only applicable for grpc This is equivalent to status.Status in GRPC

func (*ResponseError) Clone

func (r *ResponseError) Clone() *ResponseError

Clone clones the response error and its properties

type ResponseScript

type ResponseScript struct {
	StatusCode int               `json:"status_code,omitempty" yaml:"status_code"`
	Body       string            `json:"body,omitempty" yaml:"body"`
	Cookies    []Cookie          `json:"cookies,omitempty" yaml:"cookies"`
	Headers    map[string]string `json:"headers,omitempty" yaml:"headers"`
	Error      *ResponseError    `json:"error,omitempty" yaml:"error"`
}

ResponseScript represents for http response script

func (*ResponseScript) AssignTo

func (s *ResponseScript) AssignTo(r *Response)

type SchemaType

type SchemaType string
const (
	SchemaTypeJSON SchemaType = "json"
	SchemaTypeYAML SchemaType = "yaml"
)

type Server

type Server interface {
	SetNamespace(v string)
	GetURL(ctx context.Context) string
	Create(ctx context.Context, stubs ...*Stub) error
	UploadFile(ctx context.Context, fileID string, file []byte) (string, error)
	Close(ctx context.Context)
}

Server defines server interface

type StatusStore

type StatusStore interface {
	GetLastUpdatedStub(ctx context.Context, namespace string) (*LastUpdatedRecord, error)
	GetLastUpdatedProto(ctx context.Context) (*LastUpdatedRecord, error)
}

StatusStore defines interface to get latest updated information

type Stub

type Stub struct {
	ID int64 `json:"id" yaml:"id"`

	// Description describes the stub
	Description string `json:"description,omitempty" yaml:"description"`

	// WithNamespace sets namespace which can be used for isolating test data for each testing
	// This can be applied for the unit test which uses different test server for different test
	// With integration or unit test with single test server (which is not recommended), namespace should be empty
	Namespace string `json:"namespace,omitempty" yaml:"namespace"`

	// Tag is to add custom tag for grouping stub
	// Owner can add whatever they want to classify the stub
	Tag string `json:"tag,omitempty" yaml:"tag"`

	// Protocol defines protocol of incoming requests
	// Value is either: http or grpc. Default value is http
	Protocol string `json:"protocol,omitempty" yaml:"protocol"`

	// Matching rules which will be matched against the incoming requests
	Request *RequestMatching `json:"request,omitempty" yaml:"request"`

	// The expected response which includes the body, header and cookies
	Response *Response `json:"response,omitempty" yaml:"response"`

	// The mock server will act as reserved proxy if this settings are provided
	Proxy *Proxy `json:"proxy,omitempty" yaml:"proxy"`

	Active bool `json:"active,omitempty" yaml:"active"`

	// WithWeight sets weight. The higher weight, the higher priority
	// It is sometimes the case that you'll want to declare two or more stub mappings that "overlap",
	// in that a given request would be a match for more than one of them
	// By default, the most recently added matching stub will be used to satisfy the request
	// However, in some cases it is useful to exert more control
	Weight int `json:"weight,omitempty" yaml:"weight"`

	Settings StubSettings `json:"settings,omitempty" yaml:"settings"`

	CreatedAt time.Time `json:"created_at,omitempty" yaml:"created_at"`
	UpdatedAt time.Time `json:"updated_at,omitempty" yaml:"updated_at"`
}

Stub defines a stub

func NewStub

func NewStub() *Stub

NewStub returns a new stub

func SelectStubs

func SelectStubs(stubs []*Stub) *Stub

SelectStubs gets the item with the highest priority. If weight is not set, then get the latest

func (*Stub) Clone

func (s *Stub) Clone() *Stub

Clone clones stubs

func (*Stub) For

func (s *Stub) For(method string, urlMatchingFuncs ...CreateOperator) *Stub

For sets http request method and matching functions for URL If method is empty, it will be ignored with matching logic

func (*Stub) ForAny

func (s *Stub) ForAny(urlMatchingFuncs ...CreateOperator) *Stub

ForAny for matching request with any method

func (*Stub) ForGRPC

func (s *Stub) ForGRPC(urlMatchingFuncs ...CreateOperator) *Stub

ForGRPC for matching request with grpc method

func (*Stub) ForceJSON

func (s *Stub) ForceJSON(ctx context.Context) string

ForceJSON marshall to json

func (*Stub) HasTemplate

func (s *Stub) HasTemplate() bool

HasTemplate returns true if response template is defined

func (*Stub) IsReversed

func (s *Stub) IsReversed() bool

IsReversed returns true if stub is reverse proxy

func (*Stub) Send

func (s *Stub) Send(ctx context.Context, server Server) error

Send submits stub to server for matching upcoming requests

func (*Stub) ShouldDeactivateWhenMatched

func (s *Stub) ShouldDeactivateWhenMatched() *Stub

ShouldDeactivateWhenMatched deactivates when matched

func (*Stub) ShouldDelay

func (s *Stub) ShouldDelay(d time.Duration) *Stub

ShouldDelay sets delay duration Use this to simulate the slow API response time

func (*Stub) Validate

func (s *Stub) Validate(ctx context.Context) error

Validate returns an non-nil error if stub is invalid

func (*Stub) WillReturn

func (s *Stub) WillReturn(resp *Response) *Stub

WillReturn sets the response

func (*Stub) WithCookie

func (s *Stub) WithCookie(name string, createFunc CreateOperator) *Stub

WithCookie sets cookie matching operator

func (*Stub) WithDescription

func (s *Stub) WithDescription(desc string) *Stub

WithDescription sets description

func (*Stub) WithEnableRecord

func (s *Stub) WithEnableRecord(v bool) *Stub

WithEnableRecord enables recording proxy as a stub

func (*Stub) WithHeader

func (s *Stub) WithHeader(name string, createFunc CreateOperator) *Stub

WithHeader sets header matching operator

func (*Stub) WithID

func (s *Stub) WithID(id int64) *Stub

WithID sets id

func (*Stub) WithInactive

func (s *Stub) WithInactive() *Stub

WithInactive sets to inactive

func (*Stub) WithNamespace

func (s *Stub) WithNamespace(namespace string) *Stub

WithNamespace sets namespace which can be used for isolating test data for each testing This can be applied for the unit test which uses different test server for different test With integration or unit test with single test server (which is not recommended), namespace should be empty

func (*Stub) WithQuery

func (s *Stub) WithQuery(name string, createFunc CreateOperator) *Stub

WithQuery sets query matching operator

func (*Stub) WithRequestBody

func (s *Stub) WithRequestBody(createFunc CreateBodyOperator) *Stub

WithRequestBody sets body matching operator

func (*Stub) WithTag

func (s *Stub) WithTag(tag string) *Stub

WithTag sets tag for stub

func (*Stub) WithTargetURL

func (s *Stub) WithTargetURL(url string) *Stub

WithTargetURL sets base target url, request will be forwarded to the given url

func (*Stub) WithWeight

func (s *Stub) WithWeight(weight int) *Stub

WithWeight sets weight. The higher weight, the higher priority It is sometimes the case that you'll want to declare two or more stub mappings that "overlap", in that a given request would be a match for more than one of them By default, the most recently added matching stub will be used to satisfy the request However, in some cases it is useful to exert more control

type StubMemory

type StubMemory struct {
	// contains filtered or unexported fields
}

StubMemory implements in memory store which using for unit test

func NewStubMemory

func NewStubMemory() *StubMemory

NewStubMemory returns a new instance

func (*StubMemory) Create

func (db *StubMemory) Create(ctx context.Context, stubs ...*Stub) error

Create adds to memory

func (*StubMemory) CreateIncomingRequest

func (db *StubMemory) CreateIncomingRequest(ctx context.Context, r *IncomingRequest) error

CreateIncomingRequest saves the incomes request

func (*StubMemory) CreateProto

func (db *StubMemory) CreateProto(ctx context.Context, protos ...*Proto) error

func (*StubMemory) Delete

func (db *StubMemory) Delete(ctx context.Context, id int64) error

Delete deletes a stub

func (*StubMemory) GetAll

func (db *StubMemory) GetAll(_ context.Context, namespace string) ([]*Stub, error)

GetAll gets all records

func (*StubMemory) GetIncomingRequests

func (db *StubMemory) GetIncomingRequests(ctx context.Context, option *IncomingQueryOption) ([]*IncomingRequest, error)

GetIncomingRequests returns incoming requests

func (*StubMemory) GetProtos

func (db *StubMemory) GetProtos(ctx context.Context) ([]*Proto, error)

func (*StubMemory) Reset

func (db *StubMemory) Reset(ctx context.Context, option *ResetQueryOption) error

Reset clear data

type StubSettings

type StubSettings struct {
	// This 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
	DeactivateWhenMatched bool `json:"deactivate_when_matched,omitempty" yaml:"deactivate_when_matched"`

	// It is sometimes we want to simulate slow response API.
	// Rio supports this feature by set delay duration
	DelayDuration time.Duration `json:"delay_duration,omitempty" swaggertype:"primitive,integer" yaml:"delay_duration"`

	// StoreVersion is a system field to control data structure version for stub
	// Value will be overrided by system
	StoreVersion int `json:"store_version,omitempty" yaml:"store_version"`
}

StubSettings stub settings

func (*StubSettings) Clone

func (r *StubSettings) Clone() *StubSettings

Clone clones new instance

func (*StubSettings) Scan

func (r *StubSettings) Scan(val interface{}) error

Scan implements sqlx JSON scan method

func (StubSettings) Value

func (r StubSettings) Value() (driver.Value, error)

Value implements sqlx JSON value method

type StubStore

type StubStore interface {
	Create(ctx context.Context, stubs ...*Stub) error
	Delete(ctx context.Context, id int64) error
	GetAll(ctx context.Context, namespace string) ([]*Stub, error)
	CreateProto(ctx context.Context, protos ...*Proto) error
	GetProtos(ctx context.Context) ([]*Proto, error)
	CreateIncomingRequest(ctx context.Context, r *IncomingRequest) error
	GetIncomingRequests(ctx context.Context, option *IncomingQueryOption) ([]*IncomingRequest, error)
	Reset(ctx context.Context, option *ResetQueryOption) error
}

StubStore stores the stub information

type Template

type Template struct {
	// Supported json and yaml. Default value is yaml
	ScriptSchemaType SchemaType `json:"script_schema_type" yaml:"script_schema_type"`

	// Script is content of template file. See ResponseScript for the detail structure
	Script string `json:"script" yaml:"script"`
}

Template defines setting for response template

func (*Template) Execute

func (t *Template) Execute(ctx context.Context, data *TemplateData) (*ResponseScript, error)

Execute executes the template. Only go-template is supported at the moment For supported function in Go template, see http://masterminds.github.io/sprig/

type TemplateData

type TemplateData struct {
	// Request is the incoming request
	// Which can be accessed from template as {{ .Request.<FielName> }}
	Request *http.Request `json:"request,omitempty" yaml:"request"`

	// Grpc is grpc request
	// Which can be accessed from template as {{ .Grpc.<FielName> }}
	Grpc *GrpcRequest `json:"grpc,omitempty" yaml:"grpc"`
	// contains filtered or unexported fields
}

TemplateData holds all available data for feeding to template Either HTTP or GRPC is supported

func (*TemplateData) JSONBody

func (t *TemplateData) JSONBody() map[string]interface{}

JSONBody parses request body (or request parameter in grpc) to json Panic if error for more convenient when using with template

Directories

Path Synopsis
cmd
internal
api
log
test/mock
Package mock is a generated GoMock package.
Package mock is a generated GoMock package.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL