mirror of
https://github.com/SomboChea/ui
synced 2026-01-12 06:05:43 +07:00
build: e2e integration with puppeteer (#192)
* build: add e2e testing scripts * build: add e2e testing scripts * chore: fix script * chore: fix script * chore: ignore e2e normal test * chore: fix node_latest_browser * chore: move lint to prepare * chore: fix lint * chore: add local theme * fix: e2e tests
This commit is contained in:
committed by
GitHub
parent
0c4fb7da13
commit
d1b3e6e3b5
11
test/e2e/.eslintrc
Normal file
11
test/e2e/.eslintrc
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"rules": {
|
||||
"@typescript-eslint/no-var-requires": 0,
|
||||
"@typescript-eslint/explicit-function-return-type": 0,
|
||||
"@typescript-eslint/no-explicit-any": 0,
|
||||
"@typescript-eslint/explicit-member-accessibility": 0,
|
||||
"no-console": 0,
|
||||
"new-cap": 0,
|
||||
"max-len": 0
|
||||
}
|
||||
}
|
||||
31
test/e2e/config/config-protected-e2e.yaml
Normal file
31
test/e2e/config/config-protected-e2e.yaml
Normal file
@@ -0,0 +1,31 @@
|
||||
web:
|
||||
enable: true
|
||||
title: verdaccio-server-protected-e2e
|
||||
|
||||
store:
|
||||
memory:
|
||||
limit: 10
|
||||
|
||||
auth:
|
||||
auth-memory:
|
||||
users:
|
||||
test:
|
||||
name: test
|
||||
password: test
|
||||
|
||||
logs:
|
||||
- { type: stdout, format: pretty, level: warn }
|
||||
|
||||
packages:
|
||||
'protected-*':
|
||||
access: $authenticated
|
||||
publish: $authenticated
|
||||
|
||||
theme:
|
||||
../plugins/theme:
|
||||
prop: dummy
|
||||
|
||||
listen: 0.0.0.0:55552
|
||||
|
||||
# expose internal methods
|
||||
_debug: true
|
||||
34
test/e2e/config/config-scoped-e2e.yaml
Normal file
34
test/e2e/config/config-scoped-e2e.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
web:
|
||||
enable: true
|
||||
title: verdaccio-server-e2e
|
||||
|
||||
store:
|
||||
memory:
|
||||
limit: 10
|
||||
|
||||
auth:
|
||||
auth-memory:
|
||||
users:
|
||||
test:
|
||||
name: test
|
||||
password: test
|
||||
|
||||
logs:
|
||||
- { type: stdout, format: pretty, level: warn }
|
||||
|
||||
packages:
|
||||
'@*/*':
|
||||
access: $all
|
||||
publish: $all
|
||||
'**':
|
||||
access: $all
|
||||
publish: $authenticated
|
||||
|
||||
theme:
|
||||
../plugins/theme:
|
||||
prop: dummy
|
||||
|
||||
listen: 0.0.0.0:55558
|
||||
|
||||
# expose internal methods
|
||||
_debug: true
|
||||
193
test/e2e/e2e.spec.js
Normal file
193
test/e2e/e2e.spec.js
Normal file
@@ -0,0 +1,193 @@
|
||||
const scopedPackageMetadata = require('./partials/pkg-scoped');
|
||||
const protectedPackageMetadata = require('./partials/pkg-protected');
|
||||
|
||||
describe('/ (Verdaccio Page)', () => {
|
||||
let page;
|
||||
// this might be increased based on the delays included in all test
|
||||
jest.setTimeout(20000);
|
||||
|
||||
const clickElement = async function(selector, options = { button: 'middle', delay: 100 }) {
|
||||
const button = await page.$(selector);
|
||||
await button.focus();
|
||||
await button.click(options);
|
||||
};
|
||||
|
||||
const evaluateSignIn = async function() {
|
||||
const text = await page.evaluate(() => document.querySelector('button[data-testid="header--button-login"]').textContent);
|
||||
|
||||
expect(text).toMatch('Login');
|
||||
};
|
||||
|
||||
const getPackages = async function() {
|
||||
return await page.$$('.package-list-items .package-link a');
|
||||
};
|
||||
|
||||
const logIn = async function() {
|
||||
await clickElement('button[data-testid="header--button-login"]');
|
||||
await page.waitFor(500);
|
||||
// we fill the sign in form
|
||||
const signInDialog = await page.$('#login--form-container');
|
||||
const userInput = await signInDialog.$('#login--form-username');
|
||||
expect(userInput).not.toBeNull();
|
||||
const passInput = await signInDialog.$('#login--form-password');
|
||||
expect(passInput).not.toBeNull();
|
||||
await userInput.type('test', { delay: 100 });
|
||||
await passInput.type('test', { delay: 100 });
|
||||
await passInput.dispose();
|
||||
// click on log in
|
||||
const loginButton = await page.$('#login--form-submit');
|
||||
expect(loginButton).toBeDefined();
|
||||
await loginButton.focus();
|
||||
await loginButton.click({ delay: 100 });
|
||||
await page.waitFor(500);
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
page = await global.__BROWSER__.newPage();
|
||||
await page.goto('http://0.0.0.0:55558');
|
||||
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('should load without error', async () => {
|
||||
const text = await page.evaluate(() => document.body.textContent);
|
||||
|
||||
// FIXME: perhaps it is not the best approach
|
||||
expect(text).toContain('Powered by');
|
||||
});
|
||||
|
||||
test('should match title with no packages published', async () => {
|
||||
const text = await page.evaluate(() => document.querySelector('#help-card__title').textContent);
|
||||
expect(text).toMatch('No Package Published Yet');
|
||||
});
|
||||
|
||||
test('should match title with first step', async () => {
|
||||
const text = await page.evaluate(() => document.querySelector('#help-card').textContent);
|
||||
expect(text).toContain('npm adduser --registry http://0.0.0.0:55558');
|
||||
});
|
||||
|
||||
test('should match title with second step', async () => {
|
||||
const text = await page.evaluate(() => document.querySelector('#help-card').textContent);
|
||||
expect(text).toContain('npm publish --registry http://0.0.0.0:55558');
|
||||
});
|
||||
|
||||
test('should match button Login to sign in', async () => {
|
||||
await evaluateSignIn();
|
||||
});
|
||||
|
||||
test('should click on sign in button', async () => {
|
||||
const signInButton = await page.$('button[data-testid="header--button-login"]');
|
||||
await signInButton.click();
|
||||
await page.waitFor(1000);
|
||||
const signInDialog = await page.$('#login--form-container');
|
||||
|
||||
expect(signInDialog).not.toBeNull();
|
||||
});
|
||||
|
||||
test('should log in an user', async () => {
|
||||
// we open the dialog
|
||||
await logIn();
|
||||
// check whether user is logged
|
||||
const buttonLogout = await page.$('#header--button-logout');
|
||||
expect(buttonLogout).toBeDefined();
|
||||
});
|
||||
|
||||
test('should logout an user', async () => {
|
||||
// we assume the user is logged already
|
||||
await clickElement('#header--button-account', { clickCount: 1, delay: 500 });
|
||||
await page.waitFor(1000);
|
||||
await clickElement('#header--button-logout > span', { clickCount: 1, delay: 500 });
|
||||
await page.waitFor(1000);
|
||||
await evaluateSignIn();
|
||||
});
|
||||
|
||||
test('should check registry info dialog', async () => {
|
||||
const registryInfoButton = await page.$('#header--button-registryInfo');
|
||||
registryInfoButton.click();
|
||||
await page.waitFor(500);
|
||||
|
||||
const registryInfoDialog = await page.$('#registryInfo--dialog-container');
|
||||
expect(registryInfoDialog).not.toBeNull();
|
||||
|
||||
const closeButton = await page.$('#registryInfo--dialog-close');
|
||||
closeButton.click();
|
||||
});
|
||||
|
||||
test('should publish a package', async () => {
|
||||
await global.__SERVER__.putPackage(scopedPackageMetadata.name, scopedPackageMetadata);
|
||||
await page.waitFor(1000);
|
||||
await page.reload();
|
||||
await page.waitFor(1000);
|
||||
const packagesList = await getPackages();
|
||||
expect(packagesList).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should navigate to the package detail', async () => {
|
||||
const packagesList = await getPackages();
|
||||
const firstPackage = packagesList[0];
|
||||
await firstPackage.click({ clickCount: 1, delay: 200 });
|
||||
await page.waitFor(1000);
|
||||
const readmeText = await page.evaluate(() => document.querySelector('.markdown-body').textContent);
|
||||
expect(readmeText).toMatch('test');
|
||||
});
|
||||
|
||||
test('should contains last sync information', async () => {
|
||||
const versionList = await page.$$('.sidebar-info .detail-info');
|
||||
expect(versionList).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should display dependencies tab', async () => {
|
||||
const dependenciesTab = await page.$$('#dependencies-tab');
|
||||
expect(dependenciesTab).toHaveLength(1);
|
||||
await dependenciesTab[0].click({ clickCount: 1, delay: 200 });
|
||||
await page.waitFor(1000);
|
||||
const tags = await page.$$('.dep-tag');
|
||||
const tag = tags[0];
|
||||
const label = await page.evaluate(el => el.innerText, tag);
|
||||
expect(label).toMatch('verdaccio@');
|
||||
});
|
||||
|
||||
test('should display version tab', async () => {
|
||||
const versionsTab = await page.$$('#versions-tab');
|
||||
expect(versionsTab).toHaveLength(1);
|
||||
await versionsTab[0].click({ clickCount: 1, delay: 200 });
|
||||
await page.waitFor(1000);
|
||||
const versionItems = await page.$$('.version-item');
|
||||
expect(versionItems).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('should display uplinks tab', async () => {
|
||||
const upLinksTab = await page.$$('#uplinks-tab');
|
||||
expect(upLinksTab).toHaveLength(1);
|
||||
await upLinksTab[0].click({ clickCount: 1, delay: 200 });
|
||||
await page.waitFor(1000);
|
||||
});
|
||||
|
||||
test('should display readme tab', async () => {
|
||||
const readmeTab = await page.$$('#readme-tab');
|
||||
expect(readmeTab).toHaveLength(1);
|
||||
await readmeTab[0].click({ clickCount: 1, delay: 200 });
|
||||
await page.waitFor(1000);
|
||||
});
|
||||
|
||||
test('should publish a protected package', async () => {
|
||||
await page.goto('http://0.0.0.0:55552');
|
||||
await page.waitFor(500);
|
||||
await global.__SERVER_PROTECTED__.putPackage(protectedPackageMetadata.name, protectedPackageMetadata);
|
||||
await page.waitFor(500);
|
||||
await page.reload();
|
||||
await page.waitFor(500);
|
||||
const text = await page.evaluate(() => document.querySelector('#help-card__title').textContent);
|
||||
expect(text).toMatch('No Package Published Yet');
|
||||
});
|
||||
|
||||
test('should go to 404 page', async () => {
|
||||
await page.goto('http://0.0.0.0:55552/-/web/detail/@verdaccio/not-found');
|
||||
await page.waitFor(500);
|
||||
const text = await page.evaluate(() => document.querySelector('.not-found-text').textContent);
|
||||
expect(text).toMatch("Sorry, we couldn't find it...");
|
||||
});
|
||||
});
|
||||
50
test/e2e/partials/pkg-protected.js
Normal file
50
test/e2e/partials/pkg-protected.js
Normal file
@@ -0,0 +1,50 @@
|
||||
const json = {
|
||||
_id: 'protected-pkg',
|
||||
name: 'protected-pkg',
|
||||
description: '',
|
||||
'dist-tags': {
|
||||
latest: '5.0.5',
|
||||
},
|
||||
versions: {
|
||||
'5.0.5': {
|
||||
name: 'protected-pkg',
|
||||
version: '5.0.5',
|
||||
description: '',
|
||||
main: 'index.js',
|
||||
scripts: {
|
||||
test: 'echo "Error: no test specified" && exit 1',
|
||||
},
|
||||
keywords: [],
|
||||
author: {
|
||||
name: 'User NPM',
|
||||
email: 'user@domain.com',
|
||||
},
|
||||
license: 'ISC',
|
||||
dependencies: {
|
||||
verdaccio: '^2.7.2',
|
||||
},
|
||||
readme: '# test',
|
||||
readmeFilename: 'README.md',
|
||||
_id: 'protected-pkg@5.0.5',
|
||||
_npmVersion: '5.5.1',
|
||||
_nodeVersion: '8.7.0',
|
||||
_npmUser: {},
|
||||
dist: {
|
||||
integrity: 'sha512-6gHiERpiDgtb3hjqpQH5/i7zRmvYi9pmCjQf2ZMy3QEa9wVk9RgdZaPWUt7ZOnWUPFjcr9cmE6dUBf+XoPoH4g==',
|
||||
shasum: '2c03764f651a9f016ca0b7620421457b619151b9',
|
||||
tarball: 'http://localhost:5555/protected-pkg/-/protected-pkg-5.0.5.tgz',
|
||||
},
|
||||
},
|
||||
},
|
||||
readme: '# test',
|
||||
_attachments: {
|
||||
'protected-pkg-5.0.5.tgz': {
|
||||
content_type: 'application/octet-stream',
|
||||
data:
|
||||
'H4sIAAAAAAAAE+2W32vbMBDH85y/QnjQp9qxLEeBMsbGlocNBmN7bFdQ5WuqxJaEpGQdo//79KPeQsnIw5KUDX/9IOvurLuz/DHSjK/YAiY6jcXSKjk6sMqypHWNdtmD6hlBI0wqQmo8nVbVqMR4OsNoVB66kF1aW8eML+Vv10m9oF/jP6IfY4QyyTrILlD2eqkcm+gVzpdrJrPz4NuAsULJ4MZFWdBkbcByI7R79CRjx0ScCdnAvf+SkjUFWu8IubzBgXUhDPidQlfZ3BhlLpBUKDiQ1cDFrYDmKkNnZwjuhUM4808+xNVW8P2bMk1Y7vJrtLC1u1MmLPjBF40+Cc4ahV6GDmI/DWygVRpMwVX3KtXUCg7Sxp7ff3nbt6TBFy65gK1iffsN41yoEHtdFbOiisWMH8bPvXUH0SP3k+KG3UBr+DFy7OGfEJr4x5iWVeS/pLQe+D+FIv/agIWI6GX66kFuIhT+1gDjrp/4d7WAvAwEJPh0u14IufWkM0zaW2W6nLfM2lybgJ4LTJ0/jWiAK8OcMjt8MW3OlfQppcuhhQ6k+2OgkK2Q8DssFPi/IHpU9fz3/+xj5NjDf8QFE39VmE4JDfzPCBn4P4X6/f88f/Pu47zomiPk2Lv/dOv8h+P/34/D/p9CL+Kp67mrGDRo0KBBp9ZPsETQegASAAA=',
|
||||
length: 512,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = json;
|
||||
50
test/e2e/partials/pkg-scoped.js
Normal file
50
test/e2e/partials/pkg-scoped.js
Normal file
@@ -0,0 +1,50 @@
|
||||
const json = {
|
||||
_id: '@scope/pk1-test',
|
||||
name: '@scope/pk1-test',
|
||||
description: '',
|
||||
'dist-tags': {
|
||||
latest: '1.0.6',
|
||||
},
|
||||
versions: {
|
||||
'1.0.6': {
|
||||
name: '@scope/pk1-test',
|
||||
version: '1.0.6',
|
||||
description: '',
|
||||
main: 'index.js',
|
||||
scripts: {
|
||||
test: 'echo "Error: no test specified" && exit 1',
|
||||
},
|
||||
keywords: [],
|
||||
author: {
|
||||
name: 'User NPM',
|
||||
email: 'user@domain.com',
|
||||
},
|
||||
license: 'ISC',
|
||||
dependencies: {
|
||||
verdaccio: '^2.7.2',
|
||||
},
|
||||
readme: '# test',
|
||||
readmeFilename: 'README.md',
|
||||
_id: '@scope/pk1-test@1.0.6',
|
||||
_npmVersion: '5.5.1',
|
||||
_nodeVersion: '8.7.0',
|
||||
_npmUser: {},
|
||||
dist: {
|
||||
integrity: 'sha512-6gHiERpiDgtb3hjqpQH5/i7zRmvYi9pmCjQf2ZMy3QEa9wVk9RgdZaPWUt7ZOnWUPFjcr9cmE6dUBf+XoPoH4g==',
|
||||
shasum: '2c03764f651a9f016ca0b7620421457b619151b9',
|
||||
tarball: 'http://localhost:5555/@scope/pk1-test/-/@scope/pk1-test-1.0.6.tgz',
|
||||
},
|
||||
},
|
||||
},
|
||||
readme: '# test',
|
||||
_attachments: {
|
||||
'@scope/pk1-test-1.0.6.tgz': {
|
||||
content_type: 'application/octet-stream',
|
||||
data:
|
||||
'H4sIAAAAAAAAE+2W32vbMBDH85y/QnjQp9qxLEeBMsbGlocNBmN7bFdQ5WuqxJaEpGQdo//79KPeQsnIw5KUDX/9IOvurLuz/DHSjK/YAiY6jcXSKjk6sMqypHWNdtmD6hlBI0wqQmo8nVbVqMR4OsNoVB66kF1aW8eML+Vv10m9oF/jP6IfY4QyyTrILlD2eqkcm+gVzpdrJrPz4NuAsULJ4MZFWdBkbcByI7R79CRjx0ScCdnAvf+SkjUFWu8IubzBgXUhDPidQlfZ3BhlLpBUKDiQ1cDFrYDmKkNnZwjuhUM4808+xNVW8P2bMk1Y7vJrtLC1u1MmLPjBF40+Cc4ahV6GDmI/DWygVRpMwVX3KtXUCg7Sxp7ff3nbt6TBFy65gK1iffsN41yoEHtdFbOiisWMH8bPvXUH0SP3k+KG3UBr+DFy7OGfEJr4x5iWVeS/pLQe+D+FIv/agIWI6GX66kFuIhT+1gDjrp/4d7WAvAwEJPh0u14IufWkM0zaW2W6nLfM2lybgJ4LTJ0/jWiAK8OcMjt8MW3OlfQppcuhhQ6k+2OgkK2Q8DssFPi/IHpU9fz3/+xj5NjDf8QFE39VmE4JDfzPCBn4P4X6/f88f/Pu47zomiPk2Lv/dOv8h+P/34/D/p9CL+Kp67mrGDRo0KBBp9ZPsETQegASAAA=',
|
||||
length: 512,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = json;
|
||||
6
test/e2e/plugins/theme.js
Normal file
6
test/e2e/plugins/theme.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = () => {
|
||||
console.log('loading local theme');
|
||||
return path.join(__dirname, '../../../', 'static');
|
||||
};
|
||||
4
test/e2e/pre-setup.js
Normal file
4
test/e2e/pre-setup.js
Normal file
@@ -0,0 +1,4 @@
|
||||
require('@babel/register')({
|
||||
extensions: ['.ts', '.js'],
|
||||
});
|
||||
module.exports = require('./setup');
|
||||
75
test/e2e/puppeteer_environment.js
Normal file
75
test/e2e/puppeteer_environment.js
Normal file
@@ -0,0 +1,75 @@
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const { yellow } = require('kleur');
|
||||
const NodeEnvironment = require('jest-environment-node');
|
||||
const puppeteer = require('puppeteer');
|
||||
|
||||
const VerdaccioProcess = require('./registry-launcher');
|
||||
const Server = require('./server');
|
||||
|
||||
const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup');
|
||||
|
||||
class VerdaccioConfig {
|
||||
constructor(storagePath, configPath, domainPath, port) {
|
||||
this.storagePath = storagePath;
|
||||
this.configPath = configPath;
|
||||
this.domainPath = domainPath;
|
||||
this.port = port;
|
||||
}
|
||||
}
|
||||
|
||||
class PuppeteerEnvironment extends NodeEnvironment {
|
||||
constructor(config) {
|
||||
super(config);
|
||||
}
|
||||
|
||||
async setup() {
|
||||
const config1 = new VerdaccioConfig(
|
||||
path.join(__dirname, './store-e2e'),
|
||||
path.join(__dirname, './config/config-scoped-e2e.yaml'),
|
||||
'http://0.0.0.0:55558/',
|
||||
55558
|
||||
);
|
||||
const config2 = new VerdaccioConfig(
|
||||
path.join(__dirname, './store-e2e'),
|
||||
path.join(__dirname, './config/config-protected-e2e.yaml'),
|
||||
'http://0.0.0.0:55552/',
|
||||
55552
|
||||
);
|
||||
const server1 = new Server.default(config1.domainPath);
|
||||
const server2 = new Server.default(config2.domainPath);
|
||||
const process1 = new VerdaccioProcess.default(config1, server1);
|
||||
const process2 = new VerdaccioProcess.default(config2, server2);
|
||||
const fork = await process1.init('../../node_modules/.bin/verdaccio');
|
||||
const fork2 = await process2.init('../../node_modules/.bin/verdaccio');
|
||||
this.global.__VERDACCIO_E2E__ = fork[0];
|
||||
this.global.__VERDACCIO__PROTECTED_E2E__ = fork2[0];
|
||||
|
||||
console.log(yellow('Setup Test Environment.'));
|
||||
await super.setup();
|
||||
const wsEndpoint = fs.readFileSync(path.join(DIR, 'wsEndpoint'), 'utf8');
|
||||
if (!wsEndpoint) {
|
||||
throw new Error('wsEndpoint not found');
|
||||
}
|
||||
this.global.__SERVER__ = server1;
|
||||
this.global.__SERVER_PROTECTED__ = server2;
|
||||
this.global.__BROWSER__ = await puppeteer.connect({
|
||||
browserWSEndpoint: wsEndpoint,
|
||||
});
|
||||
}
|
||||
|
||||
async teardown() {
|
||||
console.log(yellow('Teardown Test Environment.'));
|
||||
await super.teardown();
|
||||
this.global.__VERDACCIO_E2E__.stop();
|
||||
this.global.__VERDACCIO__PROTECTED_E2E__.stop();
|
||||
}
|
||||
|
||||
runScript(script) {
|
||||
return super.runScript(script);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PuppeteerEnvironment;
|
||||
60
test/e2e/registry-launcher.ts
Normal file
60
test/e2e/registry-launcher.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import path from 'path';
|
||||
import { fork } from 'child_process';
|
||||
|
||||
import { HTTP_STATUS } from '@verdaccio/commons-api';
|
||||
|
||||
export const CREDENTIALS = {
|
||||
user: 'foo',
|
||||
password: 'test',
|
||||
};
|
||||
|
||||
export default class VerdaccioProcess {
|
||||
private bridge;
|
||||
private config;
|
||||
private childFork;
|
||||
|
||||
public constructor(config, bridge) {
|
||||
this.config = config;
|
||||
this.bridge = bridge;
|
||||
}
|
||||
|
||||
public init(verdaccioPath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._start(verdaccioPath, resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
private _start(verdaccioPath: string, resolve: Function, reject: Function) {
|
||||
const verdaccioRegisterWrap: string = path.join(__dirname, verdaccioPath);
|
||||
const childOptions = {
|
||||
silent: false,
|
||||
};
|
||||
|
||||
const { configPath, port } = this.config;
|
||||
this.childFork = fork(verdaccioRegisterWrap, ['-c', configPath, '-l', port as string], childOptions);
|
||||
|
||||
this.childFork.on('message', msg => {
|
||||
// verdaccio_started is a message that comes from verdaccio in debug mode that notify has been started
|
||||
if ('verdaccio_started' in msg) {
|
||||
this.bridge
|
||||
.debug()
|
||||
.status(HTTP_STATUS.OK)
|
||||
.then(body => {
|
||||
this.bridge
|
||||
.auth(CREDENTIALS.user, CREDENTIALS.password)
|
||||
.status(HTTP_STATUS.CREATED)
|
||||
.body_ok(new RegExp(CREDENTIALS.user))
|
||||
.then(() => resolve([this, body.pid]), reject);
|
||||
}, reject);
|
||||
}
|
||||
});
|
||||
|
||||
this.childFork.on('error', err => reject([err, this]));
|
||||
this.childFork.on('disconnect', err => reject([err, this]));
|
||||
this.childFork.on('exit', err => reject([err, this]));
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
return this.childFork.kill('SIGINT');
|
||||
}
|
||||
}
|
||||
137
test/e2e/request.ts
Normal file
137
test/e2e/request.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import assert from 'assert';
|
||||
|
||||
import _ from 'lodash';
|
||||
import request from 'request';
|
||||
|
||||
const requestData = Symbol('smart_request_data');
|
||||
|
||||
export interface RequestPromise {
|
||||
status(reason: any): any;
|
||||
body_ok(reason: any): any;
|
||||
body_error(reason: any): any;
|
||||
request(reason: any): any;
|
||||
response(reason: any): any;
|
||||
send(reason: any): any;
|
||||
}
|
||||
|
||||
function injectResponse(smartObject: any, promise: Promise<any>): Promise<any> {
|
||||
// $FlowFixMe
|
||||
promise[requestData] = smartObject[requestData];
|
||||
return promise;
|
||||
}
|
||||
|
||||
export class PromiseAssert extends Promise<any> implements RequestPromise {
|
||||
public constructor(options: any) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
public status(expected: number) {
|
||||
const selfData = this[requestData];
|
||||
|
||||
return injectResponse(
|
||||
this,
|
||||
this.then(function(body) {
|
||||
try {
|
||||
assert.equal(selfData.response.statusCode, expected);
|
||||
} catch (err) {
|
||||
selfData.error.message = err.message;
|
||||
throw selfData.error;
|
||||
}
|
||||
return body;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public body_ok(expected: any) {
|
||||
const selfData = this[requestData];
|
||||
|
||||
return injectResponse(
|
||||
this,
|
||||
this.then(function(body) {
|
||||
try {
|
||||
if (_.isRegExp(expected)) {
|
||||
assert(body.ok.match(expected), "'" + body.ok + "' doesn't match " + expected);
|
||||
} else {
|
||||
assert.equal(body.ok, expected);
|
||||
}
|
||||
assert.equal(body.error, null);
|
||||
} catch (err) {
|
||||
selfData.error.message = err.message;
|
||||
throw selfData.error;
|
||||
}
|
||||
|
||||
return body;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public body_error(expected: any) {
|
||||
// $FlowFixMe
|
||||
const selfData = this[requestData];
|
||||
|
||||
return injectResponse(
|
||||
this,
|
||||
this.then(function(body) {
|
||||
try {
|
||||
if (_.isRegExp(expected)) {
|
||||
assert(body.error.match(expected), body.error + " doesn't match " + expected);
|
||||
} else {
|
||||
assert.equal(body.error, expected);
|
||||
}
|
||||
assert.equal(body.ok, null);
|
||||
} catch (err) {
|
||||
selfData.error.message = err.message;
|
||||
throw selfData.error;
|
||||
}
|
||||
return body;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public request(callback: any) {
|
||||
callback(this[requestData].request);
|
||||
return this;
|
||||
}
|
||||
|
||||
public response(cb: any) {
|
||||
const selfData = this[requestData];
|
||||
|
||||
return injectResponse(
|
||||
this,
|
||||
this.then(function(body) {
|
||||
cb(selfData.response);
|
||||
return body;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public send(data: any) {
|
||||
this[requestData].request.end(data);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
function smartRequest(options: any): Promise<any> {
|
||||
const smartObject: any = {};
|
||||
|
||||
smartObject[requestData] = {};
|
||||
smartObject[requestData].error = Error();
|
||||
Error.captureStackTrace(smartObject[requestData].error, smartRequest);
|
||||
|
||||
const promiseResult: Promise<any> = new PromiseAssert(function(resolve, reject) {
|
||||
// store request reference on symbol
|
||||
smartObject[requestData].request = request(options, function(err, res, body) {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
// store the response on symbol
|
||||
smartObject[requestData].response = res;
|
||||
resolve(body);
|
||||
});
|
||||
});
|
||||
|
||||
return injectResponse(smartObject, promiseResult);
|
||||
}
|
||||
|
||||
export default smartRequest;
|
||||
265
test/e2e/server.ts
Normal file
265
test/e2e/server.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
import assert from 'assert';
|
||||
|
||||
import _ from 'lodash';
|
||||
import { HTTP_STATUS, HEADERS, API_MESSAGE } from '@verdaccio/commons-api';
|
||||
|
||||
import smartRequest, { RequestPromise } from './request';
|
||||
import { CREDENTIALS } from './registry-launcher';
|
||||
|
||||
declare class PromiseAssert<RequestPromise> extends Promise<any> {
|
||||
public constructor(options: any);
|
||||
}
|
||||
|
||||
const TARBALL = 'tarball-blahblah-file.name';
|
||||
|
||||
function getPackage(name, version = '0.0.0'): any {
|
||||
return {
|
||||
name,
|
||||
version,
|
||||
readme: 'this is a readme',
|
||||
dist: {
|
||||
shasum: 'fake',
|
||||
tarball: `http://localhost:0000/${encodeURIComponent(name)}/-/${TARBALL}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface ServerBridge {
|
||||
url: string;
|
||||
userAgent: string;
|
||||
authstr: string;
|
||||
request(options: any): typeof PromiseAssert;
|
||||
auth(name: string, password: string): RequestPromise;
|
||||
auth(name: string, password: string): RequestPromise;
|
||||
logout(token: string): Promise<any>;
|
||||
getPackage(name: string): Promise<any>;
|
||||
putPackage(name: string, data: any): Promise<any>;
|
||||
putVersion(name: string, version: string, data: any): Promise<any>;
|
||||
getTarball(name: string, filename: string): Promise<any>;
|
||||
putTarball(name: string, filename: string, data: any): Promise<any>;
|
||||
removeTarball(name: string): Promise<any>;
|
||||
removeSingleTarball(name: string, filename: string): Promise<any>;
|
||||
addTag(name: string, tag: string, version: string): Promise<any>;
|
||||
putTarballIncomplete(name: string, filename: string, data: any, size: number, cb: Function): Promise<any>;
|
||||
addPackage(name: string): Promise<any>;
|
||||
whoami(): Promise<any>;
|
||||
ping(): Promise<any>;
|
||||
debug(): RequestPromise;
|
||||
}
|
||||
|
||||
const TOKEN_BASIC = 'Basic';
|
||||
|
||||
function buildToken(type: string, token: string): string {
|
||||
return `${_.capitalize(type)} ${token}`;
|
||||
}
|
||||
|
||||
const buildAuthHeader = (user, pass): string => {
|
||||
return buildToken(TOKEN_BASIC, new Buffer(`${user}:${pass}`).toString('base64'));
|
||||
};
|
||||
|
||||
export default class Server implements ServerBridge {
|
||||
public url: string;
|
||||
public userAgent: string;
|
||||
public authstr: string;
|
||||
|
||||
public constructor(url: string) {
|
||||
this.url = url.replace(/\/$/, '');
|
||||
this.userAgent = 'node/v8.1.2 linux x64';
|
||||
this.authstr = buildAuthHeader(CREDENTIALS.user, CREDENTIALS.password);
|
||||
}
|
||||
|
||||
public request(options: any): any {
|
||||
assert(options.uri);
|
||||
const headers = options.headers || {};
|
||||
|
||||
headers.accept = headers.accept || HEADERS.JSON;
|
||||
headers['user-agent'] = headers['user-agent'] || this.userAgent;
|
||||
headers.authorization = headers.authorization || this.authstr;
|
||||
|
||||
return smartRequest({
|
||||
url: this.url + options.uri,
|
||||
method: options.method || 'GET',
|
||||
headers: headers,
|
||||
encoding: options.encoding,
|
||||
json: _.isNil(options.json) === false ? options.json : true,
|
||||
});
|
||||
}
|
||||
|
||||
public auth(name: string, password: string) {
|
||||
// pragma: allowlist secret
|
||||
this.authstr = buildAuthHeader(name, password);
|
||||
return this.request({
|
||||
uri: `/-/user/org.couchdb.user:${encodeURIComponent(name)}/-rev/undefined`,
|
||||
method: 'PUT',
|
||||
json: {
|
||||
name,
|
||||
password,
|
||||
email: `${CREDENTIALS.user}@example.com`,
|
||||
_id: `org.couchdb.user:${name}`,
|
||||
type: 'user',
|
||||
roles: [],
|
||||
date: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public logout(token: string) {
|
||||
return this.request({
|
||||
uri: `/-/user/token/${encodeURIComponent(token)}`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
}
|
||||
|
||||
public getPackage(name: string) {
|
||||
return this.request({
|
||||
uri: `/${encodeURIComponent(name)}`,
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
public putPackage(name: string, data) {
|
||||
if (_.isObject(data) && !Buffer.isBuffer(data)) {
|
||||
data = JSON.stringify(data);
|
||||
}
|
||||
|
||||
return this.request({
|
||||
uri: `/${encodeURIComponent(name)}`,
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
[HEADERS.CONTENT_TYPE]: HEADERS.JSON,
|
||||
},
|
||||
}).send(data);
|
||||
}
|
||||
|
||||
public putVersion(name: string, version: string, data: any) {
|
||||
if (_.isObject(data) && !Buffer.isBuffer(data)) {
|
||||
data = JSON.stringify(data);
|
||||
}
|
||||
|
||||
return this.request({
|
||||
uri: `/${encodeURIComponent(name)}/${encodeURIComponent(version)}/-tag/latest`,
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
[HEADERS.CONTENT_TYPE]: HEADERS.JSON,
|
||||
},
|
||||
}).send(data);
|
||||
}
|
||||
|
||||
public getTarball(name: string, filename: string) {
|
||||
return this.request({
|
||||
uri: `/${encodeURIComponent(name)}/-/${encodeURIComponent(filename)}`,
|
||||
method: 'GET',
|
||||
encoding: null,
|
||||
});
|
||||
}
|
||||
|
||||
public putTarball(name: string, filename: string, data: any) {
|
||||
return this.request({
|
||||
uri: `/${encodeURIComponent(name)}/-/${encodeURIComponent(filename)}/whatever`,
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
[HEADERS.CONTENT_TYPE]: HEADERS.OCTET_STREAM,
|
||||
},
|
||||
}).send(data);
|
||||
}
|
||||
|
||||
public removeTarball(name: string) {
|
||||
return this.request({
|
||||
uri: `/${encodeURIComponent(name)}/-rev/whatever`,
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
[HEADERS.CONTENT_TYPE]: HEADERS.JSON_CHARSET,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public removeSingleTarball(name: string, filename: string) {
|
||||
return this.request({
|
||||
uri: `/${encodeURIComponent(name)}/-/${filename}/-rev/whatever`,
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
[HEADERS.CONTENT_TYPE]: HEADERS.JSON_CHARSET,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public addTag(name: string, tag: string, version: string) {
|
||||
return this.request({
|
||||
uri: `/${encodeURIComponent(name)}/${encodeURIComponent(tag)}`,
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
[HEADERS.CONTENT_TYPE]: HEADERS.JSON,
|
||||
},
|
||||
}).send(JSON.stringify(version));
|
||||
}
|
||||
|
||||
public putTarballIncomplete(pkgName: string, filename: string, data: any, headerContentSize: number): Promise<any> {
|
||||
const promise = this.request({
|
||||
uri: `/${encodeURIComponent(pkgName)}/-/${encodeURIComponent(filename)}/whatever`,
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
[HEADERS.CONTENT_TYPE]: HEADERS.OCTET_STREAM,
|
||||
[HEADERS.CONTENT_LENGTH]: headerContentSize,
|
||||
},
|
||||
timeout: 1000,
|
||||
});
|
||||
|
||||
promise.request(function(req) {
|
||||
req.write(data);
|
||||
// it auto abort the request
|
||||
setTimeout(function() {
|
||||
req.req.abort();
|
||||
}, 20);
|
||||
});
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
promise
|
||||
.then(function() {
|
||||
reject(Error('no error'));
|
||||
})
|
||||
.catch(function(err) {
|
||||
if (err.code === 'ECONNRESET') {
|
||||
resolve();
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public addPackage(name: string) {
|
||||
return this.putPackage(name, getPackage(name))
|
||||
.status(HTTP_STATUS.CREATED)
|
||||
.body_ok(API_MESSAGE.PKG_CREATED);
|
||||
}
|
||||
|
||||
public whoami() {
|
||||
return this.request({
|
||||
uri: '/-/whoami',
|
||||
})
|
||||
.status(HTTP_STATUS.OK)
|
||||
.then(function(body) {
|
||||
return body.username;
|
||||
});
|
||||
}
|
||||
|
||||
public ping() {
|
||||
return this.request({
|
||||
uri: '/-/ping',
|
||||
})
|
||||
.status(HTTP_STATUS.OK)
|
||||
.then(function(body) {
|
||||
return body;
|
||||
});
|
||||
}
|
||||
|
||||
public debug() {
|
||||
return this.request({
|
||||
uri: '/-/_debug',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
[HEADERS.CONTENT_TYPE]: HEADERS.JSON,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
17
test/e2e/setup.js
Normal file
17
test/e2e/setup.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const { green } = require('kleur');
|
||||
const puppeteer = require('puppeteer');
|
||||
const mkdirp = require('mkdirp');
|
||||
|
||||
const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup');
|
||||
|
||||
module.exports = async function() {
|
||||
console.log(green('Setup Puppeteer'));
|
||||
const browser = await puppeteer.launch({ headless: true, /* slowMo: 300 */ args: ['--no-sandbox'] });
|
||||
global.__BROWSER__ = browser;
|
||||
mkdirp.sync(DIR);
|
||||
fs.writeFileSync(path.join(DIR, 'wsEndpoint'), browser.wsEndpoint());
|
||||
};
|
||||
13
test/e2e/teardown.js
Normal file
13
test/e2e/teardown.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const { green } = require('kleur');
|
||||
const rimraf = require('rimraf');
|
||||
|
||||
const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup');
|
||||
|
||||
module.exports = async function() {
|
||||
console.log(green('Teardown Puppeteer'));
|
||||
await global.__BROWSER__.close();
|
||||
rimraf.sync(DIR);
|
||||
};
|
||||
27
test/jest.config.e2e.js
Normal file
27
test/jest.config.e2e.js
Normal file
@@ -0,0 +1,27 @@
|
||||
module.exports = {
|
||||
name: 'verdaccio-e2e-jest',
|
||||
verbose: true,
|
||||
collectCoverage: false,
|
||||
globalSetup: './e2e/pre-setup.js',
|
||||
globalTeardown: './e2e/teardown.js',
|
||||
testEnvironment: './e2e/puppeteer_environment.js',
|
||||
testRegex: '(/e2e.*\\.spec)\\.js',
|
||||
modulePathIgnorePatterns: [
|
||||
'<rootDir>/unit/partials/mock-store/.*/package.json',
|
||||
'<rootDir>/functional/store/.*/package.json',
|
||||
'<rootDir>/unit/partials/store/.*/package.json',
|
||||
'<rootDir>/../coverage',
|
||||
'<rootDir>/../docs',
|
||||
'<rootDir>/../debug',
|
||||
'<rootDir>/../scripts',
|
||||
'<rootDir>/../.circleci',
|
||||
'<rootDir>/../tools',
|
||||
'<rootDir>/../wiki',
|
||||
'<rootDir>/../systemd',
|
||||
'<rootDir>/../flow-typed',
|
||||
'<rootDir>unit/partials/mock-store/.*/package.json',
|
||||
'<rootDir>functional/store/.*/package.json',
|
||||
'<rootDir>/../build',
|
||||
'<rootDir>/../.vscode/',
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user