# Middlewares

Sometimes, you may need to perform some tasks, such as altering the traffic that is captured by Hypertest. This can be done programmatically via "Middlewares" which are in simple terms, functions that a user can write in order to implement a custom functionality.

# Using middlewares

middlewares

A middleware section contains the following parts:

  • Middleware Name
  • Description
  • Sample data contains test data on which the middleware provided would be run, in order to verify its correctness.
  • Input globals is the list of variables globally available in the middleware body
  • Code contains the actual middleware code, it is here that the middleware is written

TIP

After writing your middleware always use the check output function to verify the middleware.

# Middleware Types

Following is a detailed explanation of all the middlewares that Hypertest provides:

  • REQUEST_LOGGER: Changes request parameter in newly logged requests based on the parameters provided to it.

  • SESSION_DIFFERENTIATOR: Used when the session differentiator value needs to figured out programmatically. For example, sorting incoming requests into groups; those that have a specific key in header are grouped together, those that have same ip are grouped together and so on.

  • PRE_REQUEST: Allows altering request object before making request to you application.

  • POST_RESPONSE: Updates the session variables for a particular request.

  • PRE_SESSION: Updates the session variables at the start of a test(User Session).

# Common Use Cases

# Modifying a captured request before logging it to the database (REQUEST LOGGER)

