c8yctrl
is a subpackage of cumulocity-cypress providing a HTTP controller that allows serving static builds of Web SDK based applications or plugins and proxying API requests to Cumulocity IoT. As an intermediary service, c8yctrl
can record and mock proxied requests and with this allow running tests without requiring a Cumulocity IoT backend.
Use c8yctrl
for
- testing shell and remotes without deploying them to Cumulocity IoT
- offline testing by recording and mocking API requests
- e2e, component and microservice unit testing
c8yctrl
does not require using cumulocity-cypress
in your e2e or component tests. However, cumulocity-cypress
integrates with c8yctrl
without the need for any custom implementation or configuration in your tests.
Install c8yctrl
as a dev dependency in your project
npm install cumulocity-cypress-ctrl --save-dev
or install globally
npm install -g cumulocity-cypress-ctrl
As cumulocity-cypress
is not a direct dependency of c8yctrl
, you need to install it separately if you want to use it in your tests.
npm install cumulocity-cypress --save-dev
c8yctrl
is designed as an intermediary service, a middleware, in between your test framework and a Cumulocity IoT tenant. To use your tests with c8yctrl
, you need to configure your tests to not directly access a Cumulocity IoT tenant, but instead use the c8yctrl
service as the baseUrl for all requests. This is the same for E2E tests and possibly component tests, but also for microservices that require or access a Cumulocity IoT tenant.
For Cypress E2E tests, use the cypress.config.ts file to configure the baseUrl for the tests:
import { defineConfig } from 'cypress'
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:8181',
},
})
A microservice .property file could configugure the baseUrl for c8yctrl
be the management tenant of the Cumulocity IoT instance you want to test against. Authentication is not required, as the microservice will take care of the authenticating.
application.name=demo-microservice
application.key=demo-microservice
C8Y.bootstrap.register=true
C8Y.bootstrap.tenant=management
C8Y.baseURL=http://localhost:8181
C8Y.bootstrap.user=servicebootstrap_testuser
C8Y.bootstrap.password=secret
C8Y.microservice.isolation=MULTI_TENANT
To start c8yctrl
at least the baseUrl of the Cumulocity IoT tenant must be provided. All other options, as for example the port the service needs to listen is optional and does come with defaults. The following command would for example start the controller on port 8181
with a given baseUrl.
npx c8yctrl --baseUrl https://abc.eu-latest.cumulocity.com --port 8181
For all configuration options see the Configuration section.
One of the main use cases of c8yctrl
is to serve builds of Web SDK based applications or plugins from a local folder without requiring deployment to a Cumulocity IoT tenant. This could be useful for testing compatibility of plugin and shell in development environments or with minimal requirements in CI/CD workflows. By just serving static files, it is easily possible to setup different combinations of plugin and shell versions, again, without requiring deployment to Cumulocity IoT.
To enable serving static files provide the path to folder representing the root of all files to be server using the staticRoot
configuration option. c8yctrl
will serve all files from the given folder without forwarding to baseUrl if the request path matches a file in the folder. To serve multiple plugins and even multiple shells, just copy or unzip all files of the shells and plugins to the root folder.
A folder structure could look like this:
/path/to/my/root
/app
/plugin1
/plugin2
/shell1
/shell2
When starting c8yctrl
with the following command, all files from the /path/to/my/root
folder will be served as static files and are not proxied to the configured baseUrl.
npx c8yctrl --baseUrl https://abc.eu-latest.cumulocity.com --port 8181 --staticRoot /path/to/my/root
As an intermediary service in between your tests and a Cumulocity IoT tenant, c8yctrl
can record and mock requests and responses. Recording of requests and responses allows to easily create a set of mocks for your tests, without creating or maintaining the mocks manually. By enabling recording mode all requests are proxied to the configured Cumulocity IoT tenant and requests, responses as well as some metadata are stored automatically via a configurable C8yPactFileAdapter
implementation. As a default implementation, C8yPactDefaultFileAdapter
stores the recordings as JSON files of C8yPact
format in a configured folder.
To mock responses, c8yctrl
will use the existing recordings and return the recorded response if it matches the request. If no recording is available, c8yctrl
will return an error response or if strictMocking
is disabled, forward the request to the configured Cumulocity IoT tenant.
There is different ways to enable recording and mocking. When starting c8yctrl
, use the --recording
option with value true
or the C8Y_PACT_MODE
environment variable with value recording
to enable recording of all requests and responses.
Another option is to use the REST interface of c8yctrl
to enable recording and mocking at runtime. This is meant to change the recording mode in between your tests and allows to run tests in your suites with different recording modes. See the Rest Interface section for more details.
Note: By default, recorded requests will be appended to the existing recording. If you want to clear the existing recordings before enabling recording mode, use the
clear
parameter in the REST interface or or remove recoded files.
c8yctrl
can be configured using environment variables, command line arguments or a configuration file. Environment variables and command line arguments have the highest priority and allow configuration of options required to start the HTTP controller. This is useful for CI/CD pipelines or running the controller in a container and overwriting the configuration at runtime.
The section on Configuration File provides details on custom TypeScript configuration file for more complex setups.
The following environment variables are currently supported:
C8Y_PACT_FOLDER=/path/to/my/recordings
C8Y_BASE_URL=https://abc.eu-latest.cumulocity.com
C8Y_BASE_USERNAME=myuser
C8Y_BASE_PASSWORD=mypassword
C8Y_BASE_TENANT=t123456
C8Y_STATIC_ROOT=/path/to/my/static/files
C8Y_HTTP_PORT=8181
C8Y_PACT_MODE=recording
It's recommended to use .env
or .c8yctrl
files to store environment variables. Both files will be automatically loaded by c8yctrl
.
All environment variables can be passed as command line arguments.
npx c8yctrl --folder /path/to/my/recordings \
--baseUrl https://abc.eu-latest.cumulocity.com \
--user myuser \
--password mypassword \
--tenant t123456 \
--staticRoot /path/to/my/static/files \
--port 8181 \
--recording true
Environment variables and command line arguments are mapped to the following configuration options:
-
baseUrl
- The base URL of the Cumulocity IoT tenant to proxy requests to. -
tenant
- The tenant id of the Cumulocity IoT tenant configured in thebaseUrl
. The tenant id is required for login if theusername
andpassword
are provided. -
username
- The username to authenticate with the Cumulocity IoT tenant. If authentication is provided, the controller will automatically login to the tenant and use the authorization for all requests. -
password
- The password to authenticate with the Cumulocity IoT tenant. -
folder
- The folder where recordings are stored. By providing a folder, the controller will useC8yPactDefaultFileAdapter
to store recordings in the given folder. Each file will be named by the given id of the recording. -
staticRoot
- The root folder where static files are served from. The controller will serve always serve static files if the request path matches a file in the folder. If served fromstaticRoot
, the controller will not proxy the request to thebaseUrl
. -
port
- The port the controller listens on. The default port is8181
. -
recording
- If set totrue
, the controller will record proxied requests and responses. If set tofalse
, the controller will mock responses using the existing recording without sending requests to thebaseUrl
.
The configuration file is a TypeScript file that exports a function with a single argument of type Partial<C8yPactHttpControllerConfig>
containing all configuration options provided in the environment variables and command line arguments.
Note: Options from the configuration file do not override options from environment variables or command line arguments. This is by design to allow different configurations for CI/CD pipelines.
By default, c8yctrl
looks for a file named c8yctrl.config.ts
in the current working directory. You can specify a different configuration file using the --config
command line argument.
npx c8yctrl --config /path/to/my/custom.config.ts
Sample c8yctrl.config.ts
registering a file logger with custom log format and log level. The config argument is optional and is only required if a different config file than c8yctrl.config.ts
is used.
import { C8yPactHttpControllerConfig } from 'cumulocity-cypress-ctrl';
export default (config: Partial<C8yPactHttpControllerConfig>) => {
config.logLevel = "debug";
config.preprocessor = new C8yDefaultPactPreprocessor({
obfuscate: ["request.headers.Authorization", "response.body.password"],
obfuscationPattern: "****",
});
};
For more details on available configuration options, see C8yPactHttpControllerConfig
interface.
A more complex configuration file can be found in the contributions folder.
c8yctrl
can be considered a intermediary service or middleware that tries to serve static files from a given folder and proxies all other requests to a given Cumulocity IoT tenant. As it is acting as a middleware, c8yctrl
can record and mock requests based on its current configuration.
On startup, c8yctrl
will authenticate with the Cumulocity IoT tenant configured in baseUrl
and tenant
using the provided credentials. If authentication is successful, the controller will automatically add the Authorization
header or cookies to all requests proxied to the baseUrl
. If no authentication is provided, the client must login, for example using cy.login()
as part of the test.
When receiving a request, c8yctrl
will check if the request path matches a file in the staticRoot
folder. If a file is found, the controller will serve the file and not proxy the request to the baseUrl
. If no file is found, the controller will proxy the request to the baseUrl
.
c8yctrl
provides a REST interface to configure the HTTP controller at runtime. The interface is available via /c8yctrl
and provides the following endpoints.
The /c8yctrl/current
endpoint allows creating a new recording for a give id or title. The endpoint accepts a JSON body or query parameters with the following properties:
{
"title": "Test case title",
"recording": true,
"clear": true,
"strictMocking": true
}
-
title
- The title of the test case being recorded. Anid
is generated from the title and used to store the recording. Might be an array of strings representing the suite hierarchy and test case title. -
recording
- If set totrue
, the controller will record proxied requests and responses. If set tofalse
, the controller will mock responses using the existing recording without sending requests to thebaseUrl
. -
clear
- If set totrue
, the controller will clear the existing recording before recording new following requests. -
strictMocking
- If set totrue
, the controller will send anerrorResponseRecord
if no recording is available to mock the response. If set tofalse
, the controller will forward the request tobaseUrl
(without recording the response)
Gets the current C8yPact
including its configuration (C8yPactInfo
) and number of recorded requests.
Resets the current C8yPact
. This will however not delete any data!
Sends a log message to the controller. The log message is forwarded to the logger registered in the controller. The body of the request should be a JSON object with the following properties:
{
"level": "info",
"message": "This is a log message"
}
This is useful for logging from your tests or other services that are connected to the controller.
To use c8yctrl
from Cypress tests without or an older version of cumulocity-cypress
, it is needed to configure c8yctrl
directly using its REST interface.
The following example creates new recordings for each test case and suite. The title of the recording is the test case title including all suite names in its hierarchy. Without recording suites, requests in before()
hooks might not be recorded or assigned to the wrong test case. The global before()
hook must be ha
For recording of global or suite hooks, such as before()
, use the global before()
hook to create a new recording with a custom title and
it is important to also create a recording for the global before()
hook to ensure that requests in the global hook are recorded.
before(() => {
// required if requests need to be recorded in global before () hook
cy.wrap(c8yctrl('global before hook')).then((response: Response) => {
// run requests from here as for example
cy.getAuth('admin').getCurrentTenant();
});
// create recording for all suite start events. without, requests might be
// recorded for the previous (currently active) test case.
// this will ensure for every suite start a new recording is created
const runner = Cypress.mocha.getRunner();
runner.on('suite', (suite) => c8yctrl(getSuiteTitles(suite)));
});
// create recording for each test. name for the recording will be the test title
// including all suites it its hierarchy
beforeEach(() => {
cy.wrap(c8yctrl(), { log: false });
});
function getSuiteTitles(suite) {
if (suite.parent && !_.isEmpty(suite.parent.title)) {
return [...getSuiteTitles(suite.parent), suite.title];
} else {
return [suite.title];
}
}
// sample wrapper for c8yctrl REST interface. extend as needed.
function c8yctrl(title: string | string[] = Cypress.currentTest.titlePath) {
const recording = Cypress.env('C8Y_PACT_MODE') === 'recording';
const parameter: string = recording ? '?recording=true&clear' : '?recording=false';
return (cy.state('window') as Cypress.AUTWindow).fetch(
`${Cypress.config().baseUrl}/c8yctrl/current${parameter}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title,
}),
}
);
}
- [X] document recording for before() hooks from your tests
- [X] document recording tests suite for a separate id
- [X] save pact file only when first record is added - no file for empty suites
- [X] allow multiple request logger
- [ ] allow adding custom request handlers (maybe for logging or other purposes)
- [ ] new C8Y_PACT_MODE to enable testing without recording or mocking
- [ ] avoid recording of requests that are already recorded (configurable)
- [ ] join pact ids with suite ids
- [ ] pass C8yPactConfigOptions and TestConfigOverrides to store in pact file
- [ ] store tags from test cases in recording
- [X] rename interface to /c8yctrl/current
- [ ] add support for recording realtime notifications API
- [ ] automatically reduce timeouts when mocking, e.g. in visitAndWaitForSelector
- [ ] pass auth user and alias to store in record
- [ ] allow mocking configurable requests or cypress hooks
- [X] allow passing strictMocking as parameter to /c8yctrl/current
- [X] add logging from via c8yctrl from cypress tests
- [ ] disable login via cy.session when recording
- [X] read parameters for c8yctrl from query and body
- [X] add sample config file to contributions folder
- [ ] add /c8yctrl/current/summary endpoint for statistics, incl. urls, count, etc.