mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-03 10:49:57 -06:00
add benchmark tests
add more env variables add oidc auth flow add playbooks for better test reusability add more api endpoints update rollup config for test file transpilation update test folder structure split api package into smaller chunks
This commit is contained in:
@@ -11,5 +11,13 @@ $ yarn build
|
||||
|
||||
## How to run
|
||||
```console
|
||||
k6 run ./dist/TESTNAME.js
|
||||
k6 run ./dist/test/NAME_OF_TEST.js
|
||||
```
|
||||
|
||||
## Environment variables
|
||||
```console
|
||||
$ OC_OCIS_HOST=URL k6 run ...
|
||||
$ OC_OIDC_HOST=URL k6 run ...
|
||||
$ OC_OIDC=BOOL k6 run ...
|
||||
$ OC_TEST_FILE=STRING k6 run ...
|
||||
```
|
||||
@@ -44,6 +44,7 @@
|
||||
"typescript": "^3.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.20"
|
||||
"lodash": "^4.17.20",
|
||||
"query-string": "^6.13.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import resolve from '@rollup/plugin-node-resolve'
|
||||
import babel from 'rollup-plugin-babel'
|
||||
import { terser } from 'rollup-plugin-terser'
|
||||
import multiInput from 'rollup-plugin-multi-input';
|
||||
import path from 'path';
|
||||
import utils from '@rollup/pluginutils';
|
||||
import pkg from './package.json';
|
||||
|
||||
@@ -12,7 +11,7 @@ const extensions = ['.js', '.ts'];
|
||||
|
||||
export default [
|
||||
{
|
||||
input: ['src/test-*.ts'],
|
||||
input: ['src/test/**/*.ts'],
|
||||
external: utils.createFilter([
|
||||
'k6/**',
|
||||
...Object.keys(pkg.devDependencies),
|
||||
@@ -27,7 +26,7 @@ export default [
|
||||
],
|
||||
plugins: [
|
||||
multiInput({
|
||||
transformOutputPath: (output, input) => path.basename(output),
|
||||
transformOutputPath: (output, input) => `tests/${output.split('/').join('-')}`,
|
||||
}),
|
||||
json(),
|
||||
resolve(
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import encoding from 'k6/encoding';
|
||||
import {bytes} from "k6";
|
||||
import http, {RefinedResponse, ResponseType} from "k6/http";
|
||||
import * as defaults from "./defaults";
|
||||
import * as types from "./types";
|
||||
|
||||
export const uploadFile = <RT extends ResponseType | undefined>(account: types.Account, data: bytes, name: string): RefinedResponse<RT> => {
|
||||
return http.put(
|
||||
`https://${defaults.host.name}/remote.php/dav/files/${account.login}/${name}`,
|
||||
data as any,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Basic ${encoding.b64encode(`${account.login}:${account.password}`)}`,
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export const userInfo = <RT extends ResponseType | undefined>(account: any): RefinedResponse<RT> => {
|
||||
return http.get(
|
||||
`https://${defaults.host.name}/ocs/v1.php/cloud/users/${account.login}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Basic ${encoding.b64encode(`${account.login}:${account.password}`)}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
12
tests/k6/src/lib/api/api.ts
Normal file
12
tests/k6/src/lib/api/api.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import encoding from 'k6/encoding';
|
||||
import * as types from "../types";
|
||||
|
||||
export const headersDefault = ({credential}: { credential: types.Account | types.Token }): { [key: string]: string } => {
|
||||
const isOIDCGuard = (credential as types.Token).tokenType !== undefined;
|
||||
const authOIDC = credential as types.Token;
|
||||
const authBasic = credential as types.Account;
|
||||
|
||||
return {
|
||||
Authorization: isOIDCGuard ? `${authOIDC.tokenType} ${authOIDC.accessToken}` : `Basic ${encoding.b64encode(`${authBasic.login}:${authBasic.password}`)}`,
|
||||
}
|
||||
}
|
||||
45
tests/k6/src/lib/api/dav.ts
Normal file
45
tests/k6/src/lib/api/dav.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import http, {RefinedResponse, ResponseType} from "k6/http";
|
||||
import * as api from './api'
|
||||
import * as defaults from "../defaults";
|
||||
import * as types from "../types";
|
||||
|
||||
export const fileUpload = <RT extends ResponseType | undefined>(
|
||||
{credential, userName, asset}: { credential: types.Account | types.Token; userName: string; asset: types.Asset }
|
||||
): RefinedResponse<RT> => {
|
||||
return http.put(
|
||||
`${defaults.OC_OCIS_HOST}/remote.php/dav/files/${userName}/${asset.fileName}`,
|
||||
asset.bytes as any,
|
||||
{
|
||||
headers: {
|
||||
...api.headersDefault({credential})
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export const fileDownload = <RT extends ResponseType | undefined>(
|
||||
{credential, userName, fileName}: { credential: types.Account | types.Token; userName: string; fileName: string }
|
||||
): RefinedResponse<RT> => {
|
||||
return http.get(
|
||||
`${defaults.OC_OCIS_HOST}/remote.php/dav/files/${userName}/${fileName}`,
|
||||
{
|
||||
headers: {
|
||||
...api.headersDefault({credential})
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export const fileDelete = <RT extends ResponseType | undefined>(
|
||||
{credential, userName, fileName}: { credential: types.Account | types.Token; userName: string; fileName: string }
|
||||
): RefinedResponse<RT> => {
|
||||
return http.del(
|
||||
`${defaults.OC_OCIS_HOST}/remote.php/dav/files/${userName}/${fileName}`,
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
...api.headersDefault({credential})
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
3
tests/k6/src/lib/api/index.ts
Normal file
3
tests/k6/src/lib/api/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * as api from './api'
|
||||
export * as dav from './dav'
|
||||
export * as users from './users'
|
||||
17
tests/k6/src/lib/api/users.ts
Normal file
17
tests/k6/src/lib/api/users.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import http, {RefinedResponse, ResponseType} from "k6/http";
|
||||
import * as api from './api'
|
||||
import * as defaults from "../defaults";
|
||||
import * as types from "../types";
|
||||
|
||||
export const userInfo = <RT extends ResponseType | undefined>(
|
||||
{credential, userName}: { credential: types.Account | types.Token; userName: string; }
|
||||
): RefinedResponse<RT> => {
|
||||
return http.get(
|
||||
`${defaults.OC_OCIS_HOST}/ocs/v1.php/cloud/users/${userName}`,
|
||||
{
|
||||
headers: {
|
||||
...api.headersDefault({credential})
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
87
tests/k6/src/lib/auth.ts
Normal file
87
tests/k6/src/lib/auth.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import * as defaults from "./defaults";
|
||||
import http from "k6/http";
|
||||
import queryString from "query-string";
|
||||
import * as types from "./types";
|
||||
import {fail} from 'k6';
|
||||
import {get} from 'lodash'
|
||||
|
||||
export const oidc = (account: types.Account): types.Token => {
|
||||
const redirectUri = `${defaults.OC_OIDC_HOST}/oidc-callback.html`;
|
||||
|
||||
const logonUri = `${defaults.OC_OIDC_HOST}/signin/v1/identifier/_/logon`;
|
||||
const logonResponse = http.post(
|
||||
logonUri,
|
||||
JSON.stringify(
|
||||
{
|
||||
params: [account.login, account.password, '1'],
|
||||
hello: {
|
||||
scope: 'openid profile email',
|
||||
client_id: 'phoenix',
|
||||
redirect_uri: redirectUri,
|
||||
flow: 'oidc'
|
||||
},
|
||||
'state': 'vp42cf'
|
||||
},
|
||||
),
|
||||
{
|
||||
headers: {
|
||||
'Kopano-Konnect-XSRF': '1',
|
||||
Referer: defaults.OC_OIDC_HOST,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
);
|
||||
const authorizeURI = get(logonResponse.json(), 'hello.continue_uri');
|
||||
|
||||
if (logonResponse.status != 200 || !authorizeURI) {
|
||||
fail(logonUri);
|
||||
}
|
||||
|
||||
const authorizeUri = `${authorizeURI}?${
|
||||
queryString.stringify(
|
||||
{
|
||||
client_id: 'phoenix',
|
||||
prompt: 'none',
|
||||
redirect_uri: redirectUri,
|
||||
response_mode: 'query',
|
||||
response_type: 'code',
|
||||
scope: 'openid profile email',
|
||||
},
|
||||
)
|
||||
}`;
|
||||
const authorizeResponse = http.get(
|
||||
authorizeUri,
|
||||
{
|
||||
redirects: 0,
|
||||
},
|
||||
)
|
||||
const authCode = get(queryString.parseUrl(authorizeResponse.headers.Location), 'query.code')
|
||||
|
||||
if (authorizeResponse.status != 302 || !authCode) {
|
||||
fail(authorizeURI);
|
||||
}
|
||||
|
||||
const tokenUrl = `${defaults.OC_OIDC_HOST}/konnect/v1/token`;
|
||||
const tokenResponse = http.post(
|
||||
tokenUrl,
|
||||
{
|
||||
client_id: 'phoenix',
|
||||
code: authCode,
|
||||
redirect_uri: redirectUri,
|
||||
grant_type: 'authorization_code'
|
||||
}
|
||||
)
|
||||
|
||||
const token = {
|
||||
accessToken: get(tokenResponse.json(), 'access_token'),
|
||||
tokenType: get(tokenResponse.json(), 'token_type'),
|
||||
idToken: get(tokenResponse.json(), 'id_token'),
|
||||
expiresIn: get(tokenResponse.json(), 'expires_in'),
|
||||
}
|
||||
|
||||
if (tokenResponse.status != 200 || !token.accessToken || !token.tokenType || !token.idToken || !token.expiresIn) {
|
||||
fail(authorizeURI);
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
@@ -1,10 +1,18 @@
|
||||
import * as types from './types';
|
||||
import {Options} from "k6/options";
|
||||
|
||||
export const host = {
|
||||
name: __ENV.OC_HOST_NAME || 'localhost:9200'
|
||||
const ocTestFile = '../_files/' + (__ENV.OC_TEST_FILE || 'kb_50.jpg').split('/').pop()
|
||||
export const OC_OCIS_HOST = __ENV.OC_OCIS_HOST || 'https://localhost:9200'
|
||||
export const OC_OIDC_HOST = __ENV.OC_OIDC_HOST || OC_OCIS_HOST
|
||||
export const OC_OIDC = __ENV.OC_OIDC === 'true' || false
|
||||
export const OC_TEST_FILE = {
|
||||
fileName: ocTestFile,
|
||||
bytes: open(ocTestFile, 'b'),
|
||||
}
|
||||
|
||||
export const accounts: { [key: string]: types.Account; } = {
|
||||
export const k6OptionsDefault: Options = {
|
||||
insecureSkipTLSVerify: true,
|
||||
};
|
||||
export const knownAccounts: { [key: string]: types.Account; } = {
|
||||
einstein: {
|
||||
login: 'einstein',
|
||||
password: 'relativity',
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
export * as defaults from './defaults'
|
||||
export * as api from './api'
|
||||
export * as utils from './utils'
|
||||
export * as playbook from './playbook'
|
||||
export * as auth from './auth'
|
||||
export * as defaults from './defaults'
|
||||
export * as types from './types'
|
||||
export * as utils from './utils'
|
||||
|
||||
70
tests/k6/src/lib/playbook/dav.ts
Normal file
70
tests/k6/src/lib/playbook/dav.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import {Gauge, Trend} from "k6/metrics";
|
||||
import * as api from "../api";
|
||||
import * as utils from "../utils";
|
||||
import {bytes, check} from "k6";
|
||||
import * as types from "../types";
|
||||
|
||||
export const fileUpload = () => {
|
||||
const fileUploadTrend = new Trend('occ_file_upload_trend', true);
|
||||
const fileUploadErrorRate = new Gauge('occ_file_upload_error_rate');
|
||||
|
||||
return ({credential, userName, asset}: { credential: types.Account | types.Token; userName: string; asset: types.Asset }): string => {
|
||||
const fileName = `upload-${userName}-${__VU}-${__ITER}.${utils.extension(asset.fileName)}`;
|
||||
const uploadResponse = api.dav.fileUpload({
|
||||
credential: credential as any,
|
||||
asset: {
|
||||
fileName,
|
||||
bytes: asset.bytes,
|
||||
},
|
||||
userName,
|
||||
});
|
||||
|
||||
check(uploadResponse, {
|
||||
'file upload status is 201': () => uploadResponse.status === 201,
|
||||
}) || fileUploadErrorRate.add(1);
|
||||
|
||||
fileUploadTrend.add(uploadResponse.timings.duration)
|
||||
|
||||
return fileName
|
||||
}
|
||||
};
|
||||
|
||||
export const fileDelete = () => {
|
||||
const fileDeleteTrend = new Trend('occ_file_delete_trend', true);
|
||||
const fileDeleteErrorRate = new Gauge('occ_file_delete_error_rate');
|
||||
|
||||
return ({credential, userName, fileName}: { credential: types.Account | types.Token, userName: string; fileName: string }) => {
|
||||
const deleteResponse = api.dav.fileDelete({
|
||||
credential: credential as any,
|
||||
fileName,
|
||||
userName,
|
||||
});
|
||||
|
||||
check(deleteResponse, {
|
||||
'file delete status is 204': () => deleteResponse.status === 204,
|
||||
}) || fileDeleteErrorRate.add(1);
|
||||
|
||||
fileDeleteTrend.add(deleteResponse.timings.duration)
|
||||
}
|
||||
};
|
||||
|
||||
export const fileDownload = () => {
|
||||
const fileDownloadTrend = new Trend('occ_file_download_trend', true);
|
||||
const fileDownloadErrorRate = new Gauge('occ_file_download_error_rate');
|
||||
|
||||
return ({credential, userName, fileName}: { credential: types.Account | types.Token, userName: string; fileName: string }): bytes => {
|
||||
const downloadResponse = api.dav.fileDownload({
|
||||
credential: credential as any,
|
||||
fileName,
|
||||
userName,
|
||||
});
|
||||
|
||||
check(downloadResponse, {
|
||||
'file download status is 200': () => downloadResponse.status === 200,
|
||||
}) || fileDownloadErrorRate.add(1);
|
||||
|
||||
fileDownloadTrend.add(downloadResponse.timings.duration)
|
||||
|
||||
return downloadResponse.body as bytes
|
||||
}
|
||||
};
|
||||
1
tests/k6/src/lib/playbook/index.ts
Normal file
1
tests/k6/src/lib/playbook/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * as dav from './dav'
|
||||
@@ -1,3 +1,17 @@
|
||||
import {bytes} from "k6";
|
||||
|
||||
export interface Asset {
|
||||
bytes: bytes;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
export interface Token {
|
||||
accessToken: string;
|
||||
tokenType: string;
|
||||
idToken: string;
|
||||
expiresIn: string;
|
||||
}
|
||||
|
||||
export interface Account {
|
||||
login: string
|
||||
password: string
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
export const randomString = (): string => {
|
||||
return Math.random().toString(36).slice(2)
|
||||
}
|
||||
}
|
||||
|
||||
export const extension = (p: string): string | undefined => {
|
||||
return (p.split('/').pop())!.split('.').pop()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import {sleep, check} from 'k6';
|
||||
import {Options} from "k6/options";
|
||||
import {defaults, api} from "./lib";
|
||||
|
||||
const files = {
|
||||
'kb_50.jpg': open('./_files/kb_50.jpg', 'b'),
|
||||
}
|
||||
|
||||
export let options: Options = {
|
||||
insecureSkipTLSVerify: true,
|
||||
iterations: 100,
|
||||
vus: 100,
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const res = api.uploadFile(
|
||||
defaults.accounts.einstein,
|
||||
files['kb_50.jpg'],
|
||||
`kb_50-${__VU}-${__ITER}.jpg`,
|
||||
);
|
||||
|
||||
check(res, {
|
||||
'status is 204': () => res.status === 204,
|
||||
});
|
||||
|
||||
sleep(1);
|
||||
};
|
||||
53
tests/k6/src/test/benchmark/file-download.ts
Normal file
53
tests/k6/src/test/benchmark/file-download.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import {defaults, playbook} from '../../lib'
|
||||
import {Options} from 'k6/options';
|
||||
import {sleep} from "k6";
|
||||
import * as auth from "../../lib/auth";
|
||||
import * as types from "../../lib/types";
|
||||
|
||||
interface dataI {
|
||||
credential: types.Account | types.Token;
|
||||
}
|
||||
|
||||
export const options: Options = {
|
||||
...defaults.k6OptionsDefault,
|
||||
iterations: 1,
|
||||
vus: 1,
|
||||
};
|
||||
const account = defaults.knownAccounts.einstein;
|
||||
const playbooks = {
|
||||
fileUpload: playbook.dav.fileUpload(),
|
||||
fileDownload: playbook.dav.fileDownload(),
|
||||
fileDelete: playbook.dav.fileDelete(),
|
||||
}
|
||||
export const setup = (): dataI => {
|
||||
return {
|
||||
credential: defaults.OC_OIDC ? auth.oidc(account) : account,
|
||||
}
|
||||
}
|
||||
export default (data: dataI) => {
|
||||
const credential = data.credential;
|
||||
const userName = account.login;
|
||||
const fileName = playbooks.fileUpload({
|
||||
credential,
|
||||
userName,
|
||||
asset: defaults.OC_TEST_FILE
|
||||
});
|
||||
|
||||
sleep(1)
|
||||
|
||||
playbooks.fileDownload({
|
||||
credential,
|
||||
userName,
|
||||
fileName,
|
||||
});
|
||||
|
||||
sleep(1)
|
||||
|
||||
playbooks.fileDelete({
|
||||
credential,
|
||||
userName,
|
||||
fileName,
|
||||
});
|
||||
|
||||
sleep(1)
|
||||
};
|
||||
44
tests/k6/src/test/benchmark/file-upload.ts
Normal file
44
tests/k6/src/test/benchmark/file-upload.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import {defaults, playbook} from '../../lib'
|
||||
import {Options} from 'k6/options';
|
||||
import {sleep} from "k6";
|
||||
import * as auth from "../../lib/auth";
|
||||
import * as types from "../../lib/types";
|
||||
|
||||
interface dataI {
|
||||
credential: types.Account | types.Token;
|
||||
}
|
||||
|
||||
export const options: Options = {
|
||||
...defaults.k6OptionsDefault,
|
||||
iterations: 1,
|
||||
vus: 1,
|
||||
};
|
||||
const account = defaults.knownAccounts.einstein;
|
||||
const playbooks = {
|
||||
fileUpload: playbook.dav.fileUpload(),
|
||||
fileDelete: playbook.dav.fileDelete(),
|
||||
}
|
||||
export const setup = (): dataI => {
|
||||
return {
|
||||
credential: defaults.OC_OIDC ? auth.oidc(account) : account,
|
||||
}
|
||||
}
|
||||
export default (data: dataI) => {
|
||||
const credential = data.credential;
|
||||
const userName = account.login;
|
||||
const fileName = playbooks.fileUpload({
|
||||
credential,
|
||||
userName,
|
||||
asset: defaults.OC_TEST_FILE
|
||||
});
|
||||
|
||||
sleep(1)
|
||||
|
||||
playbooks.fileDelete({
|
||||
credential,
|
||||
userName,
|
||||
fileName,
|
||||
});
|
||||
|
||||
sleep(1)
|
||||
};
|
||||
10
tests/k6/src/test/issue/github/ocis/162.ts
Normal file
10
tests/k6/src/test/issue/github/ocis/162.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import {Options} from 'k6/options';
|
||||
import * as uploadFilesBenchmark from '../../../benchmark/file-upload'
|
||||
|
||||
export const options: Options = {
|
||||
...uploadFilesBenchmark.options,
|
||||
iterations: 200,
|
||||
vus: 50,
|
||||
};
|
||||
export const {setup} = uploadFilesBenchmark;
|
||||
export default uploadFilesBenchmark.default;
|
||||
@@ -4850,6 +4850,15 @@ qs@~6.5.2:
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
||||
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
|
||||
|
||||
query-string@^6.13.7:
|
||||
version "6.13.7"
|
||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.13.7.tgz#af53802ff6ed56f3345f92d40a056f93681026ee"
|
||||
integrity sha512-CsGs8ZYb39zu0WLkeOhe0NMePqgYdAuCqxOYKDR5LVCytDZYMGx3Bb+xypvQvPHVPijRXB0HZNFllCzHRe4gEA==
|
||||
dependencies:
|
||||
decode-uri-component "^0.2.0"
|
||||
split-on-first "^1.0.0"
|
||||
strict-uri-encode "^2.0.0"
|
||||
|
||||
quick-lru@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8"
|
||||
@@ -5528,6 +5537,11 @@ spdx-license-ids@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz#c80757383c28abf7296744998cbc106ae8b854ce"
|
||||
integrity sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw==
|
||||
|
||||
split-on-first@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f"
|
||||
integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==
|
||||
|
||||
split-string@^3.0.1, split-string@^3.0.2:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
|
||||
@@ -5582,6 +5596,11 @@ stealthy-require@^1.1.1:
|
||||
resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
|
||||
integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
|
||||
|
||||
strict-uri-encode@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
|
||||
integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY=
|
||||
|
||||
string-argv@0.3.1:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da"
|
||||
|
||||
Reference in New Issue
Block a user