Initial Commit

This commit is contained in:
2023-07-10 10:17:17 +07:00
commit 48d1d02925
81 changed files with 24380 additions and 0 deletions
@@ -0,0 +1,109 @@
# Integration tests automated mocks setup
Integration tests in this repository leverage automated mocks created by [Nock](https://github.com/nock/nock). The data from SAP is requested once (during the first integration tests run) and saved in the __nock-fixtures__ directory. Subsequent test runs use these recorded responses and do not send any requests to SAP.
While setting up Nock, we had to solve a couple of issues to make it work seamlessly in our case.
## Running API Middleware with Storefront removed from the monorepo
When the Storefront was still in the integration's monorepo, we could simply import `middleware.js` and `middleware.config.js` files to run the API Middleware from within SDK's `jest.setup.global.js` file. After the Storefront had been removed, we had to re-adjust `jest.setup.global.js` so that it can spin up the API Middleware completely on its own.
As a result, it repeats the logic and config previously imported from `middleware.js` and `middleware.config.js` files. It also uses a dedicated `.env` file from the `./packages/sdk/__tests__/integration/__config__` directory. Bear in mind the file is not version-controlled and **has to be added manually to the repo before running the tests locally**. Otherwise, you're in for an error with the following message:
> Missing at least one environment variable. Make sure the .env file is present in `__tests__/integration/__config__`.
## Setting Nock up once before all tests instead of every test separately
The simplest way to set up Nock would be to do it in every single test case. However, this would add a lot of boilerplate to our integration test suites. Thus, we've taken some time to figure out how to set it up once, before all tests, and not worry about it anymore.
For that purpose, we've created the `jest.setup.js` file where we have:
- set Nock [mode](https://www.npmjs.com/package/nock#modes) to `record`,
- told Nock what directory it should save recorded responses (fixtures) in,
- told Nock to create fixtures names from `expect.getState().currentTestName`.
## Giving test groups uniform names
To avoid boilerplate while describing a test group, we use the `describeGroup()` helper. Instead of describing it like this:
```ts
describe('[SDK][Integration Tests] getProduct', () => {});
```
we describe it like this:
```ts
import { describeGroup } from './__config__/jest.setup';
describe(describeGroup('getProduct'), () => {});
```
In both cases, the result is the same. However, using the helper makes our test suites cleaner, makes sure every test group has the same description and allows us to create **uniform Nock fixtures names**.
## Mocking only requests to SAP (and not to API Middleware)
Let's say we are running our integration tests for the first time. During every test, 2 **real** requests are sent:
- From SDK to API Middleware,
- From API Middleware to SAP OCC API.
As a result of that first test run, Nock records responses to both of these requests. In subsequent test runs - instead of sending a real request from SDK to API Middleware again, Nock simply returns the mocked response. It means that our API Middleware is not reached and does not make call #2 (to SAP OCC API).
We want to avoid this situation. We want our integration tests to always send the **real** request #1 and only use the mocked response to request #2. To achieve that, we had to do two things in the `nockSetup()` method:
- Add `afterRecord` callback to `nock.back()` and manually remove the recorded response for request #1 there
- Call `nock.enableNetConnect()` after `nock.back()`. Without it, request #1 was blocked by Nock in subsequent test runs with the NetConnectNotAllowedError.
## Solving errors while running tests in parallel
While recording, Nock does not handle very well tests running in parallel (in multiple child processes) instead of serially in a single process. Therefore, to record responses properly, we must run integration tests with the `--runInBand` flag.
This repository has a dedicated command for running integration tests to create (or re-create) Nock mocks:
`test:integration:init`
For subsequent test runs (where mocks are already present in the `__nock-fixtures__` directory), the basic command should be used:
`yarn test:integration`
## Simulating authenticated user requests
Our SDK methods can send requests on behalf of both anonymous and authenticated users. We can simulate an authenticated user session by calling the async `initUserSession()` helper (imported from the `jest.setup` file) at the beginning of a test case:
```ts
import { initUserSession } from './__config__/jest.setup';
it('simulates authenticated user session', async () => {
await initUserSession();
// ...
});
```
First, the helper requests a real user token from the SAP authorization server and the response is saved in a fixture. Second, it attaches the token as a cookie header to all remaining requests to API Middleware within the running test case.
After the test case execution is over, the cookie header is cleared automatically within the global `afterEach()` hook. Subsequent test cases must call `initUserSession()` again if they want to simulate an authenticated user session.
```ts
import { initUserSession } from './__config__/jest.setup';
it('simulates authenticated user session', async () => {
await initUserSession();
// ...
});
it('simulates authenticated user session, too', async () => {
await initUserSession();
// ...
});
```
If - for some reason - you need to simulate both authenticated and anonymous sessions within a single test, you can use a complimentary `terminateUserSession()` helper:
```ts
import { initUserSession, terminateUserSession } from './__config__/jest.setup';
it('simulates authenticated user session', async () => {
await initUserSession();
// send some requests as a logged-in user...
terminateUserSession();
// send some requests as an anonymous user...
});
```
@@ -0,0 +1,4 @@
/** Setup variables */
export const NOCK_FIXTURES_CATALOG_NAME = '__nock-fixtures__';
export const NOCK_MODE = 'record';
export const NOCK_EXCLUDED_SCOPE = 'localhost';
@@ -0,0 +1,26 @@
import { createServer } from '@vue-storefront/middleware';
const middlewareConfig = {
integrations: {
boilerplate: {
location: '@vue-storefront/integration-boilerplate-api/server',
configuration: {},
},
}
};
export default async () => {
const app = await createServer(middlewareConfig);
const server = await runMiddleware(app);
// eslint-disable-next-line
(globalThis as any).__MIDDLEWARE__ = server;
};
async function runMiddleware (app: any) {
return new Promise(resolve => {
const server = app.listen(8181, async () => {
resolve(server);
});
});
}
@@ -0,0 +1,36 @@
import nock from 'nock';
import path from 'path';
import {
NOCK_MODE,
NOCK_FIXTURES_CATALOG_NAME,
NOCK_EXCLUDED_SCOPE,
} from './jest.const';
let nockDone;
/**
* Sets Nock mode, fixture name and directory. Removes recorded requests to API Middleware
* so that integration tests send them every time.
*/
async function setupNock (customFixtureName?) {
nock.back.setMode(NOCK_MODE);
nock.back.fixtures = path.join(__dirname, '../', NOCK_FIXTURES_CATALOG_NAME);
const fixtureName = customFixtureName ?? expect.getState().currentTestName.split(' ').join('-');
const afterRecord = (recordings) => recordings.filter(
recording => !recording.scope.toString().includes(NOCK_EXCLUDED_SCOPE)
);
const { nockDone } = await nock.back(fixtureName, { afterRecord });
nock.enableNetConnect();
return nockDone;
}
beforeEach(async () => {
nockDone = await setupNock();
});
afterEach(() => {
nockDone();
});
@@ -0,0 +1,4 @@
export default () => {
// eslint-disable-next-line
(globalThis as any).__MIDDLEWARE__.close();
};
@@ -0,0 +1,10 @@
import { initSDK, buildModule } from '@vue-storefront/sdk';
import { boilerplateModule, BoilerplateModuleType } from '../../../src';
const sdkConfig = {
boilerplate: buildModule<BoilerplateModuleType>(boilerplateModule, {
apiUrl: 'http://localhost:8181/boilerplate',
}),
};
export const sdk = initSDK<typeof sdkConfig>(sdkConfig);
@@ -0,0 +1,13 @@
import { sdk } from './__config__/sdk.config';
describe('[Integration Boilerplate SDK][integration] exampleMethod', () => {
it('makes a request to the middleware', async () => {
const EXPECTED_RESPONSE = {"data": "Hello, Vue Storefront Integrator!"};
const res = await sdk.boilerplate.exampleMethod({
id: 1,
});
expect(res).toEqual(EXPECTED_RESPONSE);
});
});
@@ -0,0 +1,46 @@
import { exampleMethod } from '../../src/methods/exampleMethod';
import { client } from '../../src/client';
/** SETUP */
const API_METHOD_NAME = 'exampleEndpoint';
const PARAMS_MOCK = { id: 1 };
const RESPONSE_MOCK = { data: "Hello, Vue Storefront Integrator!" };
const ERROR_MOCK = new Error('error');
jest.mock('../../src/client', () => ({
client: {
post: jest.fn(() => RESPONSE_MOCK)
}
}));
/** TESTS */
describe('[Integration Boilerplate SDK][unit] exampleMethod', () => {
beforeEach(() => {
jest.clearAllMocks();
})
it('makes a single call to API Middleware', async () => {
await exampleMethod(PARAMS_MOCK);
expect(client.post).toBeCalledTimes(1);
});
it('makes a call to API Middleware with the right params', async () => {
await exampleMethod(PARAMS_MOCK);
expect(client.post).toBeCalledWith(API_METHOD_NAME, PARAMS_MOCK);
});
it('throws an exception in case of network error', async () => {
expect.hasAssertions();
(client.post as jest.Mock).mockRejectedValueOnce(ERROR_MOCK);
try {
await exampleMethod(PARAMS_MOCK);
} catch (err) {
expect(err).toBe(ERROR_MOCK);
}
});
});