Initial Commit

main
duc1607 3 years ago
commit 48d1d02925

24
.gitignore vendored

@ -0,0 +1,24 @@
# Dependency directories
node_modules
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Yarn Integrity file
.yarn-integrity
# Rollup generate output
lib
# Coverage directory used by tools like istanbul
coverage
# Editor directories and files
.idea
.vscode
playground/app/*
.DS_Store

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx commitlint --edit

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lerna run precommit

@ -0,0 +1,8 @@
{
"endOfLine": "lf",
"semi": true,
"singleQuote": true,
"arrowParens": "always",
"tabWidth": 2,
"bracketSpacing": true
}

@ -0,0 +1,23 @@
FROM node:16-alpine AS build
WORKDIR /var/www
RUN apk add --no-cache \
yarn
COPY . .
# Run dependencies needed to build API Reference
RUN yarn install
# Build docs
RUN cd docs \
&& yarn install \
&& sed -i "s/base: '\/',/base: '\/integration-boilerplate\/',/g" ./.vuepress/config.js \
&& cat ./.vuepress/config.js \
&& yarn api-extract \
&& yarn build
FROM nginx
COPY --from=build /var/www/docs/.vuepress/dist /usr/share/nginx/html/integration-boilerplate

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Vue Storefront
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,92 @@
# SDK Based Integration Boilerplate for VSF 2
## Creating a new integration?
The fastest way to get started is to use our CLI to generate a new integration boilerplate
```bash
npx @vue-storefront/cli create integration
```
The CLI will ask you a few questions and generate a new integration boilerplate based on your answers.
For more information about creating a custom integration using the VSF SDK, please visit the [SDK documentation](https://docs.vuestorefront.io/sdk/custom-integrations/quick-start.html).
From the project root, you can run the one of following commands, depending on your package manager:
```bash
yarn dev
```
or
```bash
npm run dev
```
This will do the following:
- start the development server for the `playground/app` application.
- start the middleware server for the `playground/middleware` application.
## Adding an endpoint
```bash
npx @vue-storefront/cli add endpoint getSomething
```
This will do the following:
- add a new endpoint to the `api-client` package
- add a new method to the `sdk` package
- add a new route to the `playground/middleware` application
- add a new route to the `playground/app` application
### Using vs Contributing
Using the CLI is the recommended way to create a new integration boilerplate.
However, if you're planning to contribute, you can follow the steps below.
___
## Would you like to contribute?
This is an open-source project. Feel free to contribute by creating a new issue or submitting a pull request.
We highly recommend opening an issue and getting feedback *before* submitting a pull request, to avoid unnecessary work.
If we feel your contribution would benefit the community, and it adheres to our standards,
we would be delighted to accept your pull requests.
> **For internal use only.**
> All changes are recorded in the [CHANGELOG.md](CHANGELOG.md) file.
This is a new integration boilerplate for VSF 2 integrations based on the SDK.
## Requirements:
- NodeJS v16 or later,
- [Yarn](https://yarnpkg.com/).
## Repository structure
This repository contains a few necessary packages to help you get started building your new integration:
- `playground/app` - (created during CLI initialization) Demonstrates the usage of `api-client` by creating an express server app. You can use this directory to demonstrate the usage of the integration.
- `playground/middleware` - An express app that uses the `api-client` to create a server-to-server connection with service providers (e.g. commerce backend).
- `packages/api-client` - The service the middleware uses. It contains an `exampleEndpoint` that can be used as an example for the other API endpoints,
- `packages/sdk`- Think of the SDK Connector as a communication layer between the storefront and the middleware. It contains an `exampleMethod` with example documentation, unit & integration tests, that can be used as an example for the rest SDK connector methods.
- `docs` - VuePress documentation with configured API extractor, to create an API Reference based on the `api-client` and `sdk` methods & interfaces.
## Getting started
```bash
yarn
```
5. Build the packages,
```bash
yarn build
```
6. Test the packages,
```bash
yarn test
```
7. That's it. Now you can start the developing your contribution,
8. Enjoy.

@ -0,0 +1,48 @@
/**
* Config file for API Extractor. For more info, please visit: https://api-extractor.com
*/
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"projectFolder": ".",
"compiler": {
"tsconfigFilePath": "<projectFolder>/tsconfig.base.json"
},
"docModel": {
"enabled": true
},
"dtsRollup": {
"enabled": true
},
"tsdocMetadata": {
"enabled": false
},
"apiReport": {
"enabled": false
},
"messages": {
"compilerMessageReporting": {
"default": {
"logLevel": "warning"
}
},
"extractorMessageReporting": {
"default": {
"logLevel": "none",
"addToApiReportFile": false
},
"ae-extra-release-tag": {
"logLevel": "none",
"addToApiReportFile": false
},
"ae-forgotten-export": {
"logLevel": "none"
}
},
"tsdocMessageReporting": {
"default": {
"logLevel": "none",
"addToApiReportFile": false
}
}
}
}

