Scenarios examples

A collection of examples to show how to declare Scenarios and how to use Copycat's features


HTTP examples

CRUD for User entity

Create

There are 3 Scenarios declared for the Create endpoint:

  1. A precondition that checks if the username has already been taken, if it matches, will return an error message
  2. Checks if the body contains field type with value different of 1.
  3. Success when the fields name,username and telephone are present and type equals to 1. There is a command on this scenario to save the received body on the database.

If none of these matches, returns the defaultResponse, "bad request". This Scenario also declares the optional fallbackResponse to be used if a failure happens while processing the received request.

{
  "key": {
    "protocol": "HTTP",
    "method": "POST",
    "pathTemplate": "/users"
  },
  "defaultResponse": {
    "status": "400",
    "headers": { "Content-Type": "application/json" },
    "body": {
      "message": "bad request"
    }
  },
  "fallbackResponse": {
    "status": "500",
    "headers": { "Content-Type": "application/json" },
    "body": {
      "message": "Unexpected error creating a new user"
    }
  },
  "globalVariables":{
    "validations": {
      "invalidAge": "${request.body.age < 18 ? 'Age must be > 18' : null}",
      "invalidType": "${request.body.type != 1 ? 'Invalid user type!' : null}"
    }
  },
  "scenarios": [
    {
      "name": "Use real system",
      "preConditions": [],
      "request": {
        "headers": {
          "x-real-system": true
        }
      },
      "redirect": {
        "passThrough": {
          "host": "http://real-app/users"
        }
      }
    },
    {
      "name": "Bad request",
      "preConditions": [ "${globalVariables.validations.values().stream().anyMatch(predicate((v)-> {v != null})) }" ],
      "request": {},
      "response": {
        "status": "400",
        "delay": 0,
        "headers": { "Content-Type": "application/json" },
        "body": {
          "message": "${globalVariables.validations.values().stream().filter(predicate((v)-> {v != null})).collect(Collectors.toList())}"
        }
      },
      "commands": []
    },
    {
      "name": "Fail to create user - username not available",
      "preConditions": [ "${db.getBy('users', {'username': request.body.username}) != null}" ],
      "request": {},
      "response": {
        "status": "409",
        "headers": { "Content-Type": "application/json" },
        "body": {
          "message": "${format.string('username %s is not available',request.body.username)}"
        }
      }
    },
    {
      "name": "Create new user - Success",
      "request": {
        "body": {
          "name": "#{.*}",
          "username": "#{.*}",
          "telephone": "#{.*}",
          "type": "1"
        }
      },
      "response": {
        "status": "201",
        "delay": 0,
        "headers": { "Content-Type": "application/json" },
        "body": {
          "id": "${id.seqId('users')}",
          "name": "${request.body.name}",
          "username": "${request.body.username}",
          "telephone": "${request.body.telephone}",
          "type": "${request.body.type}",
          "age": "${request.body.age}"
        }
      },
      "commands": [
        "${db.save('users', response.body.id, response.body)}"
      ]
    }
  ]
}

Retrieve

The GET by Id endpoint is declare with only one Scenario that checks for the existence of an User with the received Id on the database. If the user is present, it will be returned, otherwise, returns the defaultResponse, user not found.

{
  "key": {
    "protocol": "HTTP",
    "method": "GET",
    "pathTemplate": "/users/{idUser}"
  },
  "defaultResponse": {
    "status": "404",
    "headers": { "Content-Type": "application/json" },
    "body": {
      "message": "user not found"
    }
  },
  "globalVariables": {
    "user": "${!request.pathParams.idUser != null ? db.get('users', request.pathParams.idUser) : null}"
  },
  "scenarios": [
    {
      "name": "GET user - Success",
      "preConditions": ["${globalVariables.user != null}"],
      "request": {
        "headers": {},
        "pathParams": {}
      },
      "response": {
        "headers": { "Content-Type": "application/json" },
        "status": "200",
        "body": "${globalVariables.user}"
      },
      "commands": []
    }
  ]
}

List

The following Scenarios were added to provide more options than only a search by Id

  1. If an username is provided, finds an unique user based on the username. This Scenario uses a variable to avoid hitting the database twice.
  2. Checks if there are queryParams and if true, lists all Users that matches those params
  3. Lists all users

As the last Scenario does not have any restriction, it will always match, and, in this case, the defaultResponse will not be used. An alternative to this is remove the last Scenario and declare its response on defaultResponse.

{
  "key": {
    "protocol": "HTTP",
    "method": "GET",
    "pathTemplate": "/users"
  },
  "defaultResponse": {
    "status": "200",
    "headers": { "Content-Type": "application/json" },
    "body": {
      "body": "[]"
    }
  },
  "scenarios": [
    {
      "name": "find one user by userName - Success/Failure",
      "preConditions": [],
      "request": {
        "queryParams": {
          "username": "${request.queryParams.username != null}"
        }
      },
      "variables": {"user": "${db.getBy('users', request.queryParams)}"},
      "response": {
        "headers": { "Content-Type": "application/json" },
        "body": "${variables.user}",
        "status": "${variables.user != null ? 200 : 404}"
      },
      "commands": []
    },
    {
      "name": "Filter by parameters - Success",
      "preConditions": ["${!request.queryParams.isEmpty()}"],
      "request": {},
      "response": {
        "headers": { "Content-Type": "application/json" },
        "status": "200",
        "body": "${db.listBy('users', request.queryParams)}"
      },
      "commands": []
    },
    {
      "name": "List all Users - Success",
      "request": {},
      "response": {
        "headers": { "Content-Type": "application/json" },
        "status": "200",
        "body": "${db.list('users')}"
      },
      "commands": []
    }
  ]
}

