Code Monkey home page Code Monkey logo

cumulocity-cypress's Introduction

Cypress commands for Cumulocity

Collection of commands and utilities to be used for automating tests for Cumulocity IoT with Cypress. Also use the repository to discuss questions and issues when testing your Cumulocity IoT applications with Cypress.

Contribute by raising pull requests. All commands must be documented and, if possible, tested using test suite in this repository.

Content

Overview of commands

Current set of commands include

General commands

  • visitAndWaitForSelector
  • setLanguage
  • hideCookieBanner
  • disableGainsights

Authentication related commands

  • login
  • setAuth
  • useAuth
  • bootstrapDeviceCredentials

Date related commands

  • toDate
  • toISODate
  • compareDates

Administration related commands

  • getCurrentTenant and getTenantId
  • createUser and deleteUser
  • assignUserRoles and clearUserRoles
  • getSystemVersion

Integration and API testing related commands

  • c8yclient, c8yclientf
  • c8ymatch
  • retryRequest

See Integration and API testing for more information.

Installation and setup

Add dependency to your package.json.

npm install cumulocity-cypress --save-dev
yarn add -D cumulocity-cypress

You also need to have @c8y/client installed and make it available within the tested project as cumulocity-cypress defines @c8y/client as a peer-dependency. This is to ensure the version of @c8y/client to be used is the same as in the hosted test project.

Install @c8y/client if needed using

npm install @c8y/client --save-dev
yarn add -D @c8y/client

Configure Cumulocity authentication. Easiest way to configure authentication is to create cypress.env.json file in your project and add all credentials needed for the tests, for example with different permissions or roles.

{
  "admin_username": "admin",
  "admin_password": "password",
  "noaccess_username": "noaccess",
  "noaccess_password": "password"
}

Update your projects e2e.supportFile (e.g. cypress/support/e2e.ts) to make custom commands available to your tests.

Import all commands:

import "cumulocity-cypress/lib/commands/";

Import selected commands:

import "cumulocity-cypress/lib/commands/request";

See API and Integration Testing for more information on how to enable recording and matching of requests and responses using cy.c8yclient and cy.intercept.

With this, also in the support file of your Cypress project, you can init environment variables as for example with:

before(() => {
  cy.getAuth("admin")
    .getCurrentTenant()
    .then((tenant) => {
      Cypress.env("C8Y_TENANT", tenant.body.name);
      Cypress.env(
        "C8Y_INSTANCE",
        tenant.body.domainName?.split(".").slice(1).join(".")
      );
    })
    .then(() => {
      expect(Cypress.env("C8Y_TENANT")).not.be.undefined;
      expect(Cypress.env("C8Y_INSTANCE")).not.be.undefined;
    });
});

Additional frameworks

Other frameworks that might help improve efficiency, quality and reliability of Cypress tests include:

Concepts

To use the custom commands provided in this library across different projects, it comes with some concepts to allow more flexible use.

The most important use case has been accessing credentials for authentication, as probably every project uses a different approach to pass credentials into it's tests.

Authentication and credentials

This library supports different ways to configure authentication and credentials in your tests. The getAuth and useAuth commands create or read authentication options from environment and pass or configure it for given commands or the entire test.

Within this library, all commands must use and support authentication based on C8yAuthOptions. C8yAuthOptions are compatible with authentication options used in

Authentication via getAuth and useAuth commands

For accessing authentication credentials, the getAuth() and useAuth() commands are provided. Both accept any arguments and create, if possible, a C8yAuthOptions object.

// get auth options from Cypress env variables, test annotation or cy.useAuth()
cy.getAuth();

// user "admin" and password from "admin_password" Cypress env variable
cy.getAuth("admin");

// user and password given as strings
cy.getAuth("admin", "password");

// use C8yAuthOptions (for chaining commands)
cy.getAuth({ user: "admin", password: "password" });

getAuth() and useAuth() support chaining by accepting arguments as previous subjects. All commands requiring authentication should accept C8yAuthOptions object as it's first (previous subject) argument.

// without chaining
cy.getAuth().then((auth) => {
  cy.createUser(auth, "newuser");
});

// with chaining
cy.getAuth().createUser("newuser");

With useAuth() the C8yAuthOptions object will be available for all commands within the scope of the test. Use if there is more than one command requiring authentication or if not all commands in a chain yield auth options.

cy.useAuth("admin");

cy.deleteUser("newuser");
cy.createUser({
  userName: "newuser",
  password: "newpassword",
  email: "[email protected]",
  displayName: "New User",
});
cy.getApplicationsByName("OEE").subscribeApplications("newuser");

// using getAuth() to pass auth options into login will override the
// authentication options configured via cy.useAuth()
cy.getAuth("newuser").login();

Authentication via test case annotations

Instead of calling useAuth(), it is also possible to annotate the test with authentication options.