4
docs/.gitignore vendored

@ -0,0 +1,4 @@
.vuepress/dist
.vuepress/public/commercetools
api.json
reference

@ -0,0 +1,40 @@
module.exports = {
title: 'Integration Boilerplate',
base: '/integration-boilerplate/',
description: 'Documentation for the new Integration boilerplate',
head: [['link', { rel: 'icon', href: '/favicon.png' }]],
theme: 'vsf-docs',
configureWebpack: (config) => {
config.module.rules = config.module.rules.map((rule) => ({
...rule,
use:
rule.use &&
rule.use.map((useRule) => ({
...useRule,
options:
useRule.loader === 'url-loader'
? /**
Hack for loading images properly.
ref: https://github.com/vuejs/vue-loader/issues/1612#issuecomment-559366730
*/
{ ...useRule.options, esModule: false }
: useRule.options
}))
}));
},
themeConfig: {
nav: [
{ text: 'Vue Storefront', link: 'https://vuestorefront.io/' },
{ text: 'Core Documentation', link: 'https://docs.vuestorefront.io/v2/' }
],
sidebar: {
'/': [
{
title: 'Reference',
collapsable: false,
children: [['/reference/api/', 'API Reference']]
}
]
}
}
};

@ -0,0 +1,6 @@
export default ({
Vue, // the version of Vue being used in the VuePress app
options, // the options for the root Vue instance
router, // the router instance for the app
siteData // site metadata
}) => {}

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

@ -0,0 +1,18 @@
.badge {
margin-top: 4px;
background-color: #22c34b;
}
.badge.info {
background-color: #22c34b !important;
}
.multiselect__tag {
background: #22c34b !important;
}
.custom-block.tip {
border-color: #22c34b;
background-color: #f0f7f2;
}

@ -0,0 +1 @@
$accentColor = #22c34b;

@ -0,0 +1,10 @@
---
enterpriseTag: true
hideToc: true
---
# Integrations boilerplate
This project is the new Integrations Boilerplate
<CoreDocsList />

@ -0,0 +1,37 @@
{
"name": "@vue-storefront/integration-boilerplate-docs",
"version": "0.0.1",
"private": true,
"description": "Documentation for the new Integration boilerplate",
"main": "index.js",
"scripts": {
"dev": "vuepress dev",
"build": "NODE_OPTIONS=--max_old_space_size=8192 vuepress build",
"api-extract": "cd ../ && yarn build && cd docs/ && yarn api-ref && yarn sdk-ref && yarn ref-md",
"api-ref": "cd ../packages/api-client && api-extractor run --local",
"sdk-ref": "cd ../packages/sdk && api-extractor run --local",
"ref-md": "api-documenter markdown --i reference/api --o reference/api"
},
"devDependencies": {
"@microsoft/api-documenter": "^7.13.30",
"@microsoft/api-extractor": "7.18.1",
"concat-md": "^0.3.5",
"handlebars": "^4.7.7",
"typedoc": "^0.20.20",
"typedoc-plugin-markdown": "^3.4.5",
"typescript": "^3.6.4",
"vuepress": "^1.8.2",
"vuepress-plugin-mermaidjs": "^1.9.1"
},
"author": "VSF",
"dependencies": {
"sass-loader": "^8.0.2",
"vue-multiselect": "^2.1.6",
"vuepress-theme-vsf-docs": "^1.1.0-alpha.8"
},
"workspaces": {
"nohoist": [
"typedoc-plugin-markdown"
]
}
}