Update

There are 3 Scenarios declared for the Update endpoint:

  1. A precondition that checks that the username is present, and in this case, the Scenario will return an error informing that this change is not allowed
  2. Checks if the body contains field type with value different of 1.
  3. Success when the fields name,username and telephone are present and type equals to 1. There is a command on this scenario to update the database with the received body.

If none of these matches, returns the defaultResponse, "bad request".

{
  "key": {
    "protocol": "HTTP",
    "method": "PUT",
    "pathTemplate": "/users/{idUser}"
  },
  "defaultResponse": {
    "status": "404",
    "headers": { "Content-Type": "application/json" },
    "body": {
      "message": "user not found"
    }
  },
  "scenarios": [
    {
      "name": "Fail to update user - cannot update username",
      "preConditions": [ "${request.body.username != null}" ],
      "request": {},
      "response": {
        "status": "409",
        "headers": { "Content-Type": "application/json" },
        "body": {
          "message": "Not allowed to update username field"
        }
      },
      "commands": []
    },
    {
      "name": "Bad request - Invalid type",
      "request": {
        "body": {
          "type": "${request.body.type != 1}"
        }
      },
      "response": {
        "status": "400",
        "delay": 0,
        "headers": { "Content-Type": "application/json" },
        "body": {
          "message": "Invalid user type!",
          "type": "${request.body.type}"
        }
      },
      "commands": []
    },
    {
      "name": "Update user - Success",
      "preConditions": [ "${db.exists('users', request.pathParams.idUser)}" ],
      "request": {
        "body": {
          "name": "#{.*}",
          "telephone": "#{.*}",
          "type": "1"
        }
      },
      "response": {
        "status": "200",
        "delay": 0,
        "headers": { "Content-Type": "application/json" },
        "body": "${db.update('users', request.pathParams.idUser, request.body)}"
      },
      "commands": []
    }
  ]
}

Delete

The DELETE by Id endpoint is declare with only one Scenario that checks for the existence of an User with the received Id on the database. If the user is present, it will be permanently deleted, otherwise, returns the defaultResponse, user not found.

{
  "key": {
    "protocol": "HTTP",
    "method": "DELETE",
    "pathTemplate": "/users/{idUser}"
  },
  "defaultResponse": {
    "status": "404",
    "headers": { "Content-Type": "application/json" },
    "body": {
      "message": "user not found"
    }
  },
  "scenarios": [
    {
      "name": "DELETE user - Success",
      "preConditions": [
        "${db.exists('users', request.pathParams.idUser)}"
      ],
      "request": {},
      "response": {
        "delay": 1000,
        "headers": { "Content-Type": "application/json" },
        "status": "204"
      },
      "commands": [
        "${db.delete('users', request.pathParams.idUser)}"
      ]
    }
  ]
}

ISO-8583 examples

Note

Remember that ISO-8583 requires a PortMapping that explains to Copycat how to interpret the received message. All mti used on the Scenario must be on this mapping.

This example adds two Scenarios to the mti 0100, Authorization Request.

  1. Checks if the field amount on the received body is greater than 10000. If true, returns a 0110 message with the error code 16 (Insufficient funds)
  2. Returns the mti 0110 with the success code (00) if the processingCode and stan are positive values and the terminal is T001. This Scenario also declares a callback that will send an 0422 message (Reversal advice) after 1000 ms using the same connection that received the original message.

If no Scenario matches, the defaultResponse will be returned, a 0110 message with error code 06 (General error)

Note

This is an ISO-8583 Scenario example with an ISO-8583 callback, but remember that the callback may be of a different type, for instance, it may be an ISO-8583 Scenario with an HTTP callback.

Authorization Request

{
  "key": {
    "protocol": "ISO8583",
    "mti": "0100",
    "resourceName": "xpto"
  },
  "defaultResponse": {
    "mti": "0110",
    "body": {
      "processingCode": "${request.body.processinCode}",
      "stan": "${request.body.stan}",
      "responseCode": "06",
      "terminal": "${request.body.terminal}",
      "cardAcceptor": "${request.body.cardAcceptor}"
    }
  },
  "scenarios": [
    {
      "name": "Insufficient funds",
      "request": {
        "body": {
          "amount": "${request.body.amount > 10000}"
        }
      },
      "response": {
        "mti": "0110",
        "delay": 1000,
        "body": {
          "processingCode": "${request.body.processinCode}",
          "stan": "${request.body.stan}",
          "responseCode": "16",
          "terminal": "${request.body.terminal}",
          "cardAcceptor": "${request.body.cardAcceptor}"
        }
      },
      "commands": []
    },
    {
      "name": "Success - Terminal T0000001",
      "request": {
        "body": {
          "terminal": "T0000001"
        }
      },
      "response": {
        "mti": "0110",
        "body": {
          "processingCode": "${request.body.processinCode}",
          "stan": "${request.body.stan}",
          "responseCode": "00",
          "terminal": "${request.body.terminal}",
          "cardAcceptor": "${request.body.cardAcceptor}"
        }
      },
      "callback": {
        "type": "ISO8583",
        "typeProperties": {
          "connectionId": "${request.connectionId}",
          "mti": "0422"
        },
        "payload": {
          "processingCode": "${request.body.processinCode}",
          "stan": "${request.body.stan}",
          "terminal": "${request.body.terminal}",
          "cardAcceptor": "${request.body.cardAcceptor}"
        },
        "delay": 1000
      },
      "commands": []
    }
  ]
}