Suppose you want to strip the base API path coming from the kubernetes ingress. (because it's specified in PRIMARY_BASE_URL and CANDIDATE_BASE_URL). We could easily do this with the use of REQUEST_LOGGER middleware.

You would be provided with standard request attributes such as headers, verb, path, query, body, timestamp, ip, and an additional attribute MIDDLEWARE_ENV which you have set in the Service Config.

REQUEST_LOGGER middleware expects you to return an object with these properties: headers, verb, path, query, body, timestamp, ip.

Here’s a sample code for the same:

// REQUEST_LOGGER
(async function () {
  const retVal = { // initializing return object
    headers,
    verb,
    path,
    query,
    body,
    timestamp,
    ip,
  };

  /**
  * Checking for the substring, if matches then striping it
  * and setting the remaining substring as new path
  * */
  if(path.startsWith('/api_master/')) {
    retVal.path = path.substr(11);
  } else if (path.startsWith('/api_new/')) {
    retVal.path = path.substr(8);
  }

  return retVal;
})()

# Custom differentiation logic between user sessions (SESSION DIFFERENTIATOR)

For differentiating between sessions by default, HYPERTEST uses GROUP_SESSION_BY from Service Config or if x-ht-session-id is found in headers.

However, in some scenarios, you might want to differentiate based on a property such as userId/customerId that could appear in the body at different levels.

For extracting such value we have SESSION_DIFFERENTIATOR middleware where you could write your logic for extraction of such value.

Precedence for session differentiation:

[x-ht-session-id] Header >> SESSION_DIFFERENTIATOR middleware >> GROUP_SESSION_BY.

As input you would be provided with standard request attributes such as headers, verb, path, query, body, timestamp, ip, and MIDDLEWARE_ENV.

SESSION_DIFFERENTIATOR middleware expects you to return an object with the sessionDifferentiatorVal property.

Here’s a sample code for the same:

// SESSION_DIFFERENTIATOR
(function (){
  if (headers['x-ht-session-id']) {
    return { sessionDifferentiatorVal: String(headers['x-ht-session-id']) + 'x-ht-session-id' };
  }

  if (typeof body === 'object' && typeof body.customerDetails === 'object' && typeof body.customerDetails.userID !== 'undefined') {
    return { sessionDifferentiatorVal: String(body.customerDetails.userID) + '_body.customerDetails.userID' };
  }

  if (typeof body === 'object' && typeof body.customerDetail === 'object') {
    return { sessionDifferentiatorVal: String(body.customerDetail.customerId) + '_body.customerDetail.customerId' };
  }

  if (query && typeof query.customerId !== 'undefined') {
    return { sessionDifferentiatorVal: String(query.customerId) + '_query.customerId' };
  }

  return { sessionDifferentiatorVal: ip + '_default_ip' };
})()

# Randomizing request parameter value at runtime(PRE REQUEST)

More often than not while writing a test for a user registration flow we would want to pass a new and unique email id in the request so that we could identify different user accounts or sometimes it’s just a constraint from the system that every user should have a unique email, or in some cases of e-commerce platforms it’s the client’s responsibility to generate the orderId and obviously, these id’s have to be unique.

To facilitate this we have PRE_REQUEST middleware which will be executed before making a request and the updated request object will be used for making the request.

For input params you would be provided with headers, verb, path, query, body, sessionVariables, instanceName, baseUrl, and MIDDLEWARE_ENV.

PRE_REQUEST middleware expects you to return an object with these properties headers, verb, path, query, body, sessionVariables and meta object which gets stored in response object, you can use it for logging errors or storing variables generated inside middleware for debugging purposes.

Sample code for randomization of email:

// PRE_REQUEST
(function () {
  const retVal = {
    headers,
    verb,
    path,
    query,
    body,
    sessionVariables,
    meta,
  }

  if (verb === 'POST' && path === '/v1/user/create' && typeof body === 'object') {
    const chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
    let randomString = '';
    for (var i=0; i<15; i++){
      randomString += chars[Math.floor(Math.random() * chars.length)];
    }

    retVal.body.emailId = `${string}@gmail.com`
  }

  return retVal;
})()

Sample code for randomization of orderId:

// PRE_REQUEST
(function () {
  const retVal = {
    headers,
    verb,
    path,
    query,
    body,
    sessionVariables,
    meta,
  }

  if (verb === 'POST' && path === '/v1/orders/create' && typeof body === 'object') {
    // New random order id: ORDER_ID_FROM_CAPTURED_REQ + _hypertest_ + RANDOM_NUMBER
    const randomOrderId = String(body.order_id) + '_hypertest_' + String(Math.random());
    retVal.body.order_id = randomOrderId;
  }

  return retVal;
})()

# Verifying a newly created user via an internal API call(POST RESPONSE)

In some scenarios, you might want to do an email or OTP based verification after creating a new user. To automate this entire process of verification you might have to make an internal API call.

For adding such custom logic which would be executed after getting a response we have POST_RESPONSE middleware, where you can make API calls, modify a response object, or update sessionVariables.

For input params, you would be provided with req_headers, req_verb, req_path, req_query, req_body, req_meta, res_statusCode, res_headers, res_body, res_responseTime, sessionVariables, baseUrl, MIDDLEWARE_ENV, and axios(HTTP client).

POST_RESPONSE middleware expects you to return an object with sessionVariables property.

Code Sample:

// POST_RESPONSE
(async function () {
  const retVal = {
    sessionVariables,
  };

  const { BASE_URL } = MIDDLEWARE_ENV;

  if (req_verb === "POST" && req_path === "/user" && typeof req_body === "object" &&typeof res_body === "object"
      && res_statusCode === 201) {
    await axios({
      method: "get",
      url: `${BASE_URL}/api/verifyUser`,
      data: {
        email: req_body.email,
      },
    });
  }

  return retVal;
})()

# Two-Factor Authentication, login via OTP(POST RESPONSE, PRE REQUEST)

Generally, a user flow for login would first require you to generate an OTP, and then it will be sent over an email or a text message.

For automating test cases you might have an API that will provide us with the OTP for a particular user.

To make this API call to fetch the OTP after generating it we will use POST_RESPONSE middleware and the OTP will be stored in the sessionVariables.

Now we have to update the OTP using sessionVariables in the following verification requests, for this, we can use PRE_REQUEST middleware.

Let’s have a look at the code for such a use case.

  • POST_RESPONSE implementation:

    Input specification and Output requirements have already been specified in the previous topic.

    Code:

    // POST_RESPONSE
    (async function () {
      const retVal = {
        sessionVariables,
      };
    
      const { AUTH_BASE_URL } = MIDDLEWARE_ENV;
    
      if (req_verb === "POST" && req_path === "/generateOTP" && typeof req_body === "object"
          && typeof res_body === "object" && res_statusCode === 200) {
        const { data: otp } = await axios({
          method: "get",
          url: `${AUTH_BASE_URL}/api/getOTP`,
          params: {
            email: req_body.email,
          },
        });
    
        if (otp) {
          retVal.sessionVariables.latestOtp = otp;
        }
      }
    
      return retVal;
    })()
    
  • PRE_REQUEST implementation:

    Input specification and Output requirements have already been specified in earlier topic.

    Code:

    // PRE_REQUEST
    (function () {
      const meta = {};
      const retVal = {
        headers,
        verb,
        path,
        query,
        body,
        sessionVariables,
        meta,
      };
    
      if (verb === "POST" && path === "/verifyOTP" && typeof body === "object") {
        const latestOtp = sessionVariables.latestOTP;
        if (typeof latestOtp !== "undefined") {
          try {
             body.otp = latestOtp;
             retVal.meta.latestOtp = latestOtp;
          } catch (e) {
             // Storing lastestOtp and error in meta object for debugging, meta will be available in response
             retVal.meta.latestOtp = latestOtp;
             retVal.meta.error1 = e;
          }
        }
      }
    
      return retVal;
    })
    

# Generating a short-lived access token for a test case(PRE SESSION, PRE REQUEST)

One of the most critical features for any application would be administrative functionalities.

Usually while writing automation scripts for admin flows you would write a function with @before annotation which would generate all the tokens required for that particular test case.

Similarly, for generating these access tokens before running a test(user session) we have middleware which will be executed before starting a test.

Let’s have a look at the code for such a use case.

  • PRE_SESSION implementation: For input params, you would be provided with ip, sessionDifferentiatorVal, lastRequestTimestamp, shouldPlayBeforeAllTests, shouldStopOnFail, shouldPlayOnLocal, shouldPlayInSmokeTest, description, isSavedSession, isActiveSession, sessionTags, baseUrl, MIDDLEWARE_ENV, and axios(HTTP client).

    PRE_SESSION middleware expects you to return an object with sessionVariables property.

    Code:

    // PRE_SESSION
    (async function () {
       const retVal = {
         sessionVariables,
       }
       const { BASE_URL } = MIDDLEWARE_ENV;
    
       // Fetching admin credentials from MIDDLEWARE_ENV set in Service Config
       const { adminUserName, adminPassword } = MIDDLEWARE_ENV;
    
       const { data: authorization } = await axios({
         method: 'get',
         url: `${BASE_URL}/api/admin/login`,
         params: {
           username: adminUserName,
           password: adminPassword,
         },
       });
    
       if (authorization) {
         retVal.sessionVariables.adminToken = authorization.token;
       }
    
       return retVal;
    })()
    
  • PRE_REQUEST implementation: Input specification and Output requirements have already been specified earlier.

    Code:

    // PRE_REQUEST
    (function () {
      const meta = {};
      const retVal = {
        headers,
        verb,
        path,
        query,
        body,
        sessionVariables,
        meta,
      };
    
      const { lastestAdminToken } = sessionVariables;
      if (verb === 'POST' && path === '/admin/createUser' && typeof headers === 'object') {
        retVal.headers.authorization = lastestAdminToken;
      }
      return retVal;
    })()
    

# Advanced Use Case

# Money Lending Platform(State Machine)

Suppose we want to test a loan processing flow for a money lending application, where we have to create an initial state for the application before the actual test starts.

All the state operations for the flow is automatically captured by Hypertest, but by default sessions are segregated based on the GROUP_SESSION_BY configuration.

Here we would want to group the requests based on loanId for creating a end-to-end test for loan process, for this we can use SESSION_DIFFERENTIATOR middleware.

Now, for creating initial state we will use PRE_SESSION middleware, where we will implement following steps:

  1. Generate access token for admin

  2. Create a new Agent.

  3. Create a new Customer.

  4. Create a new Loan for the Customer.

  5. Assign this loan as a task to agent.

  6. Generate access token for agent.

To update these sessionVariables in the captured request we will use PRE_REQUEST middleware.

Code:

SESSION_DIFFERENTIATOR
  (function () {

    /**
    * Session differentiator has higher precedence than x-ht-session-id in headers,
    * so to override this checking for x-ht-session-id and returning it as session
    * differentiator val.
    * */
    if (headers["x-ht-session-id"]) {
      return {
        sessionDifferentiatorVal:
        String(headers["x-ht-session-id"]) + "_x-ht-session-id",
      };
    }

    // Checking for loanId in body
    if (typeof body === "object" && typeof body.loanId !== "undefined") {
      return {
        sessionDifferentiatorVal: String(body.loanId) + "_loandId",
      };
    }

    // Checking for loanId in body
    if (typeof query === "object" && typeof query.loanId !== "undefined") {
      return {
        sessionDifferentiatorVal: String(query.loanId) + "_loanId",
      };
    }

    // By default using auth token as session differentiator
    return { sessionDifferentiatorVal: "_auth_" + headers["authorization"] };
  })();
PRE_SESSION
  (async function () {
    const { ADMIN_USERNAME, ADMIN_PASSWORD, BASE_URL } = MIDDLEWARE_ENV;

    // Generating Admin token
    const {
      data: { token: adminToken },
    } = await axios({
      method: "get",
      url: `${BASE_URL}/login/admin`,
      data: {
        username: ADMIN_USERNAME,
        password: ADMIN_PASSWORD,
      },
    });

    const adminTokenHeadersObj = { authorization: "JWT " + adminToken };


    const max = 9999999999;
    const min = 5000000000;
    const random10DigitNo = String(Math.floor(Math.random() * (max - min) + min));

    // Step 1: Create a new Agent
    const { data: agent } = await axios({
      method: "post",
      url: BASE_URL + "/api/agent",
      headers: adminTokenHeadersObj,
      data: {
        firstname: "_ht_" + random10DigitNo,
        lastname: "_ht_" + random10DigitNo,
        phone: random10DigitNo,
        cityId: 1,
      },
      validateStatus: () => true,
    });

    // Step 2: Create a new Customer
    const { data: customer } = await axios({
      method: "post",
      url: BASE_URL + "/api/customer",
      headers: adminTokenHeadersObj,
      data: {
        firstname: "_ht_" + random10DigitNo,
        lastname: "_ht_" + random10DigitNo,
        phone: random10DigitNo,
        cityId: 1,
      },
      validateStatus: () => true,
    });

    const epoch_sec = Date.now() / 1000;
    const one_day_in_sec = 86400;

    //Step 3: Create a new Loan for the Customer
    const { data: { loanId } } = await axios({
      method: "post",
      url: BASE_URL + "/api/customer/createLoan",
      headers: adminTokenHeaderObj,
      data: {
        phone: customer.phone,
        category: "education",
        requestedamount: 1000000,
        city: "Bengaluru",
        cityid: 1,
        address:
          "Test Apartment, 618, 4, ...",
        locality: "Indiranagar",
        timeslotstart: epoch_sec + one_day_in_sec,
        timeslotend: epoch_sec + 2 * one_day_in_sec,
        isscheduled: true,
        schemeid: 347,
      },
    });

    //Step 4: Assign loan task to agent
    await axios({
      method: "post",
      url: BASE_URL + "/api/agent/tasks",
      headers: adminTokenHeadersObj,
      data: {
        type: 'LOAN',
        taskId: loanId,
      },
      validateStatus: () => true,
    });

    // Step 5: Generate access token for agent
    await axios({
      method: "post",
      url: BASE_URL + "/api/agent/generateOtp",
      data: { phone: agent.phone },
    });

    const { data: { otp } } = await axios({
      method: "post",
      url: baseUrl + "/api/developer/getOTPByUser",
      headers: adminTokenHeaderObj,
      data: "",
      params: { userid: agent.id },
    });

    const otp = r6.onetimecode;

    const { data: { token: agentToken } } = await axios({
      method: "post",
      url: baseUrl + "/api/agent/otpLogin",
      data: {
        email: agent.phone,
        otp: otp,
      },
    });

    const retVal = {
      sessionVariables: {
        adminToken: adminToken,
        agentToken: agentToken,
        loanId: loanId,
        customer: customer,
        agent: agent,
      },
    };

    return retVal;
  });
PRE_REQUEST
  (function () {
    const meta = {};
    const retVal = {
      headers,
      verb,
      path,
      query,
      body,
      sessionVariables,
      meta,
    };

    // Update access tokens
    if (path.toLowerCase().match(/^\/api(\/v\d+(|.\d+)|)\/agent\//)) {
      retVal.headers.authorization = "JWT " + sessionVariables.agentToken;
      retVal.meta.agentToken = sessionVariables.agentToken;
    } else {
      retVal.headers.authorization = "JWT " + sessionVariables.adminToken;
      retVal.meta.adminToken = sessionVariables.adminToken;
    }

    // Update loanId
    if (typeof body === "object" && typeof body.loanId !== "undefined") {
      retVal.body.loanId = sessionVariables.loanId;
    } else if (
      typeof query === "object" &&
      typeof query.loanId !== "undefined"
    ) {
      retVal.query.loanId = sessionVariables.loanId;
    }

    // Update agentId
    if (typeof body === "object" && typeof body.agentId !== "undefined") {
      retVal.body.agentId = sessionVariables.agent.id;
    } else if (
      typeof query === "object" &&
      typeof query.assignedAgent !== "undefined"
    ) {
      retVal.query.assignedAgent = sessionVariables.agent.id;
    }

    return retVal;
  }
)();