it(
  "my test requiring authentication",
  { auth: { user: "myadmin", password: "mypassword" } },
  () => {
    // commands will use auth passed from annotation
  }
);

it("another test requiring authentication", { auth: "myadmin" }, () => {
  // commands will use auth from annotation with password from env variable
});

Authentication via environment variables

To provide authentication options into all tests, use C8Y_USERNAME and C8Y_PASSWORD env variables. Set env variables in your tests or use one of the ways descibed in Cypress documentation.

Example for setting environment variables in your tests:

Cypress.env("C8Y_USERNAME", "admin");
Cypress.env("C8Y_PASSWORD", "password");

Passing authentication to cy.request

With import "cumulocity-cypress/lib/commands/request", it is also possible to add authentication support to cy.request() command. If enabled, cy.request() will use authentication from environment, useAuth() and test case auth annotation. As this feature is considered experimental, it is not automatically imported.

Note: chaining authentication into cy.request() is not supported as cy.request() does not support previous subject and always is a parent in the Cypress chain.

Note: in order to work, add the import before any other imports (not related to this library) in your support file. This is required if cy.request() is overwritten. If any cy.overwrite("request", ...) is called after the import, cy.request() will not automatically use the authentication.

it(
  "use request with authentication from test annotation",
  { auth: "admin" },
  function () {
    cy.request({
      method: "GET",
      url: "/path/to/some/resource",
    }).then((response) => {
      // do something
    });
  }
);

// same as
it("standard request authentication", function () {
  cy.request({
    method: "GET",
    url: "/path/to/some/resource",
    auth: { user: "admin", password: "password" },
  }).then((response) => {
    // do something
  });
});

Chaining of commands

Custom commands provided by this library should support chaining. This means commands need to accept previousSubject and yield it's result for next command in the chain.

Instead of having one command with a lot of arguments, chaining allows splitting into multiple commands.

cy.getAuth("admin", "password").login();
cy.wrap("admin").getAuth().login();

c8y/client and Web SDK types

In general, all custom commands should use c8y/client type definitions working with Cumulocity API.

To interact with Cumulocity REST endpoints, cy.c8yclient custom command is provided. cy.c8yclient mimics cy.request to easily exchange or replace cy.request within your tests. For compatibility, the yielded result of cy.c8yclient is a Cypress.Response<T> (as used by cy.request) to make all assertions work as expected for cy.request and cy.c8yclient.

See API and Integration Testing for more information.

Development

Debugging

Debugging Cypress tests is tricky. To help debugging custom commands, this library comes with needed setup for debugging in Cypress.

Console log debugging

All custom commands of this library are logged within the Command Log of the Cypress App. By clicking the logged command in Command Log of Cypress App, extra information are printed to the console for debugging. Extra information should include at least subject as well as the value yielded by the command. Every command can add any additional information.

Use consoleProps object to pass additional information as in the following example.

// get $args from arguments passed to the command
const [auth, userOptions] = $args;
const consoleProps = {
  // include authentication and user options passed as arguments
  auth: auth,
  userOptions: userOptions,
};

Cypress.log({
  name: "createUser",
  // custom message (title) shown in command log
  message: userOptions.userName,
  consoleProps: () => consoleProps,
});

// do something

cy.request({
  method: "POST",
  url: "/user/" + tenant.name + "/users",
  auth: auth,
  body: userOptions,
}).should((response) => {
  // add more details to console props
  consoleProps.response = response;
});

When adding extra information to the log, keep overall object size in mind. You might run out of memory in case of extensive logging with many large command log entries.

See Get console log for commands from Cypress documentation for more details.

Debugging in Visual Studio Code

Debugging in Visual Studio Code is not very straight forward, but after all it is or should be possible. The project does contain the launch configuration Attach to Chrome, wich requires the Cypress app to be started with npm run test:open.

Once Cypress App has been started, select run and debug Attach to Chrome launch configuration and restart your test(s) in the Cypress App using Rerun all tests.

Create breakpoints for Cypress commands using debugger statement.

it.only("debugging test", () => {
  debugger;
  // to debug getCurrentTenant(), place another debugger statement
  // in the implementation of getCurrentTenant()
  cy.getAuth("admin")
    .getCurrentTenant()
    .then((tenant) => {
      // to debug result of getCurrentTenant, place debugger in then()
      debugger;
      expect(tenant.name).to.equal("mytenant");
      expect(Cypress.env("C8Y_TENANT")).to.deep.equal({ name: "mytenant" });
    });
});

For more information see Debug just like you always do in the official Cypress documentation.

Testing

Cypress is used for testing commands. All tests a located in test/cypress folder. If needed, add HTML fixtures in test/cypress/app/ folder.

Run tests using

npm run test

or with opening the Cypress console

npm run test:open

Test access of DOM elements

tbd.

Test requests

Testing requests and the processing of it's responses a set of utilities is provided by this library.

// can be called in beforeEach() hook
initRequestStub();

stubResponse<ICurrentTenant>({
  isOkStatusCode: true,
  status: 200,
  body: { name: "mytenant" },
});

cy.getAuth("admin")
  .getCurrentTenant()
  .then((tenant) => {
    expectHttpRequest({
      url: url(`/tenant/currentTenant`),
      method: "GET",
      auth: { user: "admin", password: "password" },
    });
    expect(tenant.name).to.equal("mytenant");
    expect(Cypress.env("C8Y_TENANT")).to.deep.equal({ name: "mytenant" });
  });

Test interceptions

Interceptions are a very important concept to test with stubbed network responses. If custom commands use interceptions, it can be easily triggered using JQueryStatic provided by Cypress.

$.get(url(`/tenant/currentTenant.json`));

If interceptions do not just stub a response, but modify the response from server, mock the service with fixtures in app folder. You might need to append an extension to the endpoint to get the right content type however.

const { $ } = Cypress;

cy.disableGainsight()
  .as("interception")
  .then(() => {
    return $.get(url(`/tenant/currentTenant.json`));
  })
  .then((response) => {
    expect(response.customProperties.gainsightEnabled).to.eq(false);
  })
  .wait("@interception");

Useful links

๐Ÿ“˜ Explore the Knowledge Base
Dive into a wealth of Cumulocity IoT tutorials and articles in our Tech Community Knowledge Base.

๐Ÿ’ก Get Expert Answers
Stuck or just curious? Ask the Cumulocity IoT experts directly on our Forum.

๐Ÿš€ Try Cumulocity IoT
See Cumulocity IoT in action with a Free Trial.

โœ๏ธ Share Your Feedback
Your input drives our innovation. If you find a bug, please create an issue in the repository. If youโ€™d like to share your ideas or feedback, please post them here.

More to discover

Disclaimer

These tools are provided as-is and without warranty or support. They do not constitute part of the Software AG product suite. Users are free to use, fork and modify them, subject to the license agreement. While Software AG welcomes contributions, we cannot guarantee to include every contribution in the master project.

cumulocity-cypress's People

Contributors

cguether avatar dependabot[bot] avatar hnaether-sag avatar techcommunity avatar thomaswinkler avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

cumulocity-cypress's Issues

Separate c8ypact cypress and node js code

Currently c8ypact is fully implemented assuming it's running in a Cypress context. However, some parts might need to run as well in nodejs. Move nodejs code to some shared/ src folder to be used by components.

Examples could be a mocking server or a custom implementation of FetchClient.

Improve logging of user for c8yclient commands

As c8y/client always adds the XSFR-TOKEN if the cookie is available, the user logged in via cy.login might be used instead of the basic auth user passed via cy.getAuth. Also errors are thrown before Cypress.log actually logs the username used to the console.

Logging should include all auth and cookie users and highlight the user being used by c8y/client.

Fix cy.intercept to pass mocked response to req.continue()

cy.intercept does return the mocked response, but does not allow the user to change the mocked response in a custom req.continue() function.

cy.intercept("/inventory/managedObjects*", (req) => {
  req.continue((res) => {
    res.body.test = "test2";
    res.statusCode = 222;
    res.send();
  });
})

Changes provided in the continue() must be applied to the response and passed by the interception.

Deal with case sensitive header names

cy.c8yclient and cy.intercept might record headers with different case, such as X-XSRF-TOKEN and x-xsrf-token. To make records always work, headers need to be treated case insensitive. Same for preprocessor paths, etc.

All lowercase recording?

Add mocking and widget utils

  • add utility methods where devs can easily mock ManagedObjects, Measurements, Events, Alarms to be used e.f. for interceptions
  • add utility methods for widget testing
  • add sample widget test which completely mocks away the device/ group used, the widget and dashboard

Issues in c8yclient contract testing

Remaining issues from initial c8ypact contract testing (#21):

  • check default adding content-type get requests
    • remove default or add only for post/put
  • add date() to C8yPactRecord
  • store strict matching in pact info and use in runner
  • c8ypact debug log -> false
  • test:open script for runner
  • auth needs cookie type if logged in via login()

Make c8yclient command timeout configurable

Currently cy.c8yclient is treated like an ordinary cy command. This leads to the problem that requests being run via c8yclient could time out easily as the (globally configurable) defaultCommandTimeout is 4s. As user of the framework I want to be able to just tweak the timeout for requests being made by cy.c8yclient without causing a potential test delay for all other cy commands.

=> Don't use defaultCommandTimeout for c8yclient but rather the requestTimeout/responseTimeout. See https://docs.cypress.io/guides/references/configuration

Fix c8yclient authentication error

If c8yclient authentication fails in authenticateClient, the error is not mapped to CypressError. This results in wrong and unreadable error output.

Error
the object {
  "codeFrame": {
    "absoluteFile": "/Users/twi/Projects/oee/oee-ui/node_modules/@iota/cumulocity-cypress-shared-dev/lib/commands/c8yclient.js"
    "column": 1
    "frame": "  229 | // Cookie authentication. Client.authenticate() only works with BasicAuth\n  230 | function authenticateClient(auth, baseUrl) {\n> 231 |   return cy.then(async () => {\n      | ^\n  232 |     const clientCore = new FetchClient(auth, baseUrl);\n  233 |     clientCore.defaultHeaders = {\n  234 |       "content-type": "application/json","
    "language": "js"
    "line": 231
    "originalFile": "node_modules/@iota/cumulocity-cypress-shared-dev/lib/commands/c8yclient.js"
    "relativeFile": "node_modules/@iota/cumulocity-cypress-shared-dev/lib/commands/c8yclient.js"
  }

  "isCyFailErr": true
  "isPending": true
  "name": "CypressError"
  "parsedStack": [
    {
      "message": "[object Object]"
      "whitespace": ""
    }
    {
      "message": "From Your Spec Code:"
      "whitespace": ""
    }
    {
      "absoluteFile": "/Users/twi/Projects/oee/oee-ui/node_modules/@iota/cumulocity-cypress-shared-dev/lib/commands/c8yclient.js"
      "column": 0
      "fileUrl": "http://localhost:9000/__cypress/tests?p=cypress/support/e2e.ts"
      "function": "authenticateClient"
      "line": 231
      "originalFile": "webpack://oee/./node_modules/@iota/cumulocity-cypress-shared-dev/lib/commands/c8yclient.js"
      "relativeFile": "node_modules/@iota/cumulocity-cypress-shared-dev/lib/commands/c8yclient.js"
      "whitespace": "    "
    }
    {
      "absoluteFile": "/Users/twi/Projects/oee/oee-ui/node_modules/@iota/cumulocity-cypress-shared-dev/lib/commands/c8yclient.js"
      "column": 0
      "fileUrl": "http://localhost:9000/__cypress/tests?p=cypress/support/e2e.ts"
      "function": "Context.eval"
      "line": 203
      "originalFile": "webpack://oee/./node_modules/@iota/cumulocity-cypress-shared-dev/lib/commands/c8yclient.js"
      "relativeFile": "node_modules/@iota/cumulocity-cypress-shared-dev/lib/commands/c8yclient.js"
      "whitespace": "    "
    }
  ]
  "res": "[object Response]"
  "stack": "[object Object]\nFrom Your Spec Code:\n    at authenticateClient (webpack://oee/./node_modules/@iota/cumulocity-cypress-shared-dev/lib/commands/c8yclient.js:231:0)\n    at Context.eval (webpack://oee/./node_modules/@iota/cumulocity-cypress-shared-dev/lib/commands/c8yclient.js:203:0)"
} was thrown, throw an Error :)

Because this error occurred during a before all hook we are skipping all of the remaining tests.

Record pacts for cy.intercept

Currently only cy.c8yclient stores recording of requests and responses. In recording mode, we also want to store requests and responses for interceptions. With this, we can create contract tests from e2e tests and we could provide self updating mocks for e2e tests.

Allow schema based pact matching

Instead of matching responses, enable matching against a JSON schema. Provide schema per pact record. Check if possible to automatically create schema from response.

Schema definition for recording of pacts

Allow configuration of schemas to use based on urls or content type. Before generating a schema from body, the schema to use should be looked up in extern configuration file.

Fix build scripts to work on Windows

Scripts in package.json use $npm_execpath to use the package manager binary in the script that has been used to run the script. Initially the idea was to support npm and yarn.

$npm_execpath does not work on Windows.

Make c8yclient and c8ypact separate imports

Currently both, c8yclient and c8ypact are imported automatically when importing commands. Remove from import in index and have users import it separately as already with request.

Enable pact recording and mocking for component tests

For component testing, there is some changes required to enable pact recording and mocking.

cy.intercept does work for component testing, but there is a few improvements required for more convenient use. Also, we need to provide a custom FetchClient for more convenient use of authentication and baseUrl.

Fix wrong use of _.isObject

Fix use of _.isObject by rather using _.isObjectLike. Also make sure to use "property" in object for type guarding. Seems this has been used wrong in few locations.

Add logging for pact matching

There is currently only Cypress debug logging enabled. Add logging to file including detailed information on failing matches.

Requires file logging via plugin.

Mock server from recorded pacts

Have a docker based server mocking the backend based on recorded pacts. Use for example express to create a server responding to requests with matching recorded response.

Update Cypress to 13.x

Currently repository is still with 12.8.1. Update to resolve dependency security alerts.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.