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
+5
View File
@@ -0,0 +1,5 @@
node_modules
coverage
lib
server
.env
+1
View File
@@ -0,0 +1 @@
node_modules
@@ -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);
}
});
});
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "../../api-extractor.base.json",
"mainEntryPointFilePath": "./lib/api-extractor.data.d.ts",
"dtsRollup": {
"untrimmedFilePath": "./lib/<unscopedPackageName>.d.ts"
},
"docModel": {
"apiJsonFilePath": "<projectFolder>/docs/reference/api/<unscopedPackageName>.api.json"
}
}
+12
View File
@@ -0,0 +1,12 @@
const baseConfig = require('./../../jest.base.config');
module.exports = {
...baseConfig,
preset: 'ts-jest',
transform: {
'^.+\\.(j|t)s$': 'ts-jest',
},
globalSetup: './__tests__/integration/__config__/jest.setup.global.ts',
globalTeardown: './__tests__/integration/__config__/jest.teardown.global.ts',
setupFilesAfterEnv: ['./__tests__/integration/__config__/jest.setup.ts']
};
+3
View File
@@ -0,0 +1,3 @@
const baseConfig = require('./../../jest.base.config');
module.exports = { ...baseConfig };
+32
View File
@@ -0,0 +1,32 @@
{
"name": "@vue-storefront/integration-boilerplate-sdk",
"version": "0.1.0",
"main": "lib/index.cjs.js",
"module": "lib/index.es.js",
"types": "lib/index.d.ts",
"files": [
"lib"
],
"scripts": {
"build": "rimraf lib && rollup -c",
"dev": "rimraf lib && rollup -c -w",
"lint": "eslint . --ext .ts",
"test": "yarn test:unit && yarn test:integration",
"test:unit": "jest ./unit -c ./jest.config.unit.js",
"test:integration": "jest ./integration -c ./jest.config.integration.js --runInBand",
"test:integration:init": "rm -rf __tests__/integration/__nock-fixtures__ && jest ./integration -c ./jest.config.integration.js --runInBand",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage --passWithNoTests",
"prepublish": "yarn build"
},
"dependencies": {
"axios": "^0.27.2"
},
"devDependencies": {
"@vue-storefront/integration-boilerplate-api": "0.1.0",
"@vue-storefront/sdk": "1.0.1",
"nock": "^13.2.9",
"msw": "^0.47.3",
"rollup-plugin-typescript2": "^0.34.1"
}
}
+27
View File
@@ -0,0 +1,27 @@
import typescript from 'rollup-plugin-typescript2';
import pkg from './package.json';
export default [
{
input: 'src/index.ts',
output: [
{
file: pkg.main,
format: 'cjs',
sourcemap: true,
},
{
file: pkg.module,
format: 'es',
sourcemap: true,
},
],
external: [...Object.keys(pkg.dependencies || {})],
plugins: [
typescript({
// eslint-disable-next-line global-require
typescript: require('typescript'),
}),
],
},
];
+10
View File
@@ -0,0 +1,10 @@
/**
* `sdk-connector` for Vue Storefront 2 integration bolierplate.
*
* @remarks
* In here you can find all references to the integration SDK connector.
*
* @packageDocumentation
*/
export * from './types';
export * from './methods';
+3
View File
@@ -0,0 +1,3 @@
import axios from 'axios';
export const client = axios.create();
+17
View File
@@ -0,0 +1,17 @@
import { client } from './client';
import { Options } from './types';
import * as methods from './methods/index';
/**
* Connector methods.
*/
type Methods = typeof methods;
/**
* Initialize the Boilerplate connector.
*/
export const boilerplateConnector = (options: Options): Methods => {
client.defaults.baseURL = options.apiUrl;
return methods;
};
+28
View File
@@ -0,0 +1,28 @@
import { boilerplateConnector } from './connector';
import type { Options } from './types';
import type { Module } from '@vue-storefront/sdk';
/**
* Boulerplate module type.
*/
export interface BoilerplateModuleType extends Module {
/**
* The connector of the Boilerplate module.
*/
connector: ReturnType<typeof boilerplateConnector>;
}
/**
* Boilerplate module.
*/
export const boilerplateModule = (options: Options): BoilerplateModuleType => ({
connector: boilerplateConnector({
apiUrl: options.apiUrl,
}),
utils: {},
subscribers: {},
});
export { client } from './client';
export * from './types';
@@ -0,0 +1,26 @@
import { client } from '../../client';
import { TODO } from '../../types';
/**
* Method summary - General information about the SDK method, usually a single sentence.
*
* @remarks
* In this section, we have been adding detailed information such as:
* * what API middleware endpoint this method is calling,
* * what SAP OCC API endpoints are being called as a result of using this method,
* * when this method can be used and when it cant (e.g. logged-in vs anonymous users),
* * simply everything what helps with understanding how it works.
*
* @param props
* Just like our API methods, our SDK connector methods accept a single props parameter which carries relevant sub-properties. Therefore, there isnt much to be described within that TSDoc section.
*
* @returns
* Human-friendly information what the SDK methods returns.
*
* @example
* A short code snippet showing how to use the method. Usually we have more than one @example. We should strive for adding as many examples as possible here, with multiple param configurations.
*/
export async function exampleMethod(props: TODO) {
const { data } = await client.post<TODO>('exampleEndpoint', props);
return data
}
@@ -0,0 +1,26 @@
import { client } from '../../client';
import { TODO } from '../../types';
/**
* Method summary - General information about the SDK method, usually a single sentence.
*
* @remarks
* In this section, we have been adding detailed information such as:
* * what API middleware endpoint this method is calling,
* * what SAP OCC API endpoints are being called as a result of using this method,
* * when this method can be used and when it cant (e.g. logged-in vs anonymous users),
* * simply everything what helps with understanding how it works.
*
* @param props
* Just like our API methods, our SDK connector methods accept a single props parameter which carries relevant sub-properties. Therefore, there isnt much to be described within that TSDoc section.
*
* @returns
* Human-friendly information what the SDK methods returns.
*
* @example
* A short code snippet showing how to use the method. Usually we have more than one @example. We should strive for adding as many examples as possible here, with multiple param configurations.
*/
export async function getProducts(props: TODO) {
const { data } = await client.post<TODO>('getProducts', props);
return data
}
+3
View File
@@ -0,0 +1,3 @@
export { exampleMethod } from './exampleMethod';
export { getProducts } from './getProducts';
+18
View File
@@ -0,0 +1,18 @@
import { AxiosRequestConfig } from 'axios';
/**
* Definition of the MethodOptions parameter.
*/
export interface MethodOptions {
/**
* {@link https://axios-http.com/docs/req_config | AxiosRequestConfig} object
* You can use it to override Axios request configuration
*/
axiosRequestConfig?: Readonly<AxiosRequestConfig>;
/**
* Additional optional fields. Its usage depends on the custom implementation.
*/
[key: string]: any;
}
+4
View File
@@ -0,0 +1,4 @@
export type TODO = any;
export type { MethodOptions } from './MethodOptions';
export type { Options } from './options';
+9
View File
@@ -0,0 +1,9 @@
/**
* Options for the SDK module.
*/
export interface Options {
/**
* The API URL of the client-side environment.
*/
apiUrl: string;
}
+5
View File
@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"include": ["src"],
"exclude": ["__mocks__", "__tests__"]
}