@ -0,0 +1,7 @@
{
"hideBreadcrumbs": true,
"hideInPageTOC": true,
"readme": "none",
"plugin": ["typedoc-plugin-markdown"],
"theme": "./typedoc-vsf-theme"
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,11 @@
// For a detailed explanation regarding each configuration property, visit:
// https://jestjs.io/docs/en/configuration.html
module.exports = {
transform: {
'^.+\\.(j|t)s$': 'ts-jest',
},
coverageDirectory: './coverage/',
collectCoverageFrom: ['src/**/*.ts'],
testMatch: ['<rootDir>/**/__tests__/**/*spec.[jt]s?(x)'],
};

@ -0,0 +1,15 @@
{
"useWorkspaces": true,
"npmClient": "yarn",
"command": {
"publish": {
"message": "chore(release): %s",
"conventionalCommits": true
}
},
"packages": [
"packages/**/*",
"playground/**/*"
],
"version": "1.0.0"
}

@ -0,0 +1,56 @@
{
"name": "@vue-storefront/integration-boilerplate",
"private": true,
"license": "MIT",
"scripts": {
"build": "lerna run build",
"dev": "concurrently --names \"Frontend,Middleware\" \"npm run dev:app\" \"npm run dev:middleware\"",
"dev:app": "cd playground/app && npm run dev",
"dev:middleware": "cd playground/middleware && npm run dev",
"test": "lerna run test"
},
"devDependencies": {
"@babel/core": "^7.10.5",
"@loopmode/crosslink": "^0.4.0",
"@rollup/plugin-babel": "^5.1.0",
"@rollup/plugin-node-resolve": "^13.0.6",
"@rollup/plugin-replace": "^2.3.3",
"@types/jest": "^27.4.0",
"@types/node": "^12.12.14",
"@types/supertest": "~2.0.12",
"concurrently": "^8.0.1",
"cross-env": "^6.0.3",
"jest": "^27.4.7",
"lerna": "^6.5.1",
"lint-staged": "^10.0.7",
"msw": "^0.49.1",
"nodemon": "^2.0.22",
"rimraf": "^3.0.2",
"rollup": "^2.59.0",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.34.1",
"supertest": "~6.2.4",
"ts-jest": "^27.1.3",
"ts-node": "^8.4.1",
"tslib": "^2.1.0",
"typescript": "^4.2.2",
"webpack-bundle-analyzer": "^3.5.2"
},
"engines": {
"node": ">=16.x"
},
"workspaces": [
"packages/*",
"playground/*"
],
"dependencies": {
"react": "18.2.0",
"react-dom": "18.2.0"
},
"overrides": {
"react-json-view": {
"react": "$react",
"react-dom": "$react-dom"
}
}
}

@ -0,0 +1,4 @@
node_modules
coverage
lib
server

@ -0,0 +1,5 @@
export const contextMock = {
config: {} as any,
client: jest.fn() as any,
api: jest.fn() as any,
};

@ -0,0 +1,21 @@
// import { exampleEndpoint } from '../../src/api';
// import { contextMock } from '../../__mocks__/context.mock';
// import consola from 'consola';
describe('[Integration Boilerplate API] exampleEndpoint', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('calls commerce endpoint with proper parameters', async () => {
expect(true).toBe(true);
});
it('validates paramerets', async () => {
expect(true).toBe(true);
});
it('throws an error when request fails', async () => {
expect(true).toBe(true);
});
});

@ -0,0 +1,10 @@
{
"extends": "../../api-extractor.base.json",
"mainEntryPointFilePath": "./lib/index.d.ts",
"dtsRollup": {
"untrimmedFilePath": "./lib/<unscopedPackageName>.d.ts"
},
"docModel": {
"apiJsonFilePath": "<projectFolder>/docs/reference/api/<unscopedPackageName>.api.json"
}
}

@ -0,0 +1,11 @@
const baseConfig = require('./../../jest.base.config');
const apiClientJestConfig = { ...baseConfig };
apiClientJestConfig.collectCoverageFrom = [
'src/**/*.ts',
'!src/types/**',
'!src/index.ts'
];
module.exports = apiClientJestConfig;

@ -0,0 +1,31 @@
{
"name": "@vue-storefront/integration-boilerplate-api",
"version": "0.1.0",
"sideEffects": false,
"server": "server/index.js",
"main": "lib/index.cjs.js",
"module": "lib/index.es.js",
"types": "lib/index.d.ts",
"license": "VSFEL",
"engines": {
"node": ">=16.x"
},
"scripts": {
"build": "rimraf lib server && rollup -c",
"dev": "rollup -c -w",
"test": "cross-env APP_ENV=test jest",
"prepublish": "yarn build"
},
"dependencies": {
"@vue-storefront/middleware": "3.0.0-rc.2",
"axios": "^0.21.1",
"consola": "^3.0.0"
},
"devDependencies": {
"jsdom": "^17.0.0"
},
"files": [
"lib/**/*",
"server/**/*"
]
}

@ -0,0 +1,36 @@
import nodeResolve from '@rollup/plugin-node-resolve';
import typescript from 'rollup-plugin-typescript2';
import pkg from './package.json';
import { generateBaseConfig } from '../../rollup.base.config';
const extensions = ['.ts', '.js'];
const server = {
input: 'src/index.server.ts',
output: [
{
file: pkg.server,
format: 'cjs',
sourcemap: true
}
],
external: [
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {})
],
plugins: [
nodeResolve({
extensions
}),
typescript({
// eslint-disable-next-line global-require
typescript: require('typescript'),
objectHashIgnoreUnknownHack: false
})
]
};
export default [
generateBaseConfig(pkg), // It's required for other packages using api-client's types
server
];

@ -0,0 +1,12 @@
import { Endpoints } from '../../types';
export const exampleEndpoint: Endpoints['exampleEndpoint'] = async (
context,
params
) => {
console.log('exampleEndpoint has been called');
// Example request could look like this:
// return await context.client.get(`example-url?id=${params.id}`);
return { data: 'Hello, Vue Storefront Integrator!' };
};

@ -0,0 +1,18 @@
import { Endpoints } from '../../types';
export const getProducts: Endpoints['getProducts'] = async (
context,
params
) => {
console.log('getProducts has been called');
const { client } = context;
try {
const { data } = await client.get('/shop/products?page=1&itemsPerPage=10');
return { data };
}
catch (error) {
console.log('error', error);
return { data: 'error' };
}
};

@ -0,0 +1,2 @@
export { exampleEndpoint } from './exampleEndpoint';
export { getProducts } from './getProducts';

@ -0,0 +1,31 @@
import axios from 'axios';
import { apiClientFactory } from '@vue-storefront/middleware';
import { MiddlewareConfig } from './index';
import * as apiEndpoints from './api';
/**
* In here you should create the client you'll use to communicate with the backend.
* Axios is just an example.
*/
const buildClient = () => {
const axiosInstance = axios.create({
baseURL: 'http://localhost:8000/api/v2',
});
return axiosInstance
}
const onCreate = (settings: MiddlewareConfig) => {
const client = buildClient();
return {
config: settings,
client
};
};
const { createApiClient } = apiClientFactory<any, any>({
onCreate,
api: apiEndpoints,
});
export { createApiClient };

@ -0,0 +1,10 @@
/**
* `api-client` for Vue Storefront 2 integration bolierplate.
*
* @remarks
* In here you can find all references to the integration API Client.
*
* @packageDocumentation
*/
export * from './types';

@ -0,0 +1,18 @@
import { BoilerplateIntegrationContext, TODO } from '..'
/**
* Definition of all API-client methods available in {@link https://docs.vuestorefront.io/v2/advanced/context.html#context-api | context}.
*/
export interface Endpoints {
/**
* Here you can find an example endpoint definition. Based on this example, you should define how your endpoint will look like.
* This description will appear in the API extractor, so try to document all endpoints added here.
*/
exampleEndpoint(
context: BoilerplateIntegrationContext,
params: TODO
): Promise<TODO>;
getProducts(context: BoilerplateIntegrationContext, params: TODO): Promise<TODO>;
}

@ -0,0 +1 @@
export * from './endpoints';

@ -0,0 +1,6 @@
/**
* Settings to be provided in the `middleware.config.js` file.
*/
export interface MiddlewareConfig {
// Add the fields provided in the `middleware.config.js` file.
}

@ -0,0 +1,21 @@
import { IntegrationContext } from '@vue-storefront/middleware';
import { AxiosInstance } from 'axios';
import { MiddlewareConfig, ContextualizedEndpoints } from '../index';
/**
* Runtime integration context, which includes API client instance, settings, and endpoints that will be passed via middleware server.
* This interface name is starting with `Boilerplate`, but you should use your integration name in here.
**/
export type BoilerplateIntegrationContext = IntegrationContext<
AxiosInstance,
MiddlewareConfig,
ContextualizedEndpoints
>;
/**
* Global context of the application which includes runtime integration context.
**/
export interface Context {
// This property is named `boilerplate`, but you should use your integration name in here.
$boilerplate: BoilerplateIntegrationContext;
}

@ -0,0 +1,19 @@
import { Endpoints } from './api';
/**
* All available API Endpoints without first argument - `context`, because this prop is set automatically.
*/
export type ContextualizedEndpoints = {
[T in keyof Endpoints]: Endpoints[T] extends (
x: any,
...args: infer P
) => infer R
? (...args: P) => R
: never;
};
export type TODO = any;
export * from './api';
export * from './config';
export * from './context';

@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"include": ["src"],
"exclude": ["__mocks__", "__tests__"]
}

@ -0,0 +1,8 @@
module.exports = {
integrations: {
boilerplate: {
location: '@vue-storefront/integration-boilerplate-api/server',
configuration: {},
},
},
};

@ -0,0 +1,26 @@
const { createServer } = require("@vue-storefront/middleware");
const { integrations } = require("./middleware.config");
const cors = require("cors");
(async () => {
const app = await createServer({ integrations });
const host = process.argv[2] ?? "0.0.0.0";
const port = process.argv[3] ?? 8181;
const CORS_MIDDLEWARE_NAME = "corsMiddleware";
const corsMiddleware = app._router.stack.find(
(middleware) => middleware.name === CORS_MIDDLEWARE_NAME
);
corsMiddleware.handle = cors({
origin: [
"http://localhost:3000",
...(process.env.MIDDLEWARE_ALLOWED_ORIGINS?.split(",") ?? []),
],
credentials: true,
});
app.listen(port, host, () => {
console.log(`Middleware started: ${host}:${port}`);
});
})();

@ -0,0 +1,20 @@
{
"name": "@vue-storefront/integration-boilerplate-demo",
"private": true,
"version": "0.1.0",
"engines": {
"node": ">=16.x"
},
"scripts": {
"middleware": "node middleware.js"
},
"dependencies": {
"@vue-storefront/middleware": "3.0.0-rc.2",
"@vue-storefront/integration-boilerplate-api": "*",
"cors": "^2.8.5"
},
"files": [
"lib/**/*",
"server/**/*"
]
}

@ -0,0 +1,5 @@
node_modules
coverage
lib
server
.env

@ -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);
}
});
});

@ -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"
}
}

@ -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']
};

@ -0,0 +1,3 @@
const baseConfig = require('./../../jest.base.config');
module.exports = { ...baseConfig };

@ -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"
}
}

@ -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'),
}),
],
},
];

@ -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';

@ -0,0 +1,3 @@
import axios from 'axios';
export const client = axios.create();

@ -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;
};

@ -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
}

@ -0,0 +1,3 @@
export { exampleMethod } from './exampleMethod';
export { getProducts } from './getProducts';

@ -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;
}

@ -0,0 +1,4 @@
export type TODO = any;
export type { MethodOptions } from './MethodOptions';
export type { Options } from './options';

@ -0,0 +1,9 @@
/**
* Options for the SDK module.
*/
export interface Options {
/**
* The API URL of the client-side environment.
*/
apiUrl: string;
}

@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"include": ["src"],
"exclude": ["__mocks__", "__tests__"]
}

@ -0,0 +1,8 @@
module.exports = {
integrations: {
boilerplate: {
location: '@vue-storefront/integration-boilerplate-api/server',
configuration: {},
},
},
};

@ -0,0 +1,26 @@
const { createServer } = require("@vue-storefront/middleware");
const { integrations } = require("./middleware.config");
const cors = require("cors");
(async () => {
const app = await createServer({ integrations });
const host = process.argv[2] ?? "0.0.0.0";
const port = process.argv[3] ?? 8181;
const CORS_MIDDLEWARE_NAME = "corsMiddleware";
const corsMiddleware = app._router.stack.find(
(middleware) => middleware.name === CORS_MIDDLEWARE_NAME
);
corsMiddleware.handle = cors({
origin: [
"http://localhost:3000",
...(process.env.MIDDLEWARE_ALLOWED_ORIGINS?.split(",") ?? []),
],
credentials: true,
});
app.listen(port, host, () => {
console.log(`Middleware started: ${host}:${port}`);
});
})();

@ -0,0 +1,20 @@
{
"name": "@vue-storefront/integration-boilerplate-playground",
"private": true,
"version": "0.1.0",
"engines": {
"node": ">=16.x"
},
"scripts": {
"dev": "nodemon --watch node middleware.js"
},
"dependencies": {
"@vue-storefront/middleware": "3.0.1",
"@vue-storefront/integration-boilerplate-api": "*",
"cors": "^2.8.5"
},
"files": [
"lib/**/*",
"server/**/*"
]
}

@ -0,0 +1,38 @@
import nodeResolve from '@rollup/plugin-node-resolve';
import typescript from 'rollup-plugin-typescript2';
import { terser } from 'rollup-plugin-terser';
const extensions = ['.ts', '.js'];
export function generateBaseConfig(pkg) {
return {
input: 'src/index.ts',
output: [
{
file: pkg.main,
format: 'cjs',
sourcemap: true
},
{
file: pkg.module,
format: 'es',
sourcemap: true
}
],
external: [
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {})
],
plugins: [
nodeResolve({
extensions
}),
typescript({
// eslint-disable-next-line global-require
typescript: require('typescript'),
objectHashIgnoreUnknownHack: false
}),
terser()
]
};
}

@ -0,0 +1,22 @@
{
"compilerOptions": {
"outDir": "./lib",
"esModuleInterop": true,
"target": "ES2020",
"module": "ES2015",
"moduleResolution": "node",
"importHelpers": true,
"noEmitHelpers": true,
"sourceMap": true,
"declaration": true,
"declarationMap": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": "./",
"lib": ["ES2020", "dom"],
"strict": false,
"preserveSymlinks": true,
"resolveJsonModule": true
},
"exclude": ["node_modules", "**/*.spec.ts", "**/*.mock.ts"]
}

11399
yarn.lock

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save