moco Documentation

Official Java developer resources and documentation for dreamhead/moco with 4.4k stars. Verified technical context for AI.

4,447 stars Java Java 1 file · ~43,710 tokens
RAW Doc

File: README.md

[Moco](https://github.com/dreamhead/moco)


Moco is an easy setup stub framework.

Latest Release

Note: Moco 2.0.0 requires Java 17 or later.

User Voice

Why

Integration, especially based on HTTP protocol, e.g. web service, REST etc, is wildly used in most of our development.

In the old days, we just deployed another WAR to an application server, e.g. Jetty or Tomcat etc. As we all know, it's so boring to develop a WAR and deploy it to any application server, even if we use an embeded server. And the WAR needs to be reassembled even if we just want to change a little bit.

Quick Start

  • Download Standalone Moco Runner
  • Write your own configuration file to describe your Moco server configuration as follow:
json
[
  {
    "response" :
      {
        "text" : "Hello, Moco"
      }
  }
]

(foo.json)

  • Run Moco HTTP server with the configuration file.
shell
java -jar moco-runner-<version>-standalone.jar http -p 12306 -c foo.json

Documents

Build

Requirements: JDK 17+ and Gradle

  • Clone Moco
shell
git clone [email protected]:dreamhead/moco.git
  • Build Moco
shell
./gradlew build
  • Build uberjar
shell
./gradlew uberjar
  • Check code before commit
shell
./gradlew check

Contributing

Check out what you can help here if you do not have any existing idea.

Copyright 2012-2026 ZHENG Ye

Licensed under MIT License (the "License"); You may obtain a copy of the License in MIT-LICENSE.txt, or at:

https://raw.github.com/dreamhead/moco/master/MIT-LICENSE.txt

Powered By

Star History


File: MIT-LICENSE.txt

The MIT License (MIT)
Copyright (c) 2012-2026 ZHENG Ye

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


File: moco-core/src/test/resources/gbk.json

[Binary file]


File: moco-core/src/test/resources/details/bar.json

[
{
"request" : {
"uri" : "/bar"
},
"response" : {
"text" : "bar"
}
}
]


File: moco-core/src/test/resources/details/foo.json

[
{
"request" : {
"uri" : "/foo"
},
"response" : {
"text" : "foo"
}
}
]


File: moco-doc/apis.md

HTTP(s) APIs

Moco mainly focuses on server configuration. There are only two kinds of API right now: Request and Response.

That means if we get the expected request and then return our response. Now, you can see a Moco reference in details.

WARNING the json configuration below is just a snippet for one pair of request and response, instead of the whole configuration file.

Table of Contents

Composite Java API Design

Moco Java API is designed in functional fashion which means you can composite any request or response easily.

java
server.request(and(by(uri("/target")), by(version(VERSION_1_0)))).response(with(text("foo")), header("Content-Type", "text/html"));

Description as comment

@Since 0.7

In all JSON APIs, you can use description to describe what is this session for. It's just used as comment, which will be ignored in runtime.

json
[
    {
        "description": "any response",
        "response": {
            "text": "foo"
        }
    }
]

Request

Content

@Since 0.7

If you want to response according to request content, Moco server can be configured as following:

  • Java API
java
server.request(by("foo")).response("bar");
  • JSON
json
{
  "request" :
    {
      "text" : "foo"
    },
  "response" :
    {
      "text" : "bar"
    }
}

If request content is too large, you can put it in a file:

  • Java API
java
server.request(by(file("foo.request"))).response("bar");
  • JSON
json
{
  "request" :
    {
      "file" : "foo.request"
    },
  "response" :
    {
      "text" : "bar"
    }
}

@Since 1.2.0

You can also match binary request directly.

java
server.request(binary(new byte[] {1, 2, 3})).response("bar");

InputStream is also supported in binary.

java
server.request(binary(new ByteArrayInputStream(new byte[]{1, 2, 3}))).response("bar");

URI

@Since 0.7

If request uri is your major focus, Moco server could be like this:

  • Java API
java
server.request(by(uri("/foo"))).response("bar");
  • JSON
json
{
  "request" :
    {
      "uri" : "/foo"
    },
  "response" :
    {
      "text" : "bar"
    }
}

Query Parameter

@Since 0.7

Sometimes, your request has parameters in query:

  • Java API
java
server.request(and(by(uri("/foo")), eq(query("param"), "blah"))).response("bar");
  • JSON
json
{
  "request" :
    {
      "uri" : "/foo",
      "queries" : 
        {
          "param" : "blah"
        }
    },
  "response" :
    {
      "text" : "bar"
    }
}

HTTP Method

@Since 0.7

It's easy to response based on specified HTTP method:

  • Java API
java
server.get(by(uri("/foo"))).response("bar");
  • JSON
json
{
  "request" :
    {
      "method" : "get",
      "uri" : "/foo"
    },
  "response" :
    {
      "text" : "bar"
    }
}

Also for POST method:

  • Java API
java
server.post(by("foo")).response("bar");
  • JSON
json
{
  "request" :
    {
      "method" : "post",
      "text" : "foo"
    },
  "response" :
    {
      "text" : "bar"
    }
}

and PUT method:

  • Java API
java
server.put(by("foo")).response("bar");
  • JSON
json
{
  "request" :
    {
      "method" : "put",
      "text" : "foo"
    },
  "response" :
    {
      "text" : "bar"
    }
}

and DELETE method:

  • Java API
java
server.delete(by(uri("/foo"))).response(status(200));
  • JSON
json
{
  "request" :
    {
      "method" : "delete",
      "uri" : "/foo"
    },
  "response" :
    {
      "status" : "200"
    }
}

If you do need other method, feel free to specify method directly:

  • Java API
java
server.request(by(method("HEAD"))).response("bar");
  • JSON
json
{
  "request" :
    {
      "method" : "HEAD"
    },
  "response" :
    {
      "text" : "bar"
    }
}

Version

@Since 0.7

We can return different response for different HTTP version:

  • Java API
java
server.request(by(version("HTTP/1.0"))).response("version");
  • JSON
json
{
  "request": 
    {
      "version": "HTTP/1.0"
    },
  "response": 
    {
      "text": "version"
    }
}

@Since 0.7

We will focus HTTP header at times:

  • Java API
java
server.request(eq(header("foo"), "bar")).response("blah")
  • JSON
json
{
  "request" :
    {
      "method" : "post",
      "headers" : 
      {
        "content-type" : "application/json"
      }
    },
  "response" :
    {
      "text" : "bar"
    }
}

@Since 0.7

Cookie is widely used in web development.

  • Java API
java
server.request(eq(cookie("loggedIn"), "true")).response(status(200));
  • JSON
json
{
  "request" :
    {
      "uri" : "/cookie",
      "cookies" :
        {
          "login" : "true"
        }
    },
  "response" :
    {
      "text" : "success"
    }
}

Form

@Since 0.7

In web development, form is often used to submit information to server side.

  • Java API
java
server.post(eq(form("name"), "foo")).response("bar");
  • JSON
json
{
  "request" :
    {
      "method" : "post",
      "forms" :
        {
          "name" : "foo"
        }
    },
  "response" : 
    {
      "text" : "bar"
    }
}

XML

@Since 0.7

XML is a popular format for Web Services. When a request is in XML, only the XML structure is important in most cases and whitespace can be ignored. The xml operator can be used for this case.

  • Java API
java
server.request(xml(text("<request><parameters><id>1</id></parameters></request>"))).response("foo");
  • JSON
json
{
  "request": 
    {
      "uri": "/xml",
      "text": 
        {
          "xml": "<request><parameters><id>1</id></parameters></request>"
        }
    },
  "response": 
    {
      "text": "foo"
    }
}

NOTE: Please escape the quote in text.

The large request can be put into a file:

json
{
   "request": 
     {
        "uri": "/xml",
        "file": 
          {
            "xml": "your_file.xml"
          }
    },
  "response": 
    {
      "text": "foo"
    }
}

XPath

@Since 0.7

For the XML/HTML request, Moco allows us to match request with XPath.

  • Java API
java
server.request(eq(xpath("/request/parameters/id/text()"), "1")).response("bar");
  • JSON
json
{
  "request" :
    {
      "method" : "post",
      "xpaths" : 
        {
          "/request/parameters/id/text()" : "1"
        }
    },
  "response" :
    {
      "text" : "bar"
    }
}

XML Struct

@Since 1.3.0

Moco also allows you to match an XML request only for same struct no matter what actual content is.

  • Java API
java
server.request(struct(xml("<foo>1</foo>")).response("response_for_xml_struct_request");
  • JSON
json
{
  "request":
  {
    "struct":
    {
      "xml" : "<foo>1</foo>"
    }
  },
  "response":
  {
    "text": "response_for_xml_struct_request"
  }
}

JSON Request

JSON Text

@Since 0.7

Json is rising with RESTful style architecture. Just like XML, in the most case, only JSON structure is important, so json operator can be used.

  • Java API
java
server.request(json(text("{\"foo\":\"bar\"}"))).response("foo");

@Since 0.12.0
json will return a resource from 1.3.0.

java
server.request(by(json(text("{\"foo\":\"bar\"}")))).response("foo");

Note that this functionality is implemented in Jackson, please make sure your POJO is written in Jackson acceptable format.

  • JSON
json
{
  "request":
    {
      "uri": "/json",
      "text":
        {
          "json": "{\"foo\":\"bar\"}"
        }
    },
  "response":
    {
      "text": "foo"
    }
}

NOTE: Please escape the quote in text.

JSON Shortcut

If the response is JSON, we don't need to write JSON text with escape character in code.

@Since 0.10.2

You can give a POJO to Java API, it will be converted JSON text.

java
server.request(json(pojo)).response("foo");

@Since 0.12.0
json will return a resource from 1.3.0.

java
server.request(by(json(pojo))).response("foo");

@Since 0.9.2

As you have seen, it is so boring to write json with escape character, especially in json configuration. So you can try the json shortcut. The upper case could be rewritten as following:

  • JSON
json
{
    "request": {
        "uri": "/json",
        "json": {
            "foo": "bar"
        }
    },
    "response": {
        "text": "foo"
    }
}

JSON File

@Since 0.7

The large request can be put into a file:

  • Java API
java
server.request(json(file("your_file.json"))).response("foo");
  • JSON
json
{
  "request":
    {
      "uri": "/json",
      "file":
        {
          "json": "your_file.json"
        }
    },
  "response":
    {
      "text": "foo"
    }
}

JSONPath

@Since 0.7

For the JSON request, Moco allows us to match request with JSONPath.

  • Java API
java
server.request(eq(jsonPath("$.book[*].price"), "1")).response("response_for_json_path_request");
  • JSON
json
{
  "request":
    {
      "json_paths":
        {
          "$.book[*].price": "1"
	    }
    },
  "response":
    {
      "text": "response_for_json_path_request"
    }
}

JSON Struct

@Since 1.3.0

Moco also allows you to match a JSON request only for same struct no matter what actual content is.

  • Java API
java
server.request(struct(json("{\"foo\":1}")).response("response_for_json_struct_request");
  • JSON
json
{
  "request":
  {
    "struct":
    {
      "json" : {
        "foo" :1
      }
    }
  },
  "response":
  {
    "text": "response_for_json_struct_request"
  }
}

Operator

Moco also supports some operators which helps you write your expectation easily.

Match

@Since 0.7

You may want to match your request with regular expression, match could be your helper:

  • Java API
java
server.request(match(uri("/\\w*/foo"))).response("bar");
  • JSON
json
{
  "request":
    {
      "uri":
        {
          "match": "/\\w*/foo"
        }
    },
  "response":
    {
      "text": "bar"
    }
}

Moco is implemented by Java regular expression, you can refer here for more details.

Starts With

@Since 0.9.2

starsWith operator can help you decide if the request information starts with a piece of text.

  • Java API
java
server.request(startsWith(uri("/foo"))).response("bar");
  • JSON
json
{
  "request":
    {
      "uri":
        {
          "startsWith": "/foo"
        }
    },
  "response":
    {
      "text": "bar"
    }
}

Ends With

@Since 0.9.2

endsWith operator can help you decide if the request information ends with a piece of text.

  • Java API
java
server.request(endsWith(uri("foo"))).response("bar");
  • JSON
json
{
  "request":
    {
      "uri":
        {
          "endsWith": "foo"
        }
    },
  "response":
    {
      "text": "bar"
    }
}

Contain

@Since 0.9.2

contain operator helps you know whether the request information contains a piece of text.

  • Java API
java
server.request(contain(uri("foo"))).response("bar");
  • JSON
json
{
  "request":
    {
      "uri":
        {
          "contain": "foo"
        }
    },
  "response":
    {
      "text": "bar"
    }
}

Exist

@Since 0.9.2

exist operator is used to decide whether the request information exists.

  • Java API
java
server.request(exist(header("foo"))).response("bar");
  • JSON
json
{
  "request":
    {
      "headers": {
        "foo": {
          "exist" : "true"
        }
      }
    },
  "response":
    {
      "text": "bar"
    }
}

For JSON API, you can decide whether the information does not exist.

  • JSON
json
{
  "request":
    {
      "headers": {
        "foo": {
          "exist" : "false"
        }
      }
    },
  "response":
    {
      "text": "bar"
    }
}

Path

@Since 1.4.0

path operator is provided to match uri with path variable.

  • Java API
java
server.request(path(uri("/path/{path}/sub/{sub}"))).response("bar");
  • JSON
json
{
  "request": 
  {
    "uri": 
    {
      "path": "/path/{path}/sub/{sub}"
    }
  },
  "response": 
  {
    "text": "sub"
  }
}

Conditional

@Since 1.3.0

If you want to implement your own matcher, you can write with conditional API which is supported in Java code.

java
server.request(conditional(request -> request.getContent().toString().equals("foo"))).response("foo");

Response

Content

@Since 0.7

As you have seen in previous example, response with content is pretty easy.

  • Java API
java
server.request(by("foo")).response("bar");
  • JSON
json
{
  "request" :
    {
      "text" : "foo"
    },
  "response" :
    {
      "text" : "bar"
    }
}

The same as request, you can response with a file if content is too large to put in a string.

  • Java API
java
server.request(by("foo")).response(file("bar.response"));
  • JSON
json
{
  "request" :
    {
      "text" : "foo"
    },
  "response" :
    {
      "file" : "bar.response"
    }
}

@Since 0.10.1

You can specify file charset if you want to see it in correct encoding in console.

  • Java API
java
server.response(file("src/test/resources/gbk.response", Charset.forName("GBK")));
  • JSON
json
[
  {
    "response":
    {
      "file":
      {
        "name": "gbk.response",
        "charset": "GBK"
      }
    }
  }
]

Charset can also be used in path resource.

  • Java API
java
server.response(pathResource("src/test/resources/gbk.response", Charset.forName("GBK")));
  • JSON
json
[
  {
    "response":
    {
      "path_resource":
      {
        "name": "gbk.response",
        "charset": "GBK"
      }
    }
  }
]

@Since 1.2.0

You can also write binary response directly.

java
server.response(binary(new byte[] {1, 2, 3}));

@Since 1.2.0

If your response need to be transformed for some reason, e.g. encryption, you can transform your content.

java
server.response(text("hello").transform(raw -> {
    byte[] transformed = new byte[raw.length];
    for (int i = 0; i < raw.length; i++) {
        transformed[i] = (byte) (raw[i] + 1);
    }
    return transformed;
}));

@Since 1.2.0

If you need your own specific response content, you can write your own code with lambda.

java
server.response(text((request) -> "foo"));
java
server.response(binary((request) -> new byte[] {1, 2, 3}));

Currently, this function uses com.github.dreamhead.moco.Request as argument which provide content only. If you need HTTP information in HTTP scenario, you can cast request to com.github.dreamhead.moco.HttpRequest.

java
server.response(text((request) -> ((HttpRequest)request).getUri()));

Status Code

@Since 0.7

Moco also supports HTTP status code response.

  • Java API
java
server.request(by("foo")).response(status(200));
  • JSON
json
{
  "request" :
    {
      "text" : "foo"
    },
  "response" :
    {
      "status" : 200
    }
}

Version

@Since 0.7

By default, response HTTP version is supposed to request HTTP version, but you can set your own HTTP version:

  • Java API
java
server.response(version(HttpProtocolVersion.VERSION_1_0));
  • JSON
json
{
  "request":
    {
      "uri": "/version10"
    },
  "response":
    {
      "version": "HTTP/1.0"
    }
}

@Since 0.7

We can also specify HTTP header in response.

  • Java API
java
server.request(by("foo")).response(header("content-type", "application/json"));
  • JSON
json
{
  "request" :
    {
      "text" : "foo"
    },
  "response" :
    {
      "headers" :
        {
          "content-type" : "application/json"
        }
    }
}

Proxy

Single URL

@Since 0.8

We can also response with the specified url, just like a proxy.

  • Java API
java
server.request(by("foo")).response(proxy("http://www.github.com"));
  • JSON
json
{
  "request" :
    {
      "text" : "foo"
    },
  "response" :
    {
      "proxy" : "http://www.github.com"
    }
}

Actually, proxy is more powerful than that. It can forward the whole request to the target url, including HTTP method, version, header, content etc.

Failover

@Since 0.7

Besides the basic functionality, proxy also support failover, which means if remote server is not available temporarily, the server will know recovery from local configuration.

  • Java API
java
server.request(by("foo")).response(proxy("http://www.github.com", failover("failover.json")));
  • JSON
json
{
  "request" :
    {
      "text" : "foo"
    },
  "response" :
    {
      "proxy" :
        {
          "url" : "http://localhost:12306/unknown",
          "failover" : "failover.json"
        }
    }
}

Proxy will save request/response pair into your failover file. If the proxy target is not reachable, proxy will failover from the file. This feature is very useful for development environment, especially for the case the integration server is not stable.

As the file suffix suggests, this failover file is actually a JSON file, which means we can read/edit it to return whatever we want.

Playback

@Since 0.9.1

Moco also supports playback which also save remote request and response into local file. The difference between failover and playback is that playback only accesses remote server when local request and response are not available.

  • Java API
java
server.request(by("foo")).response(proxy("http://www.github.com", playback("playback.json")));
  • JSON
json
{
  "request" :
    {
      "text" : "foo"
    },
  "response" :
    {
      "proxy" :
        {
          "url" : "http://localhost:12306/unknown",
          "playback" : "playback.json"
        }
    }
}

Customize Failover/Playback Status

@Since 1.0.0

You can customize what remote statuses means that remote server is not available.

  • Java API
java
server.request(by("foo")).response(proxy("http://www.github.com", failover("failover.json", 400, 500)));
  • JSON
json
{
    "request" :
    {
        "text": "foo"
    },
    "response" :
    {
        "proxy" :
        {
            "url" : "http://www.github.com",
            "failover" : {
                "file": "failover.json",
                "status": [404, 500]
            }
        }
    }
}

Batch URLs

@Since 0.9.1

If we want to proxy with a batch of URLs in the same context, proxy can also help us.

  • Java API
java
server.get(match(uri("/proxy/.*"))).response(proxy(from("/proxy").to("http://localhost:12306/target")));
  • JSON
json
{
    "request" :
    {
        "uri" : {
            "match" : "/proxy/.*"
        }
    },
    "response" :
    {
        "proxy" : {
            "from" : "/proxy",
            "to" : "http://localhost:12306/target"
        }
    }
}

Same with single url, you can also specify a failover.

  • Java API
java
server.request(match(uri("/proxy/.*")))
      .response(proxy("http://localhost:12306/unknown"), failover("failover.response")));
  • JSON
json
{
    "request" :
    {
        "uri" : {
            "match" : "/failover/.*"
        }
    },
    "response" :
    {
        "proxy" :
        {
            "from" : "/failover",
            "to" : "http://localhost:12306/unknown",
            "failover" : "failover.response"
        }
    }
}

and playback.

  • Java API
java
server.request(match(uri("/proxy/.*")))
      .response(proxy("http://localhost:12306/unknown"), playback("playback.response")));
  • JSON
json
{
    "request" :
    {
        "uri" : {
            "match" : "/failover/.*"
        }
    },
    "response" :
    {
        "proxy" :
        {
            "from" : "/failover",
            "to" : "http://localhost:12306/unknown",
            "playback" : "playback.response"
        }
    }
}

As you may find, we often set request match same context with response, so Moco gives us a shortcut to do that.

  • Java API
java
server.proxy(from("/proxy").to("http://localhost:12306/target"));
  • JSON
json
{
    "proxy" : {
        "from" : "/proxy",
        "to" : "http://localhost:12306/target"
    }
}

Same with failover

  • Java API

    java
    server.proxy(from("/proxy").to("http://localhost:12306/unknown"), failover("failover.response"));
  • JSON

    json
    {
      "proxy" :
      {
          "from" : "/failover",
          "to" : "http://localhost:12306/unknown",
          "failover" : "failover.response"
      }
    }

and playback

  • Java API

    java
    server.proxy(from("/proxy").to("http://localhost:12306/unknown"), playback("playback.response"));
  • JSON

    json
    {
      "proxy" :
      {
          "from" : "/failover",
          "to" : "http://localhost:12306/unknown",
          "playback" : "playback.response"
      }
    }

Redirect

@Since 0.7

Redirect is a common case for normal web development. We can simply redirect a request to different url.

  • Java API
java
server.get(by(uri("/redirect"))).redirectTo("http://www.github.com");
  • JSON
json
{
  "request" :
    {
      "uri" : "/redirect"
    },
  "redirectTo" : "http://www.github.com"
}

@Since 0.7

Cookie can also be in the response.

  • Java API
java
server.response(cookie("loggedIn", "true"), status(302));
  • JSON
json
{
  "request" :
    {
      "uri" : "/cookie"
    },
  "response" :
    {
      "cookies" :
      {
        "login" : "true"
      }
    }
}

Cookie attributes are sent in http response, which are used by browsers to determine when to delete a cookie, block a cookie or whether to send a cookie to the server.

Path

@Since 0.11.1

Path cookie attribute defines the scope of the cookie. You can add your own path cookie attribute to your response.

  • Java
java
server.response(cookie("loggedIn", "true", path("/")), status(302));
  • JSON
json
{
  "request" :
    {
      "uri" : "/cookie"
    },
  "response" :
    {
      "cookies" :
      {
        "login" : {
            "value" : "true",
            "path" : "/"
        }
      }
    }
}

Domain

@Since 0.11.1

Domain cookie attribute defines the scope of the cookie. You can add your own domain cookie attribute to your response.

  • Java
java
server.response(cookie("loggedIn", "true", domain("github.com")), status(302));
  • JSON
json
{
  "request" :
    {
      "uri" : "/cookie"
    },
  "response" :
    {
      "cookies" :
      {
        "login" : {
            "value" : "true",
            "domain" : "github.com"
        }
      }
    }
}

Secure

@Since 0.11.1

A secure cookie can only be transmitted over an encrypted connection. You can add your own secure cookie attribute to your response.

  • Java
java
server.response(cookie("loggedIn", "true", secure()), status(302));
  • JSON
json
{
  "request" :
    {
      "uri" : "/cookie"
    },
  "response" :
    {
      "cookies" :
      {
        "login" : {
            "value" : "true",
            "secure" : "true"
        }
      }
    }
}

HTTP Only

@Since 0.11.1

An http only cookie cannot be accessed by client-side APIs. You can add your own httpOnly cookie attribute to your response.

  • Java
java
server.response(cookie("loggedIn", "true", httpOnly()), status(302));
  • JSON
json
{
  "request" :
    {
      "uri" : "/cookie"
    },
  "response" :
    {
      "cookies" :
      {
        "login" : {
            "value" : "true",
            "httpOnly" : "true"
        }
      }
    }
}

Max Age

@Since 0.11.1

The Max-Age attribute can be used to set the cookie's expiration as an interval of seconds in the future, relative to the time the browser received the cookie. You can add your own maxAge cookie attribute to your response.

  • Java
java
server.response(cookie("loggedIn", "true", maxAge(1, TimeUnit.HOURS)), status(302))
  • JSON
json
{
  "request" :
    {
      "uri" : "/cookie"
    },
  "response" :
    {
      "cookies" :
      {
        "login" : {
            "value" : "true",
             "maxAge": {
                 "duration": 1,
                 "unit": "hour"
             }
        }
      }
    }
}

Same Site

@Since 1.5.0

The SameSite attribute can be set to control whether or not a cookie is sent with cross-site requests, providing some protection against cross-site request forgery attacks (CSRF).

Only "Strict", "Lax" and "None" can be set as sameSite value.

  • Java
java
server.response(cookie("loggedIn", "true", sameSite("Lax")), status(302))
  • JSON
json
{
  "request" :
    {
      "uri" : "/cookie"
    },
  "response" :
    {
      "cookies" :
      {
        "login" : {
            "value" : "true",
             "sameSite": "Lax"
        }
      }
    }
}

CORS

@Since 1.5.0

Cross-origin resource sharing (CORS) is a mechanism that allows restricted resources (e.g. fonts) on a web page to be requested from another domain outside the domain from which the resource originated. You can add your own cors to your response.

Default All CORS

You can add default all CORS to your response with cors operator without any arguments.

  • Java API
java
server.response(cors());
  • JSON API
json
{
  "response" :
    {
      "cors" : true
    }
}

CORS with allowOrigin/Access-Control-Allow-Origin

You can allow CORS with specific origin with cors operator with allowOrigin argument, which provides Access-Control-Allow-Origin header.

  • Java API
java
server.response(cors(allowOrigin("https://www.github.com/")));
  • JSON API
json
{
  "response" :
    {
      "cors" :
        {
          "allowOrigin" : "https://www.github.com/"
        }
    }
}

If you allow any origin with * in origin.

  • Java API
java
server.response(cors(origin("*")));
  • JSON
json
{
  "response" :
    {
      "cors" :
        {
          "allowOrigin" : "*"
        }
    }
}

In JSON API, you can also use Access-Control-Allow-Origin directly.

  • JSON
json
{
  "response" :
    {
      "cors" :
        {
          "Access-Control-Allow-Origin" : "https://www.github.com"
        }
    }
}

CORS with allowMethods/Access-Control-Allow-Methods

You can allow CORS with specific methods with cors operator with allowMethods argument, which provides Access-Control-Allow-Methods header.

  • Java API
java
server.response(cors(allowMethods("GET", "PUT")));
server.response(cors(allowMethods(HttpMethod.GET, HttpMethod.PUT)));
  • JSON
json
{
  "response" :
    {
      "cors" :
        {
          "allowMethods" : ["GET", "PUT"],
        }
    }
}

In JSON API, you can also use Access-Control-Allow-Methods directly.

  • JSON
json
{
  "response" :
    {
      "cors" :
        {
          "Access-Control-Allow-Methods" : ["GET", "PUT"]
        }
    }
}

CORS with allowHeaders/Access-Control-Allow-Headers

You can allow CORS with specific headers with cors operator with allowHeaders argument, which provides Access-Control-Allow-Headers header.

  • Java API
java
server.response(cors(allowHeaders("X-Header", "Y-Header")));
  • JSON
json
{
  "response" :
    {
      "cors" :
        {
          "allowHeaders" : ["X-Header", "Y-Header"]
        }
    }
}

In JSON API, you can also use Access-Control-Allow-Methods directly.

json
{
  "response" :
    {
      "cors" :
        {
          "Access-Control-Allow-Headers" : ["X-Header", "Y-Header"]
        }
    }
}

CORS with maxAge/Access-Control-Max-Age

You can allow CORS with specific max age with cors operator with maxAge argument, which provides Access-Control-Max-Age header.

  • Java API
java
server.response(cors(maxAge(1728000, TimeUnit.SECONDS)));
  • JSON
json
{
  "response" :
    {
      "cors" :
        {
          "maxAge": {
            "duration": 1728000,
            "unit": "second"
          }
        }
    }
}

In JSON API, you can also use Access-Control-Max-Age directly.

json
{
  "response" :
    {
      "cors" :
        {
          "Access-Control-Max-Age" : {
            "duration": 1728000,
            "unit": "second"
          }
        }
    }
}

CORS with allowCredentials/Access-Control-Allow-Credentials

You can allow CORS with specific credentials with cors operator with allowCredentials argument, which provides Access-Control-Allow-Credentials header.

  • Java API
java
server.response(cors(allowCredentials(true)));
  • JSON
json
{
  "response" :
    {
      "cors" :
        {
          "allowCredentials" : true
        }
    }
}

In JSON API, you can also use Access-Control-Allow-Credentials directly.

json
{
  "response" :
    {
      "cors" :
        {
          "Access-Control-Allow-Credentials" : true
        }
    }
}

CORS with exposeHeaders/Access-Control-Expose-Headers

You can allow CORS with specific expose headers with cors operator with exposeHeaders argument, which provides Access-Control-Expose-Headers header.

  • Java API
java
server.response(cors(exposeHeaders("X-Header", "Y-Header")));
  • JSON
json
{
  "response" :
    {
      "cors" :
        {
          "exposeHeaders" : ["X-Header", "Y-Header"]
        }
    }
}

In JSON API, you can also use Access-Control-Expose-Headers directly.

json
{
  "response" :
    {
      "cors" :
        {
          "Access-Control-Expose-Headers" : ["X-Header", "Y-Header"]
        }
    }
}

Attachment

@Since 0.10.0

Attachment is often used in web development. You can setup an attachment in Moco as following. As you will see, you'd better set a filename for client to receive.

  • Java API
java
server.get(by(uri("/"))).response(attachment("foo.txt", file("foo.response")));
  • JSON
json
{
  "request": {
    "uri": "/file_attachment"
  },
  "response": {
    "attachment": {
        "filename": "foo.txt",
        "file": "foo.response"
    }
  }
}

Latency

@Since 0.7

Sometimes, we need a latency to simulate slow server side operation.

@Since 0.10.1

It's easy to setup latency with time unit.

  • Java API
java
server.response(latency(1, TimeUnit.SECONDS));
  • JSON
json
{
  "request" :
    {
      "text" : "foo"
    },
  "response" :
    {
      "latency":
        {
          "duration": 1,
          "unit": "second"
        }
    }
}

The original API without time unit introduced in 0.7 has been deprecated.

Sequence

@Since 0.7

Sometimes, we want to simulate a real-world operation which change server side resource. For example:

  • First time you request a resource and "foo" is returned
  • We update this resource
  • Again request the same URL, updated content, e.g. "bar" is expected.

We can do that by

java
server.request(by(uri("/seq"))).response(seq("foo", "bar", "blah"));

The other response settings are able to be set as well.

java
server.request(by(uri("/seq"))).response(seq(status(302), status(302), status(200)));

@Since 0.12.0

json
{
    "request" : {
      "uri" : "/seq"
    },
    "response": {
      "seq": [
          {
            "text" : "foo"
          },
          {
            "text" : "bar"
          },
          {
            "text" : "blah"
          }
      ]
    }
}

The other response settings are able to be set for json as well.

json
{
    "request" : {
      "uri" : "/seq"
    },
    "response": {
      "seq": [
          {
            "status" : "302"
          },
          {
            "status" : "302"
          },
          {
            "status" : "200"
          }
      ]
    }
}

Cycle

@Since 1.0.0

Cycle is similar to seq, but it will return response as cycle. An example is as following:

java
server.request(by(uri("/cycle"))).response(cycle("foo", "bar", "blah"));

The response will returned as cycle:

  • foo
  • bar
  • blah
  • foo
  • bar
  • blah
  • ...

The other response settings are able to be set as well.

java
server.request(by(uri("/cycle"))).response(cycle(status(302), status(302), status(200)));

@Since 0.12.0

json
{
    "request" : {
      "uri" : "/cycle"
    },
    "response": {
      "cycle": [
          {
            "text" : "foo"
          },
          {
            "text" : "bar"
          },
          {
            "text" : "blah"
          }
      ]
    }
}

The other response settings are able to be set for json as well.

json
{
    "request" : {
      "uri" : "/cycle"
    },
    "response": {
      "cycle": [
          {
            "status" : "302"
          },
          {
            "status" : "302"
          },
          {
            "status" : "200"
          }
      ]
    }
}

JSON Response

If the response is JSON, we don't need to write JSON text with escape character in code.

@Since 0.10.2

You can give a POJO to Java API, it will be converted JSON text. Hint, this api will setup Content-Type header as well.

java
server.request(by(uri("/json"))).response(toJson(pojo));

@Since 0.12.0
toJson will be removed from 0.12.0, use json instead.

java
server.request(by(uri("/json"))).response(json(pojo));

Note that this functionality is implemented in Jackson, please make sure your POJO is written in Jackson acceptable format.

@Since 0.9.2

For JSON API, just give json object directly

json
{
    "request": 
      {
        "uri": "/json"
      },
    "response": 
      {
        "json": 
          {
            "foo" : "bar"
          }
      }
}

@Since 1.2.0

For API user, if you want to return create dynamic JSON based on the request, you can use lambda to do this.

  • With request
java
server.request(by(uri("/json"))).response(json((request) -> new Pojo()));

Mount

@Since 0.7

Moco allows us to mount a directory to uri.

  • Java API
java
server.mount(dir, to("/uri"));
  • JSON
json
{
  "mount" :
    {
      "dir" : "dir",
      "uri" : "/uri"
    }
}

Glob is acceptable to filter specified files, e.g we can include by

  • Java API
java
server.mount(dir, to("/uri"), include("*.txt"));
  • JSON
json
{
  "mount" :
    {
      "dir" : "dir",
      "uri" : "/uri",
      "includes" :
        [
          "*.txt"
        ]
    }
}

or exclude by

  • Java API
java
server.mount(dir, to("/uri"), exclude("*.txt"));
  • JSON
json
{
  "mount" :
    {
      "dir" : "dir",
      "uri" : "/uri",
      "excludes" :
        [
          "*.txt"
        ]
    }
}

even compose them by

  • Java API
java
server.mount(dir, to("/uri"), include("a.txt"), exclude("b.txt"), include("c.txt"));
  • JSON
json
{
  "mount" :
    {
      "dir" : "dir",
      "uri" : "/uri",
      "includes" :
        [
          "a.txt",
          "c.txt"
        ],
      "excludes" :
        [
          "b.txt"
        ]
    }
}

@Since 0.10.1
You can also specify some response information like normal response, e.g.

  • JSON
json
{
  "mount" :
    {
      "dir" : "dir",
      "uri" : "/uri",
      "headers" : {
        "Content-Type" : "text/plain"
      }
    }
}

Template

Sometimes, we need to customize our response based on something, e.g. response should have same header with request.

The goal can be reached by template:

Request

You can get request information with req in template.

Version

@Since 0.8

With req.version, request version can be retrieved in template.

The following example will return response version as content.

  • Java
java
server.request(by(uri("/template"))).response(template("${req.version}"));
  • JSON
json
{
    "request": {
        "uri": "/template"
    },
    "response": {
        "text": {
            "template": "${req.version}"
        }
    }
}

Method

@Since 0.8

Request method is identified by req.method.

  • Java
java
server.request(by(uri("/template"))).response(template("${req.method}"));
  • JSON
json
{
    "request": {
        "uri": "/template"
    },
    "response": {
        "text": {
            "template": "${req.method}"
        }
    }
}

Content

@Since 0.8

All request content can be used in template with req.content

  • Java
java
server.request(by(uri("/template"))).response(template("${req.content}"));
  • JSON
json
{
    "request": {
        "uri": "/template"
    },
    "response": {
        "text": {
            "template": "${req.content}"
        }
    }
}

@Since 0.8

Header is another important element in template and we can use req.headers for headers.

  • Java
java
server.request(by(uri("/template"))).response(template("${req.headers['foo']}"));
  • JSON
json
{
    "request": {
        "uri": "/template"
    },
    "response": {
        "text": {
            "template": "${req.headers['foo']}"
        }
    }
}

Query

@Since 0.8

req.queries helps us to extract request query.

  • Java
java
server.request(by(uri("/template"))).response(template("${req.queries['foo']}"));
  • JSON
json
{
    "request": {
        "uri": "/template"
    },
    "response": {
        "text": {
            "template": "${req.queries['foo']}"
        }
    }
}

Form

@Since 0.9.1

req.forms can extract form value from request.

  • Java
java
server.request(by(uri("/template"))).response(template("${req.forms['foo']}"));
  • JSON
json
{
    "request": {
        "uri": "/template"
    },
    "response": {
        "text": {
            "template": "${req.forms['foo']}"
        }
    }
}

@Since 0.9.1

Cookie from request can extracted by req.cookies.

  • Java
java
server.request(by(uri("/template"))).response(template("${req.cookies['foo']}"));
  • JSON
json
{
    "request": {
        "uri": "/template"
    },
    "response": {
        "text": {
            "template": "${req.cookies['foo']}"
        }
    }
}

JSON

@Since 1.0.0

If your request is a JSON request, you can use req.json to visit your json object.

Note that make sure your request is a JSON request, otherwise an exception will be thrown.

  • Java
java
server.request(by(uri("/template"))).response(template("${req.json.foo}"));
  • JSON
json
{
    "request": {
        "uri": "/template"
    },
    "response": {
        "text": {
            "template": "${req.json.foo}"
        }
    }
}

XML

@Since 1.4.0

If your request is an XML request, you can use req.xml to visit your xml object.

Note that make sure your request is an XML request, otherwise an exception will be thrown.

  • Java
java
server.request(by(uri("/template"))).response(template("${req.xml.foo}"));
  • JSON
json
{
    "request": {
        "uri": "/template"
    },
    "response": {
        "text": {
            "template": "${req.xml.foo}"
        }
    }
}

Client

Address

@Since 1.4.0

req.client.address can be used in template to return client IP address.

java
server.request(by(uri("/template"))).response(template("${req.client.address}"));
json
{
    "request": {
        "uri": "/template"
    },
    "response": {
        "text": {
            "template": "${req.client.address}"
        }
    }
}

Port

@Since 1.5.0

req.client.port can be used in template to return client port.

java
server.request(by(uri("/template"))).response(template("${req.client.port}"));
json
{
    "request": {
        "uri": "/template"
    },
    "response": {
        "text": {
            "template": "${req.client.port}"
        }
    }
}

Path

@Since 1.4.0

req.path can work with path API to extract path parameter as template variable.

java
server.request(path(uri("/path/{foo}"))).response(template("${req.path.foo}"));
json
{
  "request": {
    "uri": {
      "path": "/path/{foo}"
    }
  },
  "response": {
    "text": {
      "template": "${req.path.foo}"
    }
  }
}

Custom Variable

@Since 0.9.1

You can provide your own variables in your template.

  • Java
java
server.request(by(uri("/template"))).response(template("${foo}", "foo", "bar"));
  • JSON
json
{
    "request": {
        "uri": "/template"
    },
    "response": {
        "text": {
            "template": {
                "with" : "${foo}",
                "vars" : {
                    "foo" : "bar"
                }
            }
        }
    }
}

@Since 0.10.0

You can also use extractor to extract information from request.

  • Java
java
server.request(by(uri("/template"))).response(template("${foo}", "foo", jsonPath("$.book[*].price")));
  • JSON
json
{
    "request": {
        "uri": "/template"
    },
    "response": {
        "text": {
            "template": {
                "with" : "${foo}",
                "vars" : {
                    "foo" : {
                      "json_path": "$.book[*].price"
                    }
                }
            }
        }
    }
}

Other extractors, e.g. xpath also work here.

Template Function

now

@Since 1.0.0

Current time can retrieved by 'now' function and a date format string should be passed as argument.

  • Java
java
server.request(by(uri("/template"))).response(template("${now('yyyy-MM-dd')}"));
  • JSON
json
{
    "request": {
        "uri": "/template"
    },
    "response": {
        "text": {
            "template": "${now(\"yyyy-MM-dd\")}"
        }
    }
}

random

@Since 1.0.0

random will generate a random number. If you didn't pass any argument, the generated random will be between 0 and 1.

  • Java
java
server.request(by(uri("/template"))).response(template("${random()}"));
  • JSON
json
{
    "request": {
        "uri": "/template"
    },
    "response": {
        "text": {
            "template": "${random()}"
        }
    }
}

The first argument is random number range which means the generated number will be between 0 and range.

  • Java
java
server.request(by(uri("/template"))).response(template("${random(100)}"));
  • JSON
json
{
    "request": {
        "uri": "/template"
    },
    "response": {
        "text": {
            "template": "${random(100)}"
        }
    }
}

@Since 1.3.0

If you want to limit your random number in a range. You can give two number as a start and an end.

  • Java
java
server.request(by(uri("/template"))).response(template("${random(99, 100)}"));
  • JSON
json
{
    "request": {
        "uri": "/template"
    },
    "response": {
        "text": {
            "template": "${random(99, 100)}"
        }
    }
}

The last argument could be a number format.

  • Java
java
server.request(by(uri("/template"))).response(template("${random(99, 100, '###.###')}"));
  • JSON
json
{
    "request": {
        "uri": "/template"
    },
    "response": {
        "text": {
            "template": "${random(99, 100, \"###.###\")}"
        }
    }
}

You can also use number format directly without range. By default, range is from 0 to 1.

  • Java
java
server.request(by(uri("/template"))).response(template("${random('###.###')}"));
  • JSON
json
{
    "request": {
        "uri": "/template"
    },
    "response": {
        "text": {
            "template": "${random(\"###.###\")}"
        }
    }
}

Redirect

@Since 0.10.2

Redirect can also be set as template.

  • Java
java
server.request(by(uri("/redirectTemplate"))).redirectTo(template("${var}", "var", ""https://github.com"));
  • Json
json
{
      "request" :
      {
          "uri" : "/redirect-with-template"
      },

      "redirectTo" : {
          "template" : {
              "with" : "${url}",
              "vars" : {
                  "url" : "https://github.com"
              }
          }
      }
  }

File Name Template

@Since 0.10.1

Template can also be used in file name, thus response can be different based on different request.

  • Java
java
server.response(file(template("${req.headers['foo'].txt")));
  • JSON
json
[
  {
    "response": {
      "file": {
        "name": {
          "template": "${req.headers['foo'].txt"}"
        }
      }
    }
  }
]

Proxy

@Since 0.11.1

You can use template in proxy API, so that you can dynamically decide which URL you will forward the request to.

  • Java
java
server.request(by(uri("/proxy"))).response(proxy(template("http://localhost:12306/${req.queries['foo']}")))
  • JSON
json
{
    "request" : {
        "uri" : "/proxy"
    },
    "response" : {
        "proxy" : {
            "url" : {
                "template": "http://localhost:12306/${req.queries['foo']}"
            }
        }
    }
}

Template for Event Action

Template also can ben applied to event action. Check out Event for more details about event.

  • Java
java
server.request(by(uri("/event"))).response("event").on(complete(post("http://localhost:12306/target"), template("${target}", of("target", var("target"))))));
  • JSON
json
{
  "request": {
    "uri": "/event"
  },
  "response": {
    "text": "event"
  },
  "on": {
    "complete": {
      "post": {
        "url": "http://localhost:12306/target",
        "content": {
          "template": {
            "with": "${target}",
            "vars": {
              "target" : "target"
            }
          }
        }
      }
    }
  }
}

Record and Replay

@Since 1.1.0

More powerful dynamic features are required even if you can implement some with a template. For instance, you may want to change one URL to return a different response. Record and replay will help.

In the following, /record will be used to record request and /replay will return the recorded request content. You can also configure record and replay for more capabilities.

In this case, group will be used to distinguish different record sources.

java
server.request(by(uri("/record"))).response(record(group("foo")));
server.request(by(uri("/replay"))).response(replay(group("foo")));

Group

group help you distinguish different record source, which means you can have same configuration in different group. If no group is provided, default group will be applied.

  • Java
java
server.request(by(uri("/record"))).response(record(group("foo")));
server.request(by(uri("/replay"))).response(replay(group("foo")));
  • JSON

The default parameter for record and replay is group.

json
[
  {
    "request" : {
      "uri" : "/record"
    },
    "response" : {
      "record" : "foo"
    }
  },
  {
    "request" : {
      "uri" : "/replay"
    },
    "response" : {
      "record" : "foo"
    }
  }
]

You can also specify group name explicitly.

json
[
  {
    "request" : {
      "uri" : "/record"
    },
    "response" : {
      "record" : {
        "group": "foo"
      }
    }
  },
  {
    "request" : {
      "uri" : "/replay"
    },
    "response" : {
      "record" : {
        "group": "foo"
      }
    }
  }
]

Identifier

In the same group, you can use identifier to distinguish different requst. You can extract identifier from request with template syntax.

  • Java
java
server.request(by(uri("/record"))).response(record(identifier("${req.queries['type']}")));
server.request(by(uri("/replay"))).response(replay(identifier("${req.queries['type']}")));
  • JSON
json
[
  {
    "request" : {
      "uri" : "/record"
    },
    "response" : {
      "record" : {
        "identifier": {
          "template": "${req.queries['type']}"
        }
      }
    }
  },
  {
    "request" : {
      "uri" : "/replay"
    },
    "response" : {
      "record" : {
        "identifier": {
          "template": "${req.queries['type']}"
        }
      }
    }
  }
]

In the above case, if you record your request with

  • URI /record?type=foo and content is foo
  • URI /record?type=bar and content is bar,

When you access URI /replay?type=foo, foo will be returned and access URI /replay?type=bar, bar will be returned.

Modifier

The recorded content will be returned by default. But sometimes you hope return different content.

In the following case, with modifier, type in request parameter will be returned as replay content. As you expect, modifier only apply in replay. Template syntax will apply here.

  • Java
java
server.request(by(uri("/record"))).response(record(group("foo")));
server.request(by(uri("/replay"))).response(replay(group("foo"),    
                                                   modifier("${req.queries['type']}")));
  • JSON
json
[
  {
    "request" : {
      "uri" : "/record"
    },
    "response" : {
      "record" : {
        "group": "foo",
      }
    }
  },
  {
    "request" : {
      "uri" : "/replay"
    },
    "response" : {
      "record" : {
        "group": "foo",
        "modifier": "${req.queries['type']}"
      }
    }
  }
]

By default, only response content will be set with modifier. If you want more, you can setup the response with other response configuration.

  • Java
java
server.request(by(uri("/record"))).response(record(group("foo")));
server.request(by(uri("/replay"))).response(replay(group("foo"),    
    modifier(template("${req.content}"),
             header("X-REPLAY", template("${req.queries['type']}"))
    )
));
  • JSON
json
[
  {
    "request" : {
      "uri" : "/record"
    },
    "response" : {
      "record" : {
        "group": "foo",
      }
    }
  },
  {
    "request" : {
      "uri" : "/replay"
    },
    "response" : {
      "record" : {
        "group": "foo",
        "modifier": {
          "text": {
            "template": "${req.content}"
          },
          "header": {
            "X-REPLAY": {
              "template": "${req.queries['type']}"
            }
          }
        }
      }
    }
  }
]

Tape

If you want to reuse recorded request after restart, tape will help you persist the recorded request just like a tape.

  • Java
java
server.request(by(uri("/record"))).response(record(group("foo"), tape("/path/to/tape")));
server.request(by(uri("/foo-replay"))).response(replay(record(group("foo"), 
                                                              tape("/path/to/tape")));
  • JSON
json
[
  {
    "request" : {
      "uri" : "/record"
    },
    "response" : {
      "record" : {
        "group": "foo",
        "tape": "/path/to/tape"
      }
    }
  },
  {
    "request" : {
      "uri" : "/replay"
    },
    "response" : {
      "record" : {
        "group": "foo",
        "tape": "/path/to/tape"
      }
    }
  }
]

Event

You may need to request another site when you receive a request, e.g. OAuth. Event could be your helper at that time.

Complete

@Since 0.9

Complete event will be fired after your request has been handled completely.

Get Request

You can get from an URL.

  • Java
java
server.request(by(uri("/event"))).response("event").on(complete(get("http://another_site")));
  • JSON
json
{
    "request": {
        "uri" : "/event"
    },
    "response": {
        "text": "event"
    },
    "on": {
        "complete": {
            "get" : {
                "url" : "http://another_site"
            }
        }
    }
}

@Since 1.0.0

And also get with headers.

  • Java
java
server.request(by(uri("/event"))).response("event").on(complete(get("http://another_site", header("foo", "bar"))));
  • JSON
json
{
    "request": {
        "uri" : "/event"
    },
    "response": {
        "text": "event"
    },
    "on": {
        "complete": {
            "get" : {
                "url" : "http://another_site",
                "headers": {
                    "foo": "bar"
                }
            }
        }
    }
}

Post Request

You can post some content as well.

  • Java
java
server.request(by(uri("/event"))).response("event").on(complete(post("http://another_site", "content")));
  • JSON
json
{
    "request": {
        "uri" : "/event"
    },
    "response": {
        "text": "event"
    },
    "on": {
        "complete": {
            "post" : {
                "url" : "http://another_site",
                "content": "content"
            }
        }
    }
}

@Since 0.12.0

If your post content is JSON, you can use json in your configuration directly.

json
{
    "request": {
        "uri" : "/event"
    },
    "response": {
        "text": "event"
    },
    "on": {
        "complete": {
            "post" : {
                "url" : "http://another_site",
                "json": {
                    "foo" : "bar"
                }
            }
        }
    }
}

@Since 1.0.0

And also post with headers.

  • Java
java
server.request(by(uri("/event"))).response("event").on(complete(post("http://another_site", "content", header("foo", "bar"))));
  • JSON
json
{
    "request": {
        "uri" : "/event"
    },
    "response": {
        "text": "event"
    },
    "on": {
        "complete": {
            "post" : {
                "url" : "http://another_site",
                "content": "content",
                "headers": {
                    "foo": "bar"
                }
            }
        }
    }
}

Let me know if you need more methods.

Asynchronous

@Since 0.9

Synchronized request is used by default, which means response won't be returned to client until event handler is finished.

If it is not your expected behavior, you can changed with async API which will fire event asynchronously.

  • Java
java
server.request(by(uri("/event"))).response("event").on(complete(async(post("http://another_site", "content"))));
  • JSON
json
{
    "request": {
        "uri" : "/event"
    },
    "response": {
        "text": "event"
    },
    "on": {
        "complete": {
            "async" : "true",
            "post" : {
                "url" : "http://another_site",
                "content": "content"
            }
        }
    }
}

You can specify latency for this asynchronous request as well to wait a while.

  • Java
java
server.request(by(uri("/event"))).response("event").on(complete(async(post("http://another_site", "content"), latency(1000))));
  • JSON
json
{
    "request": {
        "uri" : "/event"
    },
    "response": {
        "text": "event"
    },
    "on": {
        "complete": {
            "async" : "true",
            "latency" : 1000,
            "post" : {
                "url" : "http://another_site",
                "content": "content"
            }
        }
    }
}

Verify

@Since 0.9

Someone may want to verify what kind of request has been sent to server in testing framework.

You can verify request like this:

java
RequestHit hit = requestHit();
final HttpServer server = httpServer(12306, hit);
server.get(by(uri("/foo"))).response("bar");

running(server, () -> {
    assertThat(helper.get(remoteUrl("/foo")), is("bar"));
});

hit.verify(by(uri("/foo"))), times(1));

You can also verify unexpected request like this:

java
hit.verify(unexpected(), never());

Many verification can be used:

  • never: none of this kind of request has been sent.
  • once: only once this kind of request has been sent.
  • time: how many times this kind of request has been sent.
  • atLeast: at least how many time this kind of request has been sent.
  • atMost: at most how many time this kind of request has been sent.
  • between: the times this kind of request has been sent should be between min and max times.

Miscellaneous

Port

@Since 0.9

If you specify a port for your stub server, it means the port must be available when you start server. This is not case sometimes.

Moco provides you another way to start your server: specify no port, and it will look up an available port. The port can be got by port() method. The example is as follow:

java
final HttpServer server = httpServer();
server.response("foo");

running(server, () -> {
    Content content = Request.Get("http://localhost:" + server.port()).execute().returnContent();
    assertThat(content.asString(), is("foo"));
});

The port will be returned only when server is started, otherwise the exception will be thrown.

For standalone server, if you need this behaviour, simply don't give port argument.

shell
java -jar moco-runner-<version>-standalone.jar http -c foo.json

The port information will shown on screen.

Log

@Since 0.9.1

If you want to know more about how your Moco server running, log will be your helper.

java
final HttpServer server = httpServer(log());

The Moco server will log all your requests and responses in your console.

It you want to keep log, you can use log interface as following:

java
final HttpServer server = httpServer(log("path/to/log.log"));

@Since 0.10.1

Log content may contain some non UTF-8 character, charset could be specified in log API:

java
final HttpServer server = httpServer(log("path/to/log.log", Charset.forName("GBK")));

The log will be saved in your log file.

Log with verifier

Log will help you for some legacy system to know what detailed request/response looks like. You also need to do some verification work. Here is the case.

java
RequestHit hit = requestHit();
final HttpServer server = httpServer(port(), hit, log());

File: moco-doc/cmd.md

Standalone Command Line

Moco can be used as standalone to run with configuration and you can download standalone directly:
Standalone Moco Runner

First of all, a JSON configuration file needs to be provided to start Moco.

json
[
  {
    "response" :
      {
        "text" : "foo"
      }
  }
]

(foo.json)

Table of Contents

Server Type

A server type is a must for Moco, you need specify your server type as first argument. Different server type will support different arguments.

HTTP Server

You can start a HTTP server by the following command:

shell
java -jar moco-runner-<version>-standalone.jar http -p 12306 -c foo.json

HTTPS Server

A HTTPS server can be started by the following command:

shell
java -jar moco-runner-<version>-standalone.jar https -p 12306 -c foo.json --https /path/to/cert.jks --cert mocohttps --keystore mocohttps

Configuration Files

One Configuration File

You can specify your configuration with -c.

shell
java -jar moco-runner-<version>-standalone.jar http -p 12306 -c foo.json

Many Configuration Files

@Since 0.12.0

If you have many configuration files, you can use glob matcher as -c argument.
But you need to make sure the configuration that don't conflict.

shell
java -jar moco-runner-<version>-standalone.jar http -p 12306 -c "*.json"

Note: the quotation mark is required otherwise *.json will be parsed by your shell.

Port

Specific Port

You can specify server port with -p

shell
java -jar moco-runner-<version>-standalone.jar http -p 12306 -c foo.json

Without Port

If you don't need any specified port, you run run Moco without port.

shell
java -jar moco-runner-<version>-standalone.jar http -c foo.json

An available port will be picked up by Moco and you can see the port in console.

shell
20 Oct 2015 22:10:18 [main] INFO  Server is started at 58593
20 Oct 2015 22:10:18 [main] INFO  Shutdown port is 58594

Quiet Mode

If you don't want see too many logs, you could make the server quiet with -q option.

shell
java -jar moco-runner-<version>-standalone.jar http -p 12306 -c foo.json -q

Max Content Length

Max content length for a single request is set by default, but it is not enough for some case. You can set your own content length with content-length arguments.

shell
java -jar moco-runner-<version>-standalone.jar http -p 12306 -c foo.json --content-length 2097152

Version

You can query Moco version by the following command:

shell
java -jar moco-runner-<version>-standalone.jar version

Global Settings

You can run Moco instance with global settings.

shell
java -jar moco-runner-<version>-standalone.jar http -p 12306 -g settings.json

Environment

Environment is a good feature, which allows you start your server with different environment from CLI.

shell
java -jar moco-runner-<version>-standalone.jar http -p 12306 -g env.json -e remote

Shutdown

Moco instance can ben shutdown by shutdown command and its shutdown port. A shutdown port can be specified when start Moco instance.

shell
java -jar moco-runner-<version>-standalone.jar http -p 12306 -c foo.json -s 9527

Or leave Moco to choose by default, the shutdown port will be shown on console.

Then you can use the shutdown port to shutdown the running Moco instance.

shell
java -jar moco-runner-<version>-standalone.jar shutdown -s 9527

File: moco-doc/configurations.md

moved to APIs


File: moco-doc/extending.md

Extending Moco

You can extend Moco, if you need more features than Moco already provides.

The basic idea is very simple: RequestMatcher and ResponseHandler. If your request matches any matcher, the corresponding response handler will be invoked to return response.

Request Matcher

@Since 1.3.0

If you want to implement your own matcher, you can write with conditional API which is supported in Java code.

java
server.request(conditional(request -> request.getContent().toString().equals("foo"))).response("foo");

Response Handler

If you want to write response based on the request, simply write a ResponseHandler.

java
public interface ResponseHandler extends ConfigApplier<ResponseHandler> {
    void writeToResponse(SessionContext context);
}

You just want to rewrite your own HTTP content in most cases, you can extend AbstractHttpContentResponseHandler.

java
import com.github.dreamhead.moco.util.MediaType;

public class AbstractContentResponseHandler extends AbstractHttpContentResponseHandler {
    protected MessageContent responseContent(HttpRequest httpRequest) {
        return MessageContent.content("hello Moco"); 
    }
    
    protected abstract MediaType getContentType(HttpRequest request) {
        return MediaType.PLAIN_TEXT_UTF_8;
    }
}

Since 2.0.0 MediaType is com.github.dreamhead.moco.util.MediaType. Before 2.0.0 it was
Guava's com.google.common.net.MediaType.


File: moco-doc/global-settings.md

Global Settings

We could put all configurations in one single configuration files. But if we want stub many services in a single Moco instance, the configurations file would be huge.

In this case, we can use settings file to separate our configurations for different into different configuration files.

It's example time. We have two services to stub:

json
[
    {
        "request" : {
            "uri" : "/foo"
        },
        "response" : {
            "text" : "foo"
        }
    }
]

and

json
[
    {
        "request" : {
            "uri" : "/bar"
        },
        "response" : {
            "text" : "bar"
        }
    }
]

Now, we can write a setting file to combine these two configurations:

json
[
    {
        "include" : "foo.json"
    },
    {
        "include" : "bar.json"
    }
]

It's time start server with this setting:

shell
java -jar moco-runner-<version>-standalone.jar http -p 12306 -g settings.json

Feel free to open your browser to try.

Table of Contents

Configuration

In fact, there are some configurations for settings to simplify your configuration.

Context

@Since 0.8

We can put all responses for one service in a specified context:

json
[
    {
        "context": "/foo",
        "include": "foo.json"
    },
    {
        "context": "/bar",
        "include": "bar.json"
    }
]

Now all configurations in foo.json must be accessed by /foo context. In this case, when you access http://localhost:12306/foo/foo, you will get "foo".

On the other hand, bar.json will in /bar context which means http://localhost:12306/bar/bar will return "bar".

File Root

@Since 0.8

If you have many file APIs in your configuration, file root setting will help you to shorten configurations.
As the name suggests, file root setting will play as the file root for configuration. So all your file APIs could be used as the relative path.

json
[
    {
        "file_root": "fileroot",
        "include": "fileroot.json"
    }
]

Now, both include setting and file APIs use relative path. In this case, fileroot/fileroot.json will be used as included file.

json
[
    {
        "request" : {
            "uri" : "/fileroot"
        },
        "response" : {
            "file" : "foo.response"
        }
    }
]

(fileroot/fileroot.json)

When the request is launched, src/test/resources/foo.response will be returned.

Environment

@Since 0.8

For some different cases, you have different configuration. For example, your code may access proxy for remote server and mock server for local testing.

Now, environment will help you to configure all related configurations in one settings.

json
[
    {
        "env" : "remote",
        "include": "foo.json"
    },
    {
        "env" : "local",
        "include": "bar.json"
    }
]

(env.json)

You can start your server with different environment from CLI.

shell
java -jar moco-runner-<version>-standalone.jar http -p 12306 -g env.json -e remote

Now, when you access your server, all configurations with "remote" environment rocks!

In this case, http://localhost:12306/foo will give you "foo", but http://localhost:12306/bar will return nothing.

Request

@Since 0.10.1

You may need a global request matcher in some cases, for example, token for some REST APIs, which actually is request parameter. Global request will help you.

json
[
  {
    "request" : {
      "headers" : {
        "foo" : "bar"
      }
    },
    "include": "blah.json"
  }
]

In this case, you won't receive response until your request has "foo", "bar" header.

Response

@Since 0.9.1

In some cases, you may want to setup a global response for all response, for example, HTTP version, or HTTP header, so you don't have to setup it for every single response.

json
[
    {
        "response" : {
            "headers" : {
                "foo" : "bar"
            }
        },
        "include": "blah.json"
    }
]

When you issue any request to server, it will return response with "foo", "bar" header.

Glob Inclusion

@Since 0.12.0

You may need include many files, glob can help you to do this.

json
[
    {
        "include" : "*.json"
    }
]

File: moco-doc/junit5.md

JUnit 5 Integration

@Since will be at next release

Moco makes use of Extension in JUnit 5 to simplify JUnit integration.

@MocoExtension is required to use Moco in JUnit 5. Every test class use Moco should be annotated with @ExtendWith(MocoExtension.class).

java
@ExtendWith(MocoExtension.class)
class WithMocoTest {
  ...
}

With @MocoExtesion, Moco server will be started before test and stopped after test.

HTTP Server

@MocoHttpServer could be used to start a HTTP server. Configuration file and port are required.

java
@ExtendWith(MocoExtension.class)
@MocoHttpServer(filepath = "foo/foo.json", port=12306)
public class MocoHttpTest {
  @Test
  public void should_return_expected_message() throws IOException {
    Content content = Request.Get("http://localhost:12306").execute().returnContent();
    assertThat(content.asString(), is("foo"));
  }  
}

filepath is provided here, you can also use classpath to retrieve configuration file from classpath.

java
@ExtendWith(MocoExtension.class)
@MocoHttpServer(classpath = "foo.json", port=12306)
public class MocoHttpTest {
  ...
}

HTTPS Server

Besides @MocoHttpServer, you should also use @MocoCertificate to start Moco as a HTTPS server.

java
@ExtendWith(MocoExtension.class)
@MocoHttpServer(filepath = "foo/foo.json", port=12306)
@MocoCertificate(filepath = "certificate/cert.jks", keyStorePassword = "mocohttps", certPassword = "mocohttps")
public class MocoHttpTest {
  @Test
  public void should_return_expected_message() throws IOException {
    Content content = Request.Get("http://localhost:12306").execute().returnContent();
    assertThat(content.asString(), is("foo"));
  }  
}

Certificate file, keyStorePassword and certPassword can be configured with @MocoCertificate.

You can also configure certificate file with classpath.

java
@ExtendWith(MocoExtension.class)
@MocoHttpServer(classpath = "foo.json", port=12306)
@MocoCertificate(classpath = "certificate/cert.jks", keyStorePassword = "mocohttps", certPassword = "mocohttps")
public class MocoHttpTest {
  @Test
  public void should_return_expected_message() throws IOException {
    ...
  }  
}

File: moco-doc/plan.md

Functionality

APIs

Matcher

  • Marshal support

Handler

  • Template function

More

  • Client

Document

  • Cookbook

Tools Support

  • IntelliJ IDEA plugin
  • Eclipse plugin
  • Runtime configuration change

Polyglot

  • Groovy
  • Scala
  • Clojure
  • Node.js

File: moco-doc/ReleaseNotes.md

Release 2.0.0(unreleased)

Breaking changes

  • A BREAK CHANGE: remove socket api
  • A BREAK CHANGE: remove JUnit 4 support (moco-junit module). Migrate to moco-junit5.
  • Guava is no longer a dependency. Moco's public API no longer exposes any Guava type, which means
    several signatures changed. Recompilation is required even where the source still compiles,
    because the erased return types changed.
    • ImmutableMap and ImmutableList in the API are now java.util.Map and java.util.List. This
      affects HttpMessage.getHeaders, HttpRequest.getQueries, DefaultHttpRequest.getForms,
      getCookies, Moco.template(..., variables), Jsons.toObjects, Globs.glob,
      Configs.configItems, SettingFetcher.getSettings and the RunnerSetting/parser types.
      Code that stored a result in an ImmutableMap/ImmutableList variable, or that implements
      HttpMessage, HttpRequest, Content or SettingFetcher, must be updated.
    • MediaType moved from com.google.common.net.MediaType to
      com.github.dreamhead.moco.util.MediaType. It keeps create, parse, type, subtype,
      charset, withCharset and the constants Moco used, and renders identically on the wire.
      charset() now returns java.util.Optional, so a trailing .toJavaUtil() should be dropped.
    • FileContentType.DEFAULT_CONTENT_TYPE_WITH_CHARSET changes type accordingly.
    • CookiesRequestExtractor and FormsRequestExtractor now extend
      HttpRequestExtractor<Map<String, String>>.
  • MocoMonitor no longer carries Guava's @Subscribe on its methods. Moco has had no EventBus
    since 2013, so this changes nothing inside Moco. It does matter if you registered a
    MocoMonitor on your own Guava EventBus, because Guava resolves @Subscribe through
    interfaces: those methods will now silently never be registered, with no compile error.
    Annotate your own overrides with @Subscribe instead.

APIs

  • HTTP 2 Support

Implementation Details

  • Removed the Guava dependency from all modules. The standalone jar shrinks by roughly 210 KB and
    the uber jar by about 3 MB.
  • org.jspecify:jspecify is now declared explicitly as compileOnly; it previously arrived only
    as a transitive dependency of Guava.
  • A checkNoGuava verification task now fails the build if Guava is reimported.

Release 1.6.1(2-May-2026)

APIs

  • SSE event delay semantics changed: delay affects current event self.

Implementation Details

  • SSE support in proxy failover

Release 1.6.0(20-Apr-2026)

APIs

  • sse API for Server-Sent Events streaming support
    • event for named SSE event
    • data for anonymous SSE event
    • sse(...).delay(ms) for global delay between all events
    • sse(...).delay(duration, TimeUnit) for global delay with time unit
    • Event modifiers: id, retry, delay
    • event.delay(duration, TimeUnit) for event delay with time unit
  • SSE events from file
  • SSE proxy: proxy forwards SSE streams from remote server
  • SSE JSON API
    • Array format for inline events
    • Object format with delay and events
    • Object format with delay as {duration, unit} for time unit
    • File format with delay

Implementation Details

  • Replace logback with tinylog
  • Optimize ProGuard configuration to exclude unused resources

Release 1.5.0(1-Sep-2023)

APIs

  • req.client.port is supported in template
  • sameSite cookie attribute is supported
  • cors in response is supported for CORS
    • allowOrigin(Access-Control-Allow-Credentials)
    • allowCredentials(Access-Control-Allow-Credentials)
    • exposeHeaders(Access-Control-Expose-Headers)
    • maxAge(Access-Control-Max-Age)
    • allowMethods(Access-Control-Allow-Methods)
    • allowHeaders(Access-Control-Allow-Headers)

Release 1.4.0 (1-Dec-2022)

APIs

  • path is introduced for uri
  • req.xml is supported in template
  • req.client.address is supported in template
  • req.path is supported in template

CLI

  • add -q for quiet mode in standalone server

Implementation Details

  • enhance log with query parameters
  • event action log is supported

Release 1.3.0 (1-Jan-2022)

APIs

  • conditional Java API allows you to write your own matcher
  • random template function supports start and end as range
  • struct API for JSON and XML which matches only struct no matter what content is.

Implementation Details

  • restart with configuration when new configuration is illegal. #304
  • enhance log with query parameters
  • xml returns ContentResource which is A BREAK CHANGE.

Release 1.2.0 (1-Apr-2021)

Websocket

  • connected API return content when client connects to websocket server
  • request match request content
  • response return response content
  • join add client to specific group
  • broadcast broadcast message to all subscriber.
    • group broadcast message to specific group

APIs

  • transform Java API allows you transform your content.
  • binary Java API return binary message.
  • json Java API return pojo dynamically.
  • text, binary, json, var with function, Java API allows you write your own response content.

Implementation Details

  • Return application/json header for json API with text and resource.

Release 1.1.0 (1-Mar-2020)

  • Upgrade minimal Java version to Java 8.

APIs

  • record and replay support dynamic stub server.
    • group
    • identifier
    • modifier
    • tape

Release 1.0.0 (1-Sep-2019)

APIs

  • cycle API returns response as a cycle.
  • now as template function is supported in template
  • random as template function is supported in template
  • req.json is supported in template
  • failover and playback status API is supported.
  • header returns HttpHeader instead of ResponseHandler.
  • get and post for event action supports HTTP header.

Implementation Details

  • Support multiple HTTP headers with same name
  • Support proxy PATCH method
  • Dump HTTP message even if Content Length header is missing

Release 0.12.0 (24-Dec-2017)

  • Upgrade minimal Java version to Java 7

APIs

  • json API returns resource, so it can be used for both request and response.
  • and API for Response Handler

JSON APIs

  • seq is supported finally
  • json in Post event

Global Settings

  • Glob syntax is supported for include files in global setting

CLI

  • Glob syntax is supported for CLI '-c' argument

Release 0.11.1 (12-Jun-2017)

APIs

  • Template for Event action
  • Remove publish API with Optional, e.g. file and pathResource
  • Proxy with content resource as url
  • Add cookie attribute API

JUnit

  • HTTPS JUnit runner for POJO, JSON and from path resource
  • Http JSON runner with path resource
  • Socket JSON runner with path resource

Implementation Details

  • Fix: get Moco standalone version from manifest
  • Fix: merge server with port
  • Fix: set charset from http request
  • Fix: parse json with charset
  • Fix: fail to load configuration with multiple modification
  • Fix: allowed empty query
  • Fix: make request hit and moco monitor thread safe
  • Fix: parse form with charset

Release 0.11.0 (20-May-2016)

Usage

  • JUnit Integration

APIs

REST API

  • get
  • post
  • put
  • delete
  • head
  • patch
  • sub-resource
  • any id

Implementation Details

  • Fix: response handler case issue
  • Write JSON content to log
  • Fix: empty content for jsonpath
  • Fix: empty content for xpath

Release 0.10.2 (1-Sep-2015)

APIs

  • toJson will convert POJO to JSON text in response handler
  • json with pojo will convert POJO to JSON text in request matcher
  • socketServer without port
  • socketServer with monitor, e.g. logging and verification
  • Template as redirect target

Implementation Details

  • Enhance log dumper with MediaType implementation
  • Enhance socket server implementation with byte buf, rather than string
  • Fix charset issue in toJson
  • Enhance file content type with MediaType implementation
  • Same name query has been supported

Release 0.10.1 (1-May-2015)

APIs

  • Use httpServer as API, and httpserver has been deprecated, will be removed at next release.
  • File and path resource with charset
  • Latency with time unit, and latency without time unit has been deprecated, will be removed at next release
  • Log file with charset
  • Template as filename for file and path resource

JSON APIs

  • Mount with response setting

Global Settings

  • Request

Implementation Details

  • File and path resource read as byte array to fix file encoding issue
  • JSON path and XPath in template will return many objects.
  • Changed template extractor name to singular in JSON API, e.g. json_paths to json_path, xpaths to xpath.

Release 0.10.0 (1-Dec-2014)

Usage

  • Socket support
  • Version query
  • Start arguments have been changed from start to HTTP(HTTPS, Socket) etc.

APIs

  • Attachment

Runner API

  • server by JSON configuration

Template

  • Template extractor support

Shell

  • Added version argument

Potential Breaks

  • JSON Path, because new version JSON Path implementation has been introduced.

Release 0.9.2 (1-Jul-2014)

Usage

  • (Beta) HTTPS support

APIs

  • Http Protocol Version in Java API.
  • More operators:
    ** startsWith
    ** endsWith
    ** contain
    ** exist
  • between for verifier
  • Added multiple monitors for server creation.

JSON APIs

  • json text shortcut

Template

  • change template variables from Object to String.
    (API users have to decide how to display their vars).
  • JSON request/response shortcut in JSON configuration.

Release 0.9.1 (1-Feb-2014)

Usage

  • Runner API
  • (Beta) gmoco, moco groovy shell

APIs

  • log to record request
  • once for verifier
  • batch proxy
  • proxy playback

Template

  • cookie
  • form
  • template variables

Global Settings

  • Response

Implementation Details

  • Fixed "uri" in failover to remove query parameter.
  • Used Proguard to minimize standalone jar

Fixes

  • Keep connect alive if request wants to keep alive
  • NPE for validating form/content in request hit
  • NPE for XPath matcher
  • Wait server to shutdown completely

Release 0.9 (1-Nov-2013)

APIs

  • Request Hit as verifier
  • Event API
    ** Event: Complete
    ** Event Action: Get/Post URL
  • Turned off log for API by default
  • seq API with response handler
  • Added shortcut for PUT and DELETE method

Implementation Details

  • Server can select available port(start/shutdown) without providing no port.
  • Changed "statusCode" in failover to "status". Please changed your failover file if your tests failed.

Release 0.8.1 (1-August-2013)

  • Upgrade to Netty 4
  • Fixed proxy bug

Release 0.8 (21-July-2013)

Usage

  • global setting file

APIs

Request

  • JSONPath

Response

Integration

  • proxy
  • with
  • (Beta) template
  • removed url (using proxy instead)
  • removed content (using with instead)

Misc Response

  • removed cache

Template

  • version
  • method
  • content
  • header
  • query

Global Settings

  • context
  • file root
  • environment

Release 0.7 (1-May-2013)

the first public release of Moco

Usage

  • API
  • standalone server
  • shell
  • maven plugin (external)
  • gradle plugin (external)

APIs

Request

HTTP

  • content(text, file)
  • URI
  • query Parameter
  • HTTP method
  • version
  • header
  • redirect

Operator

  • eq
  • match

Web

  • cookie
  • form

Integration

  • XML
  • XPath
  • JSON

Response

HTTP

  • content(text, file)
  • version
  • status code
  • header

Integration

  • url

Misc Response

  • latency
  • sequence(API only)
  • cache

Misc

  • mount

File: moco-doc/rest-apis.md

REST APIs

Restful service is a defacto service design style. Moco provides REST APIs which facilitate restful service stub implementation.

Table of Contents

Basic Usage

You will create a rest server with restServer as follows:

java
RestServer server = restServer(port, log());

ResourceObject resource = new ResourceObject();
resource.code = 1;
resource.message = "hello";

server.resource("targets",
	get("1").response(toJson(resource))
);

In this case, you create a resource named targets. If you access this resource with /targets/1 (1 is the ID for this resource) in GET method, a resource object will be returned.

Note: Restful service is also an HTTP service by default, so HTTP(s) APIs also available to REST server. As you have seen here, toJson is a response handler from Moco.

REST APIs is also available in JSON APIs. The same resource could be created in JSON APIs as follows:

json
[
{
"resource": {
  "name": "targets",
  "get": [
    {
      "id": "1",
      "response": {
        "json": {
          "code": 1,
          "message": "foo"
        }
      }
    }
  ]
}
}
]

Composite REST Settings

Many REST settings could be added with resource API as follows:

java
server.resource("targets",
	get("1").response(toJson(resource1)),
	get("2").response(toJson(resource2)),
    post().response(status(201), header("Location", "/targets/123"))
);

Request and Response

You can also setup request and response for every single REST setting as you've done with regular Moco APIs.

java
server.resource("targets",
	get("1").request(eq(header("Content-Type"), "application/json")).response(toJson(resource))
);

Visit HTTP(s) APIs for more request and response settings.

ID Matcher

In REST API design, ID is used to identify a specified resource. You can specify a ID with a string.

java
server.resource("targets",
	get("1").response(toJson(resource))
);

Or you can match your single resource with ID matcher. Currently, anyId is provided in Moco REST APIs which means the following setting will cover both /targets/1 and /targets/2, even /targets/anything.

java
server.resource("targets",
	get(anyId()).response(toJson(resource))
);

* can play as any id in JSON API.

json
"resource": {
  "name": "targets",
  "get": [
    {
      "id": "*",
      "response": {
        "json": {
          "code": 1,
          "message": "foo"
        }
      }
    }
  ]
}

Methods

GET

Get with ID

@Since 0.11.0

In REST API design, GET method is used to query. If an ID is specified, it is used to query a single resource. The following setting could be visited with /targets/1 in GET method.

  • Java
java
server.resource("targets",
	get("1").response(toJson(resource))
);
  • JSON
text
"resource": {
  "name": "targets",
  "get": [
    {
      "id": "1",
      "response": {
        "json": {
          "code": 1,
          "message": "foo"
        }
      }
    }
  ]
}

Get All

@Since 0.11.0

If no id specified, all related resource will be returned. The following setting could be visited with /targets in GET method.

  • Java
java
server.resource("targets",
	get().response(toJson(Arrays.asList(resource)))
);
  • JSON
json
"resource": {
  "name": "targets",
  "get": [
    {
      "response": {
        "json": {
          "code": 1,
          "message": "foo"
        }
      }
    }
  ]
}

POST

@Since 0.11.0

POST method is used to create new resource. The following setting could be visited with /targets in POST method.

  • Java
java
server.resource("targets",
	post().response(status(201), header("Location", "/targets/123"))
);
  • JSON
json
"resource": {
  "name": "targets",
  "post": [
    {
      "response": {
        "status": 201,
        "headers" : {
          "Location": "/targets/123"
		}
      }
    }
  ]
}

PUT

@Since 0.11.0

PUT method is used to update a specified resource. The following setting could be visited with /targets/1 in PUT method.

  • Java
java
server.resource("targets",
	put("1").response(status(200))
);
  • JSON
json
"resource": {
  "name": "targets",
  "put": [
    {
      "id": 1,
      "response": {
        "status": 200,
      }
    }
  ]
}

DELETE

@Since 0.11.0

DELETE method is used to delete a specified resource. The following setting could be visited with /targets/1 in DELETE method.

  • Java
java
server.resource("targets",
	delete("1").response(status(200))
);
  • JSON
json
"resource": {
  "name": "targets",
  "delete": [
    {
      "id": 1,
      "response": {
        "status": 200,
      }
    }
  ]
}

HEAD with ID

@Since 0.11.0

HEAD method is used to query resource metadata. If an ID is specified, it is used to query a single resource. The following setting could be visited with /targets/1 in HEAD method.

  • Java
java
server.resource("targets",
	head("1").response(header("ETag", "Moco"))
);
  • JSON
text
"resource": {
  "name": "targets",
  "get": [
    {
      "id": "1",
      "response": {
        "headers": {
          "ETag": "Moco"
        }
      }
    }
  ]
}

HEAD All

@Since 0.11.0

If no id specified, all related resource metadata will be returned. The following setting could be visited with /targets in HEAD method.

  • Java
java
server.resource("targets",
	head().response(header("ETag", "Moco"))
);
  • JSON
text
"resource": {
  "name": "targets",
  "get": [
    {
      "response": {
        "headers": {
          "ETag": "Moco"
        }
      }
    }
  ]
}

PATCH

@Since 0.11.0

PATCH method is used to update a specified resource. The following setting could be visited with /targets/1 in PATCH method.

  • Java
java
server.resource("targets",
	patch("1").response(status(200))
);
  • JSON
json
"resource": {
  "name": "targets",
  "patch": [
    {
      "id": 1,
      "response": {
        "status": 200,
      }
    }
  ]
}

Sub-resource

@Since 0.11.0

Sub-resource is used to build relationship for resource. The following setting could be visited with /targets/1/subs/1 in GET method.

  • Java
java
server.resource("targets",
	id("1").name("subs").settings(
      get("1").response(toJson(resource)),
    )
);
  • JSON
json
"resource": {
  "name": "targets",
  "resource": [
  	{
      "id": "1",
      "name": "subs",
      "get": [
      	{
          "id": "1",
          "response": {
            "json": {
              "code": 3,
              "message": "sub"
            }
          }
        }
      ]
	}
  ]
}

The REST settings in sub-resource are the same as regular REST API.


File: moco-doc/sse-apis.md

SSE APIs

@Since 1.6.0

Server-Sent Events (SSE) streaming support for mocking real-time event streams, including LLM-style token output.

Basic Usage

java
import static com.github.dreamhead.moco.Moco.by;
import static com.github.dreamhead.moco.Moco.uri;
import static com.github.dreamhead.moco.MocoSse.event;
import static com.github.dreamhead.moco.MocoSse.sse;

HttpServer server = httpServer(12306);
server.request(by(uri("/events")))
      .response(sse(event("message", "Hello")));

API Reference

MocoSse Factory Class

All SSE APIs are in MocoSse:

java
import static com.github.dreamhead.moco.MocoSse.event;
import static com.github.dreamhead.moco.MocoSse.data;
import static com.github.dreamhead.moco.MocoSse.sse;

sse(SseEvent first, SseEvent... rest)

Creates an SSE streaming response handler with one or more events.

  • Java API
java
server.request(by(uri("/events")))
      .response(sse(
          event("message", "Hello"),
          event("message", " World")
      ));
  • JSON
json
{
  "request" : {
    "uri" : "/events"
  },
  "response" : {
    "sse" : [
      { "event": "message", "data": "Hello" },
      { "event": "message", "data": " World" }
    ]
  }
}

sse(Resource resource)

Creates an SSE streaming response from a resource file.

  • Java API
java
server.request(by(uri("/events")))
      .response(sse(file("events.txt")));
  • JSON
json
{
  "request" : {
    "uri" : "/events"
  },
  "response" : {
    "sse" : {
      "file" : "events.txt"
    }
  }
}

sse(...).delay(long)

Sets a default delay for all events without their own delay. Event-level delay takes precedence.

  • Java API
java
sse(event("message", "Hello"), event("message", " World")).delay(50)           // milliseconds
sse(event("message", "Hello"), event("message", " World")).delay(1, TimeUnit.SECONDS)
  • JSON
json
{
  "response" : {
    "sse" : {
      "delay" : 50,
      "events" : [
        { "event": "message", "data": "Hello" },
        { "event": "message", "data": " World" }
      ]
    }
  }
}

With time unit:

json
{
  "response" : {
    "sse" : {
      "delay" : { "duration": 1, "unit": "SECOND" },
      "events" : [
        { "event": "message", "data": "Hello" },
        { "event": "message", "data": " World" }
      ]
    }
  }
}

Supported units: NANOSECOND, MICROSECOND, MILLISECOND, SECOND, MINUTE, HOUR, DAY.

event(String name, String data, String... rest)

Creates a named SSE event.

  • Java API
java
event("message", "Hello World")

data(String data, String... rest)

Creates an anonymous SSE event (no event name).

  • Java API
java
data("Hello World")

Event Modifiers

Chain modifiers on events:

  • Java API
java
event("message", "Hello").id("1")           // Set event ID
event("message", "Hello").retry(3000)        // Set reconnection time (ms)
event("message", "Hello").delay(50)          // Set send delay (ms), must be > 0
event("message", "Hello").delay(1, TimeUnit.SECONDS)  // Set send delay with time unit
event("message", "Hello").id("1").retry(3000).delay(50)  // Combine modifiers
  • JSON
json
{ "event": "message", "data": "Hello", "id": "1", "retry": 3000, "delay": 50 }

With time unit:

json
{ "event": "message", "data": "Hello", "delay": { "duration": 1, "unit": "SECOND" } }

Streaming Behavior

SSE responses are streamed over a persistent HTTP connection:

  • Each event is sent as a separate HTTP chunk
  • Events with delay() are sent with the specified interval between them
  • Use sse(...).delay(ms) to set a default delay for all events at once
  • Connection is closed after all events are sent
  • delay is a Moco mock feature, not part of the SSE wire format

Response Headers

SSE responses automatically include:

  • Content-Type: text/event-stream
  • Cache-Control: no-cache
  • Connection: keep-alive
  • X-Accel-Buffering: no

Examples

LLM Token Streaming

  • Java API
java
server.request(by(uri("/chat")))
      .response(sse(
          event("message", "Hello").delay(50),
          event("message", " World").delay(50),
          event("message", "!").delay(50)
      ));
  • JSON
json
{
  "request" : {
    "uri" : "/chat"
  },
  "response" : {
    "sse" : [
      { "event": "message", "data": "Hello", "delay": 50 },
      { "event": "message", "data": " World", "delay": 50 },
      { "event": "message", "data": "!", "delay": 50 }
    ]
  }
}

Multiple Events with IDs

  • Java API
java
server.request(by(uri("/events")))
      .response(sse(
          event("update", "First").id("001"),
          event("update", "Second").id("002")
      ));
  • JSON
json
{
  "request" : {
    "uri" : "/events"
  },
  "response" : {
    "sse" : [
      { "event": "update", "data": "First", "id": "001" },
      { "event": "update", "data": "Second", "id": "002" }
    ]
  }
}

Anonymous Data Events

  • Java API
java
server.request(by(uri("/events")))
      .response(sse(
          data("first"),
          data("second")
      ));
  • JSON
json
{
  "request" : {
    "uri" : "/events"
  },
  "response" : {
    "sse" : [
      { "data": "first" },
      { "data": "second" }
    ]
  }
}

File-Based Events

  • Java API

Create events.txt:

text
event: message
data: Hello

event: message
data: World
delay: 50

Use it:

java
server.request(by(uri("/events")))
      .response(sse(file("events.txt")));
  • JSON
json
{
  "request" : {
    "uri" : "/events"
  },
  "response" : {
    "sse" : {
      "file" : "events.txt"
    }
  }
}

SSE Event File Format

text
event: message
data: first event

event: message
data: second event
delay: 50

id: 001
event: update
data: third event

Fields:

  • id - Event identifier
  • event - Event type/name
  • data - Event data (can appear multiple times for multi-line data)
  • retry - Reconnection time in milliseconds

Events are separated by blank lines.


File: moco-doc/usage.md

Usage

You have several ways to use Moco. One is API, which you can use in your unit test. The other is that run Moco as standalone. Currently, you put all your configuration in JSON file.

On the other hand, Moco has several different ways to integrate with some tools: Maven plugin, Gradle plugin and shell support.

Table of Contents

API

dependency

Moco has been published on Maven repository, so you can refer to it directly in your dependency. This is core dependency

xml
<dependency>
  <groupId>com.github.dreamhead</groupId>
  <artifactId>moco-core</artifactId>
  <version>1.6.1</version>
</dependency>

A gradle example is as follow:

groovy
repositories {
  mavenCentral()
}

dependencies {
  testImplementation(
    "com.github.dreamhead:moco-core:1.6.1",
  )
}

API example

Here is an typical Moco test case in JUnit.

java
import org.junit.jupiter.api.Test;
import java.io.IOException;
import com.github.dreamhead.moco.HttpServer;
import org.apache.http.client.fluent.Content;
import org.apache.http.client.fluent.Request;

import static com.github.dreamhead.moco.Moco.*;
import static com.github.dreamhead.moco.Runner.*;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.MatcherAssert.assertThat;

@Test
public void should_response_as_expected() throws Exception {
    HttpServer server = httpServer(12306);
    server.response("foo");

    running(server, () -> {
        Content content = Request.Get("http://localhost:12306").execute().returnContent();
        assertThat(content.asString(), is("foo"));
    });
}

As shown above, we created a new server and configure it as expected. And then run our test against this server.

Here, We use Apache Http Client Fluent API to request our testing server.

Runner API

You may need to control server start/stop by yourself, for example, in your Before (or setup) method, runner API could be used.

java
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.IOException;

import static com.github.dreamhead.moco.Moco.httpServer;
import static com.github.dreamhead.moco.Runner.runner;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

public class MocoRunnerTest {
    private Runner runner;

    @BeforeEach
    public void setup() {
        HttpServer server = httpServer(12306);
        server.response("foo");
        runner = runner(server);
        runner.start();
    }

    @AfterEach
    public void tearDown() {
        runner.stop();
    }

    @Test
    public void should_response_as_expected() throws IOException {
        Content content = Request.Get("http://localhost:12306").execute().returnContent();
        assertThat(content.asString(), is("foo"));
    }
}

Standalone

Moco can be used as standalone to run with configuration and you can download standalone directly:
Standalone Moco Runner

First of all, a JSON configuration file is required to start Moco.

json
[
  {
    "response" :
      {
        "text" : "foo"
      }
  }
]

(foo.json)

It's time to run Moco standalone server:

shell
java -jar moco-runner-<version>-standalone.jar http -p 12306 -c foo.json

Now, open your browser and input "http://localhost:12306". You will see "foo". That's it.

JSON configuration in Java API

@Since 0.10.0

If you have setup your server with JSON configuration, you can also run with your configuration from Java API.

java
import static com.github.dreamhead.moco.Moco.file;
import static com.github.dreamhead.moco.Moco.pathResource;
import static com.github.dreamhead.moco.MocoJsonRunner.jsonHttpServer;
import static com.github.dreamhead.moco.Runner.running;
import static com.github.dreamhead.moco.helper.RemoteTestUtils.port;
import static com.github.dreamhead.moco.helper.RemoteTestUtils.root;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

public class MocoJsonHttpRunnerTest extends AbstractMocoStandaloneTest {
    @Test
    public void should_return_expected_response() throws Exception {
        final HttpServer server = jsonHttpServer(12306, file("foo.json"));
        running(server, () -> {
            Content content = Request.Get("http://localhost:12306").execute().returnContent();
            assertThat(content.asString(), is("foo"));
        });
    }
}

HTTPS

HTTPS is also a mainstream usage for HTTP protocol. Moco supports HTTPS as well. The main difference in API is that a certificate is required for HTTPS.
On the other hand, httpsServer should be used.

java
final HttpsCertificate certificate = certificate(pathResource("cert.jks"), "mocohttps", "mocohttps");
final HttpsServer server = httpsServer(certificate, hit);

If you want to use HTTPS for standalone server. certificate information could be provided as CLI arguments.

shell
java -jar moco-runner-<version>-standalone.jar https -p 12306 -c foo.json --https /path/to/cert.jks --cert mocohttps --keystore mocohttps

JUnit Integration

@Since 0.11.0

Moco provides JUnit integration to simplify the use of Moco in JUnit.

More details can be found in here.

Maven Plugin

Moco also can be used as Maven plugin.

https://github.com/GarrettHeel/moco-maven-plugin

Gradle Plugin

Moco can be used in Gradle

https://github.com/silverjava/moco-gradle-plugin

Shell

If you are using Mac or Linux, you may try the following approach:

  • Make sure you have JDK 6 or later.
  • Download the script.
  • Place it on your $PATH. (~/bin is a good choice if it is on your path.)
  • Set it to be executable. (chmod 755 ~/bin/moco)

Now, you can try

shell
moco http -p 12306 -c foo.json

It will download the latest moco automatically if you don't have locally.

Scala

Scala fans can find Scala wrapper in

https://github.com/treppo/moco-scala


File: moco-doc/websocket-apis.md

Websocket API

Moco supports websocket api.

WARNING the json configuration below is just a snippet for one pair of request and response, instead of the whole configuration file.

Basis Usage

Websocket server is created from HTTP server.

java
HttpServer httpServer = httpServer(12306);
WebsocketServer server = httpServer.websocket("/ws");

If you want to response according to request content, Moco server can be configured as following:

java
server.request(by("foo")).response("bar");

You can also create websocket server in standalone api.

  • JSON
json
{
  "websocket": {
    "uri": "/ws",
    "sessions": [
      {
        "request": {
          "text": "foo"
        },
        "response": {
          "text": "bar"
        }
      }
    ]
  }
}

Connection Message

Client can receive Websocket server message while connection is established.

java
webSocketServer.connected("connected");
json
{
  "websocket": {
    "uri": "/ws",
    "connected": "connected"
  }
}

Ping/Pong

Ping/Pong is usually used as heartbeat in websocket.

java
websocketServer.ping("ping").pong("pong");
json
{
  "websocket": {
    "uri": "/ws",
    "pingpongs": [
        {
          "ping": "ping",
          "pong": "pong"
        }
    ]
  }
}

Broadcast

You can setup broadcast which means you broadcast your request to all client when you receive request.

java
server.request(by("foo")).response(broadcast("bar"));
json
{
  "request": {
    "text": "foo"
  },
  "response": {
    "broadcast": {
      "content": "bar"
    }
  }
}

If you just want to broadcast your message to some specific clients, group may help you. A client is supposed to join this group before your broadcast as following:

java
server.request(by("subscribe")).response(with("subscribed"), join(group("group")));
server.request(by("broadcast")).response(with("broadcasted"), broadcast("content", group("group")));
json
[
  {
    "request": {
      "text": "subscribe"
    },
    "response": {
      "content": "subscribed",
      "group": "group"
    }
  },
  {
    "request": {
      "text": "broadcast"
    },
    "response": {
      "content": "broadcasted",
      "broadcast": {
        "content": "content",
        "group": "group"
      }
    }
  }
]

File: moco-junit5/src/test/resources/base.json

[
{
"request": {
"text": "foo"
},
"response": {
"text": "bar"
}
},
{
"response": {
"text": "blah"
}
}
]


File: moco-junit5/src/test/resources/foo.json

[
{
"description": "any response",
"response": {
"text": "foo"
}
},
{
"request": {
"uri": "/foo"
},
"response": {
"text": "bar"
}
},
{
"request": {
"uri": "/file"
},
"response": {
"file": "src/test/resources/foo.response"
}
},
{
"request": {
"text": "text_request"
},
"response": {
"text": "response_for_text_request"
}
},
{
"request": {
"file": "src/test/resources/foo.request"
},
"response": {
"text": "response_for_file_request"
}
},
{
"request": {
"uri": "/header",
"headers": {
"content-type": "application/json"
}
},
"response": {
"text": "response_for_header_request"
}
},
{
"request": {
"uri": "/query",
"queries": {
"param": "foo"
}
},
"response": {
"text": "response_for_query_request"
}
},
{
"request": {
"uri": "/xpath",
"xpaths": {
"/request/parameters/id/text()": "1"
}
},
"response": {
"text": "response_for_xpath_request"
}
},
{
"request": {
"uri": "/status"
},
"response": {
"status": 200
}
},
{
"request": {
"uri": "/response_header"
},
"response": {
"headers": {
"content-type": "application/json",
"foo": "bar"
}
}
},
{
"request": {
"uri": "/proxy"
},
"response": {
"proxy": "https://github.com/"
}
},
{
"request": {
"uri": "/composite-response"
},
"response": {
"status": 200,
"headers": {
"foo": "bar"
}
}
},
{
"request": {
"uri": "/latency"
},
"response": {
"latency": 1000
}
},
{
"request": {
"uri": "/latency-with-unit"
},
"response": {
"latency": {
"duration": 1,
"unit": "second"
}
}
},
{
"request": {
"uri": "/cookie",
"cookies": {
"login": "true"
}
},
"response": {
"text": "success"
}
},
{
"request": {
"uri": "/cookie"
},
"response": {
"cookies": {
"login": "true"
}
}
},
{
"request": {
"version": "HTTP/1.0"
},
"response": {
"text": "version"
}
},
{
"request": {
"uri": "/version10"
},
"response": {
"version": "HTTP/1.0"
}
}
]


File: moco-junit5/src/test/resources/rest.json

[
{
"resource": {
"name": "targets",
"post": [
{
"response": {
"status": 201,
"headers": {
"Location": "/targets/123"
}
}
}
]
}
}
]


File: moco-runner/src/test/resources/any_response_with_file.json

[
{
"response" :
{
"file" : "src/test/resources/foo.response"
}
}
]


File: moco-runner/src/test/resources/attachment.json

[
{
"request": {
"uri": "/text_attachment"
},
"response": {
"attachment": {
"filename": "text_attachment.txt",
"text": "text_attachment"
}
}
},
{
"request": {
"uri": "/file_attachment"
},
"response": {
"attachment": {
"filename": "file_attachment.txt",
"file": "src/test/resources/foo.response"
}
}
},
{
"request": {
"uri": "/path_attachment"
},
"response": {
"attachment": {
"filename": "path_attachment.txt",
"path_resource": "path.response"
}
}
}
]


File: moco-runner/src/test/resources/base.json

[
{
"request": {
"text": "foo"
},
"response": {
"text": "bar"
}
},
{
"response": {
"text": "blah"
}
}
]


File: moco-runner/src/test/resources/contain.json

[
{
"request": {
"uri": {
"contain": "foo"
}
},
"response": {
"text": "uri_match"
}
},
{
"request": {
"uri": "/text-match",
"text": {
"contain": "foo"
}
},
"response": {
"text": "text_match"
}
},
{
"request": {
"uri": "/header-match",
"headers": {
"foo": {
"contain" : "bar"
}
}
},
"response": {
"text": "header_match"
}
},
{
"request": {
"uri": "/query-match",
"queries": {
"foo": {
"contain" : "bar"
}
}
},
"response": {
"text": "query_match"
}
}
]


[
{
"request": {
"uri": "/cookie"
},
"response": {
"cookies": {
"login": "true"
}
}
},
{
"request": {
"uri": "/cookie-with-path"
},
"response": {
"cookies": {
"login": {
"value": "true",
"path": "/"
}
}
}
},
{
"request": {
"uri": "/cookie-with-domain"
},
"response": {
"cookies": {
"login": {
"value": "true",
"domain": "github.com"
}
}
}
},
{
"request": {
"uri": "/cookie-with-secure"
},
"response": {
"cookies": {
"login": {
"value": "true",
"secure": "true"
}
}
}
},
{
"request": {
"uri": "/cookie-with-http-only"
},
"response": {
"cookies": {
"login": {
"value": "true",
"httpOnly": "true"
}
}
}
},
{
"request": {
"uri": "/cookie-with-max-age"
},
"response": {
"cookies": {
"login": {
"value": "true",
"maxAge": {
"duration": 1,
"unit": "hour"
}
}
}
}
},
{
"request": {
"uri": "/cookie-with-same-site"
},
"response": {
"cookies": {
"login": {
"value": "true",
"sameSite": "Lax"
}
}
}
}
]


File: moco-runner/src/test/resources/cors.json

[
{
"request": {
"uri": "/default-cors"
},
"response": {
"cors": true
}
},
{
"request": {
"uri": "/cors"
},
"response": {
"cors": {
"allowOrigin": "",
"allowMethods": [
"GET",
"POST",
"PUT",
"DELETE",
"OPTIONS"
],
"allowHeaders": [
"Content-Type, Authorization, Accept, X-Requested-With"
],
"maxAge": {
"duration": 1728000,
"unit": "second"
},
"exposeHeaders": [
"X-My-Custom-Header, X-Another-Custom-Header"
],
"allowCredentials": true
}
}
},
{
"request": {
"uri": "/cors-alias"
},
"response": {
"cors": {
"Access-Control-Allow-Origin": "
",
"Access-Control-Allow-Methods": [
"GET",
"POST",
"PUT",
"DELETE",
"OPTIONS"
],
"Access-Control-Allow-Headers": [
"Content-Type, Authorization, Accept, X-Requested-With"
],
"Access-Control-Max-Age": {
"duration": 1728000,
"unit": "second"
},
"Access-Control-Expose-Headers": [
"X-My-Custom-Header, X-Another-Custom-Header"
],
"Access-Control-Allow-Credentials": true
}
}
}
]


File: moco-runner/src/test/resources/cycle.json

[
{
"request" : {
"uri" : "/cycle"
},
"response": {
"cycle": [
{
"text" : "foo"
},
{
"text" : "bar"
}
]
}
},
{
"request" : {
"uri" : "/cycle-json"
},
"response": {
"cycle": [
{
"json" : {
"foo" : "bar"
}
},
{
"json" : {
"hello" : "world"
}
}
]
}
}
]


File: moco-runner/src/test/resources/delete_method.json

[
{
"request" :
{
"method" : "delete",
"uri" : "/delete"
},
"response" :
{
"text" : "response_for_delete_method"
}
}
]


File: moco-runner/src/test/resources/ends_with.json

[
{
"request": {
"uri": {
"endsWith": "foo"
}
},
"response": {
"text": "uri_match"
}
},
{
"request": {
"uri": "/text-match",
"text": {
"endsWith": "foo"
}
},
"response": {
"text": "text_match"
}
},
{
"request": {
"uri": "/header-match",
"headers": {
"content-type": {
"endsWith" : "json"
}
}
},
"response": {
"text": "header_match"
}
},
{
"request": {
"uri": "/query-match",
"queries": {
"foo": {
"endsWith" : "r"
}
}
},
"response": {
"text": "query_match"
}
}
]


File: moco-runner/src/test/resources/event.json

[
{
"request": {
"uri": "/event"
},
"response": {
"text": "post_foo"
},
"on": {
"complete": {
"async": "true",
"latency": 500,
"post": {
"url": "http://localhost:12306/target",
"content": "content"
}
}
}
},
{
"request": {
"uri": "/event-with-unit"
},
"response": {
"text": "post_foo"
},
"on": {
"complete": {
"async": "true",
"latency": {
"duration": 1,
"unit": "second"
},
"post": {
"url": "http://localhost:12306/target",
"content": "content"
}
}
}
},
{
"request": {
"uri" : "/get_event"
},
"response": {
"text": "get_foo"
},
"on": {
"complete": {
"async" : "true",
"latency" : 1000,
"get" : {
"url" : "http://localhost:12306/target"
}
}
}
},
{
"request": {
"uri" : "/get_event_with_header"
},
"response": {
"text": "get_foo_with_header"
},
"on": {
"complete": {
"async" : "true",
"latency" : 1000,
"get" : {
"url" : "http://localhost:12306/target-with-header",
"headers": {
"foo": "bar"
}
}
}
}
},
{
"request": {
"uri" : "/post_event_with_header"
},
"response": {
"text": "post_foo_with_header"
},
"on": {
"complete": {
"async" : "true",
"latency" : 1000,
"post" : {
"url" : "http://localhost:12306/target-with-header",
"headers": {
"foo": "bar"
},
"content": "content"
}
}
}
},
{
"request": {
"uri" : "/get_event_template"
},
"response": {
"text": "get_foo"
},
"on": {
"complete": {
"async" : "true",
"latency" : 1000,
"get" : {
"url" : {
"template": {
"with": "http://localhost:12306/${target}",
"vars": {
"target" : "target"
}
}
}
}
}
}
},
{
"request": {
"uri": "/post-event-with-template-url"
},
"response": {
"text": "post_foo"
},
"on": {
"complete": {
"async": "true",
"latency": 500,
"post": {
"url" : {
"template": {
"with": "http://localhost:12306/${target}",
"vars": {
"target" : "target"
}
}
},
"content": "content"
}
}
}
},
{
"request": {
"uri": "/post-event-with-template-content"
},
"response": {
"text": "post_foo"
},
"on": {
"complete": {
"async": "true",
"latency": 500,
"post": {
"url": "http://localhost:12306/target",
"content": {
"template": {
"with": "${target}",
"vars": {
"target" : "target"
}
}
}
}
}
}
},
{
"request": {
"uri": "/event-with-json-post"
},
"response": {
"text": "post_json_foo"
},
"on": {
"complete": {
"async": "true",
"latency": 500,
"post": {
"url": "http://localhost:12306/target-json",
"json": {
"foo": "bar"
}
}
}
}
},
{
"request": {
"uri" : "/target"
},
"response": {
"text": "0XCAFEBABE"
}
},
{
"request": {
"uri" : "/target-json",
"json" : {
"foo" : "bar"
}
},
"response": {
"text": "0XMOCOJSON"
}
},
{
"request": {
"uri" : "/target-with-header",
"headers" : {
"foo" : "bar"
}
},
"response": {
"text": "0XMOCOHEADER"
}
}
]


File: moco-runner/src/test/resources/exist.json

[
{
"request": {
"uri": "/text-match",
"text": {
"exist": "true"
}
},
"response": {
"text": "text_match"
}
},
{
"request": {
"uri": "/text-not-match",
"text": {
"exist": "false"
}
},
"response": {
"text": "text_not_match"
}
},
{
"request": {
"uri": "/header-match",
"headers": {
"content-type": {
"exist" : "true"
}
}
},
"response": {
"text": "header_match"
}
},
{
"request": {
"uri": "/header-not-match",
"headers": {
"foo": {
"exist" : "false"
}
}
},
"response": {
"text": "header_not_match"
}
},
{
"request": {
"uri": "/query-match",
"queries": {
"foo": {
"exist" : "true"
}
}
},
"response": {
"text": "query_match"
}
},
{
"request": {
"uri": "/query-not-match",
"queries": {
"foo": {
"exist" : "false"
}
}
},
"response": {
"text": "query_not_match"
}
}
]


File: moco-runner/src/test/resources/foo.json

[
{
"description": "any response",
"response": {
"text": "foo"
}
},
{
"request": {
"uri": "/foo"
},
"response": {
"text": "bar"
}
},
{
"request": {
"uri": "/file"
},
"response": {
"file": "src/test/resources/foo.response"
}
},
{
"request": {
"text": "text_request"
},
"response": {
"text": "response_for_text_request"
}
},
{
"request": {
"file": "src/test/resources/foo.request"
},
"response": {
"text": "response_for_file_request"
}
},
{
"request": {
"uri": "/header",
"headers": {
"content-type": "application/json"
}
},
"response": {
"text": "response_for_header_request"
}
},
{
"request": {
"uri": "/query",
"queries": {
"param": "foo"
}
},
"response": {
"text": "response_for_query_request"
}
},
{
"request": {
"uri": "/xpath",
"xpaths": {
"/request/parameters/id/text()": "1"
}
},
"response": {
"text": "response_for_xpath_request"
}
},
{
"request": {
"uri": "/status"
},
"response": {
"status": 200
}
},
{
"request": {
"uri": "/response_header"
},
"response": {
"headers": {
"content-type": "application/json",
"foo": "bar"
}
}
},
{
"request": {
"uri": "/proxy"
},
"response": {
"proxy": "http://localhost:12306/foo"
}
},
{
"request": {
"uri": "/composite-response"
},
"response": {
"status": 200,
"headers": {
"foo": "bar"
}
}
},
{
"request": {
"uri": "/latency"
},
"response": {
"latency": 1000
}
},
{
"request": {
"uri": "/latency-with-unit"
},
"response": {
"latency": {
"duration": 1,
"unit": "second"
}
}
},
{
"request": {
"version": "HTTP/1.0"
},
"response": {
"text": "version"
}
},
{
"request": {
"uri": "/version10"
},
"response": {
"version": "HTTP/1.0"
}
}
]


File: moco-runner/src/test/resources/form.json

[
{
"request" : {
"method" : "post",
"forms" : {
"name" : "dreamhead"
}
},
"response" : {
"text" : "foobar"
}
}
]


File: moco-runner/src/test/resources/get_method.json

[
{
"request" :
{
"method" : "get",
"uri" : "/get"
},
"response" :
{
"text" : "response_for_get_method"
}
}
]


File: moco-runner/src/test/resources/header.json

[
{
"request" :
{
"uri" : "/header",
"headers" : {
"content-type" : "application/json"
}
},
"response" :
{
"text" : "response_for_header_request"
}
}
]


File: moco-runner/src/test/resources/json.json

[
{
"request": {
"uri": "/json",
"text": {
"json": "{"foo":"bar"}"
}
},
"response": {
"text": "response_for_json_request"
}
},
{
"request": {
"uri": "/json_shortcut",
"json": {
"foo": "bar"
}
},
"response": {
"text": "response_for_json_shortcut"
}
},
{
"request": {
"uri": "/json_response_shortcut"
},
"response": {
"json": {
"foo" : "bar"
}
}
}
]


File: moco-runner/src/test/resources/json_struct.json

[
{
"request": {
"uri": "/json-struct",
"struct": {
"json": {
"foo": "bar"
}
}
},
"response": {
"text": "response_for_json_struct"
}
}
]


File: moco-runner/src/test/resources/jsonpath.json

[
{
"request": {
"uri": "/jsonpath",
"json_paths": {
"$.book.price": "1"
}
},
"response": {
"text": "response_for_json_path_request"
}
}
]


File: moco-runner/src/test/resources/match.json

[
{
"request": {
"uri": {
"match": "/\w*/foo"
}
},
"response": {
"text": "uri_match"
}
},
{
"request": {
"uri": "/text-match",
"text": {
"match": "\wfoo"
}
},
"response": {
"text": "text_match"
}
},
{
"request": {
"uri": "/method-match",
"method": {
"match": "get|post"
}
},
"response": {
"text": "method_match"
}
},
{
"request": {
"uri": "/header-match",
"headers": {
"content-type": {
"match" : "application/\w
"
}
}
},
"response": {
"text": "header_match"
}
},
{
"request": {
"uri": "/query-match",
"queries": {
"foo": {
"match" : "b\w*"
}
}
},
"response": {
"text": "query_match"
}
}
]


File: moco-runner/src/test/resources/mount.json

[
{
"mount" :
{
"dir" : "src/test/resources/mount",
"uri" : "/mount"
}
},
{
"mount" :
{
"dir" : "src/test/resources/mount",
"uri" : "/mount-include",
"includes" :
[
".response"
]
}
},
{
"mount" :
{
"dir" : "src/test/resources/mount",
"uri" : "/mount-exclude",
"excludes" :
[
"
.response"
]
}
},
{
"mount" :
{
"dir" : "src/test/resources/mount",
"uri" : "/mount-response",
"headers" : {
"Content-Type" : "text/plain"
}
}
}

]


File: moco-runner/src/test/resources/path.json

[
{
"request": {
"uri": {
"path": "/path/{path}"
}
},
"response": {
"text": "path"
}
},
{
"request": {
"uri": {
"path": "/path/{path}/sub/{sub}"
}
},
"response": {
"text": "sub"
}
}
]


File: moco-runner/src/test/resources/path_resource.json

[
{
"request": {
"uri": "/path-resource"
},
"response": {
"path_resource": "path.response"
}
},
{
"request": {
"path_resource": "path.request"
},
"response": {
"text": "path resource"
}
}
]


File: moco-runner/src/test/resources/post_method.json

[
{
"request" :
{
"method" : "post",
"uri" : "/post"
},
"response" :
{
"text" : "response_for_post_method"
}
}
]


File: moco-runner/src/test/resources/proxy.json

[
{
"request" :
{
"uri" : "/target"
},
"response" :
{
"text" : "proxy_target"
}
},
{
"request" :
{
"uri" : "/sse-target"
},
"response" :
{
"sse" : [
{ "event": "message", "data": "Hello" },
{ "event": "message", "data": "World" }
]
}
},
{
"request" :
{
"uri" : "/sse-delay-target"
},
"response" :
{
"sse" : [
{ "event": "message", "data": "first", "delay": 100 },
{ "event": "message", "data": "second", "delay": 100 }
]
}
},
{
"request" :
{
"uri" : "/proxy"
},
"response" :
{
"proxy" : "http://localhost:12306/target"
}
},
{
"request" :
{
"uri" : "/sse-proxy"
},
"response" :
{
"proxy" : "http://localhost:12306/sse-target"
}
},
{
"request" :
{
"uri" : "/sse-proxy-delay"
},
"response" :
{
"proxy" : "http://localhost:12306/sse-delay-target"
}
},
{
"request" :
{
"uri" : "/failover"
},
"response" :
{
"proxy" :
{
"url" : "http://localhost:12306/unknown",
"failover" : "src/test/resources/standalone_failover.json"
}
}
},
{
"request" :
{
"uri" : "/playback"
},
"response" :
{
"proxy" :
{
"url" : "http://localhost:12306/unknown",
"playback" : "src/test/resources/standalone_failover.json"
}
}
},
{
"request" :
{
"uri" : "/template-url"
},
"response" :
{
"proxy" :
{
"url" : {
"template": "http://localhost:12306/${req.queries['foo']}"
}
}
}
},
{
"request" :
{
"uri" : "/failover-target"
},
"response" :
{
"seq" : [
{
"status": 404
},
{
"status": 500
}
]
}
},
{
"request" :
{
"uri" : "/failover-with-status"
},
"response" :
{
"proxy" :
{
"url" : "http://localhost:12306/failover-target",
"failover" : {
"file": "src/test/resources/standalone_failover.json",
"status": [404, 500]
}
}
}
}
]


File: moco-runner/src/test/resources/proxy_batch.json

[
{
"request" :
{
"uri" : "/target/1"
},
"response" :
{
"text" : "target_1"
}
},
{
"request" :
{
"uri" : "/target/2"
},
"response" :
{
"text" : "target_2"
}
},
{
"request" :
{
"uri" : {
"match" : "/proxy/."
}
},
"response" :
{
"proxy" : {
"from" : "/proxy",
"to" : "http://localhost:12306/target"
}
}
},
{
"request" :
{
"uri" : {
"match" : "/failover/.
"
}
},
"response" :
{
"proxy" :
{
"from" : "/failover",
"to" : "http://localhost:12306/unknown",
"failover" : "src/test/resources/standalone_failover.json"
}
}
}
]


File: moco-runner/src/test/resources/proxy_error_from_missing.json

[
{
"request" :
{
"uri" : "/target/1"
},
"response" :
{
"text" : "target_1"
}
},
{
"request" :
{
"uri" : "/target/2"
},
"response" :
{
"text" : "target_2"
}
},
{
"request" :
{
"uri" : {
"match" : "/proxy/.*"
}
},
"response" :
{
"proxy" : {
"to" : "http://localhost:12306/target"
}
}
}
]


File: moco-runner/src/test/resources/proxy_error_multiple_mode.json

[
{
"request" :
{
"uri" : "/target/1"
},
"response" :
{
"text" : "target_1"
}
},
{
"request" :
{
"uri" : "/target/2"
},
"response" :
{
"text" : "target_2"
}
},
{
"request" :
{
"uri" : {
"match" : "/proxy/.*"
}
},
"response" :
{
"proxy" : {
"url" : "http://localhost:12306/target/1",
"from" : "/proxy",
"to" : "http://localhost:12306/target"
}
}
}
]


File: moco-runner/src/test/resources/proxy_error_to_missing.json

[
{
"request" :
{
"uri" : "/target/1"
},
"response" :
{
"text" : "target_1"
}
},
{
"request" :
{
"uri" : "/target/2"
},
"response" :
{
"text" : "target_2"
}
},
{
"request" :
{
"uri" : {
"match" : "/proxy/.*"
}
},
"response" :
{
"proxy" : {
"from" : "/proxy"
}
}
}
]


File: moco-runner/src/test/resources/proxy_error_url_from_server.json

[
{
"proxy" : {
"url" : "http://localhost:12306/target"
}
}
]


File: moco-runner/src/test/resources/proxy_server.json

[
{
"request" :
{
"uri" : "/target/1"
},
"response" :
{
"text" : "target_1"
}
},
{
"request" :
{
"uri" : "/target/2"
},
"response" :
{
"text" : "target_2"
}
},
{
"proxy" : {
"from" : "/proxy",
"to" : "http://localhost:12306/target"
}
},
{
"proxy" :
{
"from" : "/failover",
"to" : "http://localhost:12306/unknown",
"failover" : "src/test/resources/standalone_failover.json"
}
}
]


File: moco-runner/src/test/resources/proxy_sse_failover.json

[
{
"request": {
"uri": "/sse-target"
},
"response": {
"sse": [
{ "event": "message", "data": "Hello" },
{ "event": "message", "data": "World" }
]
}
},
{
"request": {
"uri": "/sse-proxy-failover"
},
"response": {
"proxy": {
"url": "http://localhost:12306/sse-target",
"failover": "src/test/resources/standalone_sse_failover.json"
}
}
},
{
"request": {
"uri": "/sse-proxy-playback"
},
"response": {
"proxy": {
"url": "http://localhost:12306/unknown",
"playback": "src/test/resources/standalone_sse_failover.json"
}
}
}
]


File: moco-runner/src/test/resources/proxy_sse_failover_delay.json

[
{
"request": {
"uri": "/sse-delay-target"
},
"response": {
"sse": [
{ "event": "message", "data": "first", "delay": 100 },
{ "event": "message", "data": "second", "delay": 100 }
]
}
},
{
"request": {
"uri": "/sse-proxy-delay"
},
"response": {
"proxy": {
"url": "http://localhost:12306/sse-delay-target",
"failover": "src/test/resources/standalone_sse_failover_delay.json"
}
}
}
]


File: moco-runner/src/test/resources/proxy_sse_failover_partial.json

[
{
"request": {
"uri": "/sse-target-partial"
},
"response": {
"sse": [
{ "event": "message", "data": "Hello" },
{ "event": "message", "data": "World", "delay": 30000 }
]
}
},
{
"request": {
"uri": "/sse-proxy-partial"
},
"response": {
"proxy": {
"url": "http://localhost:12306/sse-target-partial",
"failover": "src/test/resources/standalone_sse_failover_partial.json"
}
}
}
]


File: moco-runner/src/test/resources/put_method.json

[
{
"request" :
{
"method" : "put",
"uri" : "/put"
},
"response" :
{
"text" : "response_for_put_method"
}
}
]


File: moco-runner/src/test/resources/query.json

[
{
"request" :
{
"uri" : "/query",
"queries" : {
"param" : "foo"
}
},
"response" :
{
"text" : "response_for_query_request"
}
},
{
"request" :
{
"uri" : "/empty-query",
"queries" : {
"param" : ""
}
},
"response" :
{
"text" : "response_for_empty_query_request"
}
},
{
"request" :
{
"uri" : "/multi-query",
"queries" : {
"param1" : "foo",
"param2" : "bar"
}
},
"response" :
{
"text" : "response_for_multi_query_request"
}
}
]


File: moco-runner/src/test/resources/record_replay.json

[
{
"request" : {
"uri" : "/record"
},
"response" : {
"record" : "name"
}
},
{
"request" : {
"uri" : "/replay"
},
"response" : {
"replay" : "name"
}
},
{
"request" : {
"uri" : "/record-template"
},
"response" : {
"record" : {
"identifier": {
"template": "${req.queries['type']}"
}
}
}
},
{
"request" : {
"uri" : "/replay-template"
},
"response" : {
"replay" : {
"identifier": {
"template": "${req.queries['type']}"
}
}
}
},
{
"request" : {
"uri" : "/record-modifier"
},
"response" : {
"record" : {
"identifier": "modifier"
}
}
},
{
"request" : {
"uri" : "/replay-modifier"
},
"response" : {
"replay" : {
"identifier": "modifier",
"modifier": "${req.queries['type']}"
}
}
},
{
"request" : {
"uri" : "/record-group"
},
"response" : {
"record" : {
"group": "group",
"identifier": "modifier"
}
}
},
{
"request" : {
"uri" : "/replay-group"
},
"response" : {
"replay" : {
"group": "group",
"identifier": "modifier"
}
}
},
{
"request" : {
"uri" : "/record-tape"
},
"response" : {
"record" : {
"group": "tape-foo",
"tape": "src/test/resources/tape.json"
}
}
},
{
"request" : {
"uri" : "/replay-tape"
},
"response" : {
"replay" : {
"group": "tape-foo",
"tape": "src/test/resources/tape.json"
}
}
},
{
"request" : {
"uri" : "/record-modifier-with-header"
},
"response" : {
"record" : {
"identifier": "modifier"
}
}
},
{
"request" : {
"uri" : "/replay-modifier-with-header"
},
"response" : {
"replay" : {
"identifier": "modifier",
"modifier": {
"headers": {
"X-REPLAY": {
"template": "${req.queries['type']}"
}
}
}
}
}
}
]


File: moco-runner/src/test/resources/redirect.json

[
{
"request" :
{
"uri" : "/redirectTo"
},
"response" :
{
"text" : "foo"
}
},
{
"request" :
{
"uri" : "/redirect"
},

text
"redirectTo" : "http://localhost:12306/redirectTo"
},
{
    "request" :
    {
        "uri" : "/redirect-with-template"
    },

    "redirectTo" : {
        "template" : {
            "with" : "${url}",
            "vars" : {
                "url" : "http://localhost:12306/redirectTo"
            }
        }
    }
},
{
    "request" :
    {
        "uri" : "/redirect-with-path-resource"
    },

    "redirectTo" : {
        "path_resource": "redirect.url"
    }
}

]


File: moco-runner/src/test/resources/response_with_file_and_charset.json

[
{
"response":
{
"file":
{
"name": "src/test/resources/foo.response",
"charset": "utf-8"
}
}
},
{
"request":
{
"uri":"/charset_first"
},
"response":
{
"file":
{
"charset": "utf-8",
"name": "src/test/resources/foo.response"
}
}
}
]


File: moco-runner/src/test/resources/response_with_path_resource_and_charset.json

[
{
"response":
{
"path_resource":
{
"name": "path.response",
"charset": "utf-8"
}
}
}
]


File: moco-runner/src/test/resources/response_with_template_name.json

[
{
"response":
{
"file":
{
"name": {
"template": {
"with" : "src/test/resources/${var}.response",
"vars" : {
"var" : "foo"
}
}
}
}
}
}
]


File: moco-runner/src/test/resources/seq.json

[
{
"request" : {
"uri" : "/seq"
},
"response": {
"seq": [
{
"text" : "foo"
},
{
"text" : "bar"
}
]
}
},
{
"request" : {
"uri" : "/seq-json"
},
"response": {
"seq": [
{
"json" : {
"foo" : "bar"
}
},
{
"json" : {
"hello" : "world"
}
}
]
}
}
]


File: moco-runner/src/test/resources/sse.json

[
{
"request" : {
"uri" : "/sse"
},
"response" : {
"sse" : {
"file" : "src/test/resources/events.sse"
}
}
},
{
"request" : {
"uri" : "/inline"
},
"response" : {
"sse" : [
{ "event": "message", "data": "Hello" },
{ "event": "message", "data": " World" }
]
}
},
{
"request" : {
"uri" : "/delay"
},
"response" : {
"sse" : [
{ "event": "message", "data": "token1", "delay": 100 },
{ "event": "message", "data": "token2", "delay": 100 },
{ "event": "message", "data": "token3", "delay": 100 }
]
}
},
{
"request" : {
"uri" : "/sse-delay"
},
"response" : {
"sse" : {
"delay": 100,
"events": [
{ "event": "message", "data": "token1" },
{ "event": "message", "data": "token2" },
{ "event": "message", "data": "token3" }
]
}
}
},
{
"request" : {
"uri" : "/sse-delay-unit"
},
"response" : {
"sse" : {
"delay": { "duration": 100, "unit": "MILLISECOND" },
"events": [
{ "event": "message", "data": "token1" },
{ "event": "message", "data": "token2" },
{ "event": "message", "data": "token3" }
]
}
}
},
{
"request" : {
"uri" : "/event-delay-unit"
},
"response" : {
"sse" : [
{ "event": "message", "data": "token1", "delay": { "duration": 100, "unit": "MILLISECOND" } },
{ "event": "message", "data": "token2", "delay": { "duration": 100, "unit": "MILLISECOND" } },
{ "event": "message", "data": "token3" }
]
}
}
]


File: moco-runner/src/test/resources/standalone_failover.json

[ {
"request" : {
"version" : "HTTP/1.1",
"headers" : {
"Host" : "localhost:12306"
}
},
"response" : {
"version" : "HTTP/1.1",
"content" : "proxy",
"headers" : {
"Content-Length" : "5",
"Content-Type" : "text/plain; charset=UTF-8"
},
"status" : 200
}
} ]


File: moco-runner/src/test/resources/standalone_sse_failover.json

[ {
"request" : {
"content" : null,
"headers" : {
"Host" : [ "localhost:12306" ]
},
"method" : "GET",
"queries" : null,
"uri" : "/sse-proxy-playback",
"version" : "HTTP/1.1"
},
"response" : {
"content" : null,
"headers" : {
"Content-Type" : [ "text/event-stream" ]
},
"sseEvents" : [ {
"id" : null,
"event" : "message",
"data" : [ "Hello" ],
"retry" : null,
"delay" : 0
}, {
"id" : null,
"event" : "message",
"data" : [ "World" ],
"retry" : null,
"delay" : 0
} ],
"status" : 200,
"version" : "HTTP/1.1"
}
}, {
"request" : {
"content" : "",
"headers" : {
"content-length" : [ "0" ],
"Connection" : [ "keep-alive" ],
"Host" : [ "localhost:12306" ],
"Accept-Encoding" : [ "gzip, x-gzip, deflate" ]
},
"method" : "GET",
"queries" : { },
"uri" : "/sse-proxy-failover",
"version" : "HTTP/1.1"
},
"response" : {
"content" : null,
"headers" : { },
"sseEvents" : [ {
"id" : null,
"event" : "message",
"data" : [ "Hello" ],
"retry" : null,
"delay" : 0
}, {
"id" : null,
"event" : "message",
"data" : [ "World" ],
"retry" : null,
"delay" : 0
} ],
"status" : 200,
"version" : "HTTP/1.1"
}
} ]


File: moco-runner/src/test/resources/standalone_sse_failover_delay.json

[ {
"request" : {
"content" : "",
"headers" : {
"content-length" : [ "0" ],
"Connection" : [ "keep-alive" ],
"Host" : [ "localhost:12306" ],
"Accept-Encoding" : [ "gzip, x-gzip, deflate" ]
},
"method" : "GET",
"queries" : { },
"uri" : "/sse-proxy-delay",
"version" : "HTTP/1.1"
},
"response" : {
"content" : null,
"headers" : { },
"sseEvents" : [ {
"id" : null,
"event" : "message",
"data" : [ "first" ],
"retry" : null,
"delay" : 0
}, {
"id" : null,
"event" : "message",
"data" : [ "second" ],
"retry" : null,
"delay" : 0
} ],
"status" : 200,
"version" : "HTTP/1.1"
}
} ]


File: moco-runner/src/test/resources/standalone_sse_failover_partial.json

[ {
"request" : {
"content" : "",
"headers" : {
"content-length" : [ "0" ],
"Connection" : [ "keep-alive" ],
"Host" : [ "localhost:12306" ],
"Accept-Encoding" : [ "gzip, x-gzip, deflate" ]
},
"method" : "GET",
"queries" : { },
"uri" : "/sse-proxy-partial",
"version" : "HTTP/1.1"
},
"response" : {
"content" : null,
"headers" : { },
"sseEvents" : [ {
"id" : null,
"event" : "message",
"data" : [ "Hello" ],
"retry" : null,
"delay" : 0
} ],
"status" : 200,
"version" : "HTTP/1.1"
}
} ]


File: moco-runner/src/test/resources/starts_with.json

[
{
"request": {
"uri": {
"startsWith": "/foo"
}
},
"response": {
"text": "uri_match"
}
},
{
"request": {
"uri": "/text-match",
"text": {
"startsWith": "foo"
}
},
"response": {
"text": "text_match"
}
},
{
"request": {
"uri": "/header-match",
"headers": {
"content-type": {
"startsWith" : "application"
}
}
},
"response": {
"text": "header_match"
}
},
{
"request": {
"uri": "/query-match",
"queries": {
"foo": {
"startsWith" : "b"
}
}
},
"response": {
"text": "query_match"
}
}
]


File: moco-runner/src/test/resources/tape.json

{
"content" : {
"tape-foo" : {
"content" : {
"content" : "Zm9v",
"charset" : "UTF-8"
},
"headers" : {
"Connection" : [ "keep-alive" ],
"Host" : [ "localhost:12306" ],
"Accept-Encoding" : [ "gzip, x-gzip, deflate" ],
"Content-Length" : [ "3" ],
"Content-Type" : [ "text/plain; charset=UTF-8" ]
},
"method" : "POST",
"queries" : { },
"uri" : "/record-tape",
"version" : "HTTP/1.1"
}
}
}


File: moco-runner/src/test/resources/template.json

[
{
"request": {
"uri": "/template"
},
"response": {
"text": {
"template": "${req.method}"
}
}
},
{
"request": {
"uri": "/file_template"
},
"response": {
"file": {
"template": "src/test/resources/template.foo"
}
}
},
{
"request": {
"uri": "/path_resource_template"
},
"response": {
"path_resource": {
"template": "template.foo"
}
}
},
{
"request": {
"uri": "/version_template"
},
"response": {
"version": {
"template": "${req.version}"
}
}
},
{
"request": {
"uri": "/header_template"
},
"response": {
"headers": {
"foo": {
"template": "${req.headers['foo']}"
}
}
}
},
{
"request": {
"uri": "/cookie_template",
"cookies": {
"foo": "GET"
}
},
"response": {
"text": {
"template": "${req.cookies['foo']}"
}
}
},
{
"request": {
"uri": "/cookie_template"
},
"response": {
"cookies": {
"foo": {
"template": "${req.method}"
}
},
"status": 302
}
},
{
"request": {
"uri": "/form_template"
},
"response": {
"text": {
"template": "${req.forms['foo']}"
}
}
},
{
"request": {
"uri": "/query_template"
},
"response": {
"text": {
"template": "${req.queries['foo']}"
}
}
},
{
"request": {
"uri": "/json_template"
},
"response": {
"text": {
"template": "${req.json.foo}"
}
}
},
{
"request": {
"uri": "/xml_template"
},
"response": {
"text": {
"template": "${req.xml.foo}"
}
}
},
{
"request": {
"uri": "/client_address_template"
},
"response": {
"text": {
"template": "${req.client.address}"
}
}
},
{
"request": {
"uri": "/client_port_template"
},
"response": {
"text": {
"template": "${req.client.port}"
}
}
},
{
"request": {
"uri": {
"path": "/path/{path}"
}
},
"response": {
"text": {
"template": "${req.path.path}"
}
}
}
]


File: moco-runner/src/test/resources/template_with_extractor.json

[
{
"request": {
"uri": "/extractor_template"
},
"response": {
"text": {
"template": {
"with" : "${foo}",
"vars" : {
"foo" : {
"json_path": "$.book.price"
}
}
}
}
}
}
]


File: moco-runner/src/test/resources/template_with_function.json

[
{
"request": {
"uri": "/now_template"
},
"response": {
"text": {
"template": "${now("yyyy-MM-dd")}"
}
}
},
{
"request": {
"uri": "/random_template_with_range_and_format"
},
"response": {
"text": {
"template": "${random(100, "###.######")}"
}
}
},
{
"request": {
"uri": "/random_template_with_range"
},
"response": {
"text": {
"template": "${random(100)}"
}
}
},
{
"request": {
"uri": "/random_template_with_format"
},
"response": {
"text": {
"template": "${random("###.######")}"
}
}
},
{
"request": {
"uri": "/random_template_without_arg"
},
"response": {
"text": {
"template": "${random()}"
}
}
}
]


File: moco-runner/src/test/resources/template_with_var.json

[
{
"request": {
"uri": "/var_template"
},
"response": {
"text": {
"template": {
"with" : "${foo} ${bar}",
"vars" : {
"foo" : "another",
"bar" : "template"
}
}
}
}
}
]


File: moco-runner/src/test/resources/xml.json

[
{
"request": {
"uri": "/xml",
"text": {
"xml": "1"
}
},
"response": {
"text": "response_for_xml_request"
}
},
{
"request": {
"uri": "/xmlfile",
"file": {
"xml": "src/test/resources/xml.xml"
}
},
"response": {
"text": "response_for_xml_file_request"
}
}
]


File: moco-runner/src/test/resources/xml_struct.json

[
{
"request": {
"uri": "/struct-xml",
"struct": {
"xml": "1"
}
},
"response": {
"text": "response_for_xml_struct_request"
}
}
]


File: moco-runner/src/test/resources/xpath.json

[
{
"request" :
{
"uri" : "/xpath",
"xpaths" : {
"/request/parameters/id/text()" : "1"
}
},
"response" :
{
"text" : "response_for_xpath_request"
}
}
]


File: moco-runner/src/test/resources/rest/rest.json

[
{
"resource": {
"name": "targets",
"get": [
{
"id": "1",
"request": {
"headers": {
"Content-Type": "application/json"
}
},
"response": {
"json": {
"code": 1,
"message": "foo"
}
}
},
{
"id": "2",
"response": {
"json": {
"code": 2,
"message": "bar"
}
}
}
],
"post": [
{
"response": {
"status": 201,
"headers": {
"Location": "/targets/123"
}
}
}
],
"put": [
{
"id": "1",
"response": {
"status": 200
}
}
],
"delete": [
{
"id": "1",
"response": {
"status": 200
}
}
],
"head": [
{
"id": "1",
"response": {
"status": 200,
"headers": {
"ETag": "Moco"
}
}
}
],
"patch": [
{
"id": "1",
"response": {
"text": "patch result"
}
}
],
"resource": [
{
"id": "1",
"name": "subs",
"get": [
{
"id": "1",
"response": {
"json": {
"code": 3,
"message": "sub"
}
}
}
],
"resource": [
{
"id": "1",
"name": "sub-subs",
"get": [
{
"id": "1",
"response": {
"json": {
"code": 4,
"message": "sub-sub"
}
}
}
]
}
]
},
{
"id": "",
"name": "any-subs",
"get": [
{
"id": "1",
"response": {
"json": {
"code": 4,
"message": "any-sub"
}
}
}
]
}
]
}
},
{
"resource": {
"name": "any-targets",
"get": [
{
"id": "
",
"request": {
"headers": {
"Content-Type": "application/json"
}
},
"response": {
"json": {
"code": 1,
"message": "any"
}
}
}
],
"resource": [
{
"id": "",
"name": "any-subs",
"get": [
{
"id": "
",
"response": {
"json": {
"code": 100,
"message": "any-sub"
}
}
}
]
}
]
}
},
{
"resource": {
"name": "all-resources",
"get": [
{
"response": {
"json": [
{
"code": 1000,
"message": "all-1"
},
{
"code": 100,
"message": "all-2"
}
]
}
}
],
"head": [
{
"response": {
"headers" : {
"ETag": "Moco"
}
}
}
]
}
}
]


File: moco-runner/src/test/resources/rest/rest_error_without_delete_id.json

[
{
"resource" : {
"name" : "error",
"delete": [
{
"response": {
"status": 200
}
}
]
}
}
]


File: moco-runner/src/test/resources/rest/rest_error_without_patch_id.json

[
{
"resource" : {
"name" : "error",
"patch": [
{
"response": {
"status": 200
}
}
]
}
}
]


File: moco-runner/src/test/resources/rest/rest_error_without_put_id.json

[
{
"resource" : {
"name" : "error",
"put": [
{
"response": {
"status": 200
}
}
]
}
}
]


File: moco-runner/src/test/resources/rest/rest_error_without_response.json

[
{
"resource": {
"name": "targets",
"get": [
{
"id": "1"
}
]
}
}
]


File: moco-runner/src/test/resources/settings/context-settings.json

[
{
"context": "/foo",
"include": "src/test/resources/settings/details/foo.json"
},
{
"context": "/bar",
"include": "src/test/resources/settings/details/bar.json"
}
]


File: moco-runner/src/test/resources/settings/env-settings.json

[
{
"context": "/foo",
"env": "foo",
"include": "src/test/resources/settings/details/foo.json"
},
{
"context": "/bar",
"env": "bar",
"include": "src/test/resources/settings/details/bar.json"
}
]


File: moco-runner/src/test/resources/settings/fileroot-settings.json

[
{
"context": "/fileroot",
"file_root": "src/test/resources",
"include": "settings/fileroot.json"
}
]


File: moco-runner/src/test/resources/settings/fileroot.json

[
{
"request" : {
"uri" : "/fileroot"
},
"response" : {
"file" : "foo.response"
}
}
]


File: moco-runner/src/test/resources/settings/glob-settings.json

[
{
"include" : "src/test/resources/settings/details/*.json"
}
]


File: moco-runner/src/test/resources/settings/request-settings.json

[
{
"request" : {
"headers" : {
"foo" : "bar"
}
},
"include": "src/test/resources/settings/details/foo.json"
}
]


File: moco-runner/src/test/resources/settings/response-settings.json

[
{
"response" : {
"headers" : {
"foo" : "bar"
}
},
"include": "src/test/resources/settings/details/foo.json"
}
]


File: moco-runner/src/test/resources/settings/settings.json

[
{
"include" : "src/test/resources/settings/details/foo.json"
},
{
"include" : "src/test/resources/settings/details/bar.json"
}
]


File: moco-runner/src/test/resources/settings/details/bar.json

[
{
"request" : {
"uri" : "/bar"
},
"response" : {
"text" : "bar"
}
}
]


File: moco-runner/src/test/resources/settings/details/foo.json

[
{
"request" : {
"uri" : "/foo"
},
"response" : {
"text" : "foo"
}
}
]


File: moco-runner/src/test/resources/websocket/websocket.json

[
{
"websocket": {
"uri": "/ws",
"connected": "connected",
"pingpongs": [
{
"ping": "ping",
"pong": "pong"
}
],
"sessions": [
{
"request": {
"text": "foo"
},
"response": {
"text": "bar"
}
},
{
"request": {
"file": "src/test/resources/foo.request"
},
"response": {
"file": "src/test/resources/foo.response"
}
}
]
}
}
]


File: moco-runner/src/test/resources/websocket/websocket_with_broadcast.json

[
{
"websocket": {
"uri": "/ws",
"sessions": [
{
"request": {
"text": "foo"
},
"response": {
"text": "bar"
}
},
{
"request": "broadcast",
"response": {
"text": "subcribed",
"broadcast": {
"content": "login"
}
}
},
{
"request": "broadcast2",
"response": {
"text": "broadcast2",
"broadcast": {
"content": "login2"
}
}
},
{
"request": "broadcast-with-file",
"response": {
"text": "broadcast-with-file",
"broadcast": {
"content": {
"file": "src/test/resources/foo.response"
}
}
}
},
{
"request": "subscribe-with-group",
"response": {
"text": "subscribed",
"group": "group"
}
},
{
"request": "broadcast-with-group",
"response": {
"text": "subscribed",
"broadcast": {
"group" : "group",
"content": "broadcast-content"
}
}
}
]
}
}
]


File: .github/workflows/build.yaml

name: CI

on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]

jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
java-version: ["17", "21", "25"]

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

  - name: Setup Java ${{ matrix.java-version }}
    uses: actions/setup-java@v4
    with:
      distribution: 'oracle'
      java-version: '${{ matrix.java-version }}'

  - name: Setup Gradle
    uses: gradle/actions/setup-gradle@v4

  - name: Run checks
    run: ./gradlew check -i

  - name: Run ProGuard check
    run: ./gradlew :moco-runner:proguardCheck -i

  - name: Upload test reports
    if: failure()
    uses: actions/upload-artifact@v4
    with:
      name: test-reports-java-${{ matrix.java-version }}
      path: |
        **/build/reports/tests/
        **/build/test-results/