feat: add quotas (#4779)

adds possibilities to cap authenticated requests and execution seconds of actions on a defined intervall
This commit is contained in:
Elio Bischof
2023-02-15 02:52:11 +01:00
committed by GitHub
parent 45f6a4436e
commit 681541f41b
117 changed files with 4652 additions and 510 deletions

View File

@@ -1,7 +1,42 @@
import { defineConfig } from 'cypress';
import { Client } from "pg";
import { createServer } from 'http'
import { ZITADELWebhookEvent } from 'cypress/support/types';
const jwt = require('jsonwebtoken');
const privateKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAzi+FFSJL7f5yw4KTwzgMP34ePGycm/M+kT0M7V4Cgx5V3EaD
IvTQKTLfBaEB45zb9LtjIXzDw0rXRoS2hO6th+CYQCz3KCvh09C0IzxZiB2IS3H/
aT+5Bx9EFY+vnAkZjccbyG5YNRvmtOlnvIeIH7qZ0tEwkPfF5GEZNPJPtmy3UGV7
iofdVQS1xRj73+aMw5rvH4D8IdyiAC3VekIbpt0Vj0SUX3DwKtog337BzTiPk3aX
RF0sbFhQoqdJRI8NqgZjCwjq9yfI5tyxYswn+JGzHGdHvW3idODlmwEt5K2pasiR
IWK2OGfq+w0EcltQHabuqEPgZlmhCkRdNfixBwIDAQABAoIBAA9jNoBkRdxmH/R9
Wz+3gBqA9Aq4ZFuzJJk8QCm62V8ltWyyCnliYeKhPEm0QWrWOwghr/1AzW9Wt4g4
wVJcabD5TwODF5L0626eZcM3bsscwR44TMJzEgD5EWC2j3mKqFCPaoBj08tq4KXh
wW8tgjgz+eTk3cYD583qfTIZX1+SzSMBpetTBsssQtGhhOB/xPiuL7hi+fXmV2rh
8mc9X6+wJ5u3zepsyK0vBeEDmurD4ZUIXFrZ0WCB/wNkSW9VKyoH+RC1asQAgqTz
glJ/NPbDJSKGvSBQydoKkqoXx7MVJ8VObFddfgo4dtOoz6YCfUVBHt8qy+E5rz5y
CICjL/kCgYEA9MnHntVVKNXtEFZPo02xgCwS3eG27ZwjYgJ1ZkCHM5BuL4MS7qbr
743/POs1Ctaok0udHl1PFB4uAG0URnmkUnWzcoJYb6Plv03F0LRdsnfuhehfIxLP
nWvxSm5n21H4ytfxm0BWY09JkLDnJZtXrgTILbuqb9Wy6TmAvUaF2YUCgYEA16Ec
ywSaLVdqPaVpsTxi7XpRJAB2Isjp6RffNEecta4S0LL7s/IO3QXDH9SYpgmgCTah
3aXhpT4hIFlpg3eBjVfbOwgqub8DgirnSQyQt99edUtHIK+K8nMdGxz6X6pfTKzK
asSH7qPlt5tz1621vC0ocXSZR7zm99/FgwILwBsCgYBOsP8nJFV4By1qbxSy3qsN
FR4LjiAMSoFlZHzxHhVYkjmZtH1FkwuNuwwuPT6T+WW/1DLyK/Tb9se7A1XdQgV9
LLE/Qn/Dg+C7mvjYmuL0GHHpQkYzNDzh0m2DC/L/Il7kdn8I9anPyxFPHk9wW3vY
SVlAum+T/BLDvuSP9DfbMQKBgCc1j7PG8XYfOB1fj7l/volqPYjrYI/wssAE7Dxo
bTGIJrm2YhiVgmhkXNfT47IFfAlQ2twgBsjyZDmqqIoUWAVonV+9m29NMYkg3g+l
bkdRIa74ckWaRgzSK8+7VDfDFjMuFFyXwhP9z460gLsORkaie4Et75Vg3yrhkNvC
qnpTAoGBAMguDSWBbCewXnHlKGFpm+LH+OIvVKGEhtCSvfZojtNrg/JBeBebSL1n
mmT1cONO+0O5bz7uVaRd3JdnH2JFevY698zFfhVsjVCrm+fz31i5cxAgC39G2Lfl
YkTaa1AFLstnf348ZjuvBN3USUYZo3X3mxnS+uluVuRSGwIKsN0a
-----END RSA PRIVATE KEY-----`
let tokensCache = new Map<string,string>()
let webhookEvents = new Array<ZITADELWebhookEvent>()
export default defineConfig({
reporter: 'mochawesome',
@@ -17,23 +52,56 @@ export default defineConfig({
env: {
ORGANIZATION: process.env.CYPRESS_ORGANIZATION || 'zitadel',
BACKEND_URL: process.env.CYPRESS_BACKEND_URL || baseUrl().replace("/ui/console", "")
BACKEND_URL: backendUrl(),
WEBHOOK_HANDLER_PORT: webhookHandlerPort(),
WEBHOOK_HANDLER_HOST: process.env.CYPRESS_WEBHOOK_HANDLER_HOST || 'localhost',
},
e2e: {
baseUrl: baseUrl(),
experimentalSessionAndOrigin: true,
experimentalRunAllSpecs: true,
setupNodeEvents(on, config) {
startWebhookEventHandler()
on('task', {
safetoken({key, token}) {
tokensCache.set(key,token);
return null
}
})
on('task', {
},
loadtoken({key}): string | null {
return tokensCache.get(key) || null;
},
systemToken(): Promise<string> {
let iat = Math.floor(Date.now() / 1000);
let exp = iat + (999*12*30*24*60*60) // ~ 999 years
return jwt.sign({
"iss": "cypress",
"sub": "cypress",
"aud": backendUrl(),
"iat": iat,
"exp": exp
}, Buffer.from(privateKey, 'ascii').toString('ascii'), { algorithm: 'RS256' })
},
async runSQL(statement: string) {
const client = new Client({
connectionString: process.env.CYPRESS_DATABASE_CONNECTION_URL || 'postgresql://root@localhost:26257/zitadel'
});
return client.connect().then(() => {
return client.query(statement).then((result) => {
return client.end().then(() => {
return result
})
})
})
},
resetWebhookEvents() {
webhookEvents = []
return null
},
handledWebhookEvents(){
return webhookEvents
}
})
},
@@ -43,3 +111,31 @@ export default defineConfig({
function baseUrl(){
return process.env.CYPRESS_BASE_URL || 'http://localhost:8080/ui/console'
}
function backendUrl(){
return process.env.CYPRESS_BACKEND_URL || baseUrl().replace("/ui/console", "")
}
function webhookHandlerPort() {
return process.env.CYPRESS_WEBHOOK_HANDLER_PORT || '8900'
}
function startWebhookEventHandler() {
const port = webhookHandlerPort()
const server = createServer((req, res) => {
const chunks = [];
req.on("data", (chunk) => {
chunks.push(chunk);
});
req.on("end", () => {
webhookEvents.push(JSON.parse(Buffer.concat(chunks).toString()));
});
res.writeHead(200);
res.end()
});
server.listen(port, () => {
console.log(`Server is running on http://:${port}`);
});
}

View File

@@ -1,22 +1,26 @@
import { Apps, ensureProjectExists, ensureProjectResourceDoesntExist } from '../../support/api/projects';
import { apiAuth } from '../../support/api/apiauth';
import { Context } from 'support/commands';
const testProjectName = 'e2eprojectapplication';
const testAppName = 'e2eappundertest';
describe('applications', () => {
const testProjectName = 'e2eprojectapplication';
const testAppName = 'e2eappundertest';
beforeEach(() => {
apiAuth()
.as('api')
.then((api) => {
ensureProjectExists(api, testProjectName).as('projectId');
cy.context()
.as('ctx')
.then((ctx) => {
ensureProjectExists(ctx.api, testProjectName).as('projectId');
});
});
describe('add app', function () {
beforeEach(`ensure it doesn't exist already`, function () {
ensureProjectResourceDoesntExist(this.api, this.projectId, Apps, testAppName);
cy.visit(`/projects/${this.projectId}`);
describe('add app', () => {
beforeEach(`ensure it doesn't exist already`, () => {
cy.get<Context>('@ctx').then((ctx) => {
cy.get<string>('@projectId').then((projectId) => {
ensureProjectResourceDoesntExist(ctx.api, projectId, Apps, testAppName);
cy.visit(`/projects/${projectId}`);
});
});
});
it('add app', () => {

View File

@@ -1,25 +1,28 @@
import { apiAuth } from '../../support/api/apiauth';
import { ensureHumanUserExists, ensureUserDoesntExist } from '../../support/api/users';
import { loginname } from '../../support/login/users';
import { ensureDomainPolicy } from '../../support/api/policies';
import { Context } from 'support/commands';
describe('humans', () => {
const humansPath = `/users?type=human`;
beforeEach(() => {
cy.context().as('ctx');
});
[
{ mustBeDomain: false, addName: 'e2ehumanusernameaddGlobal', removeName: 'e2ehumanusernameremoveGlobal' },
{ mustBeDomain: false, addName: 'e2ehumanusernameadd@test.com', removeName: 'e2ehumanusernameremove@test.com' },
{ mustBeDomain: true, addName: 'e2ehumanusernameadd', removeName: 'e2ehumanusernameremove' },
// TODO:Changing the policy return 409 User already exists (SQL-M0dsf)
// { mustBeDomain: true, addName: 'e2ehumanusernameadd', removeName: 'e2ehumanusernameremove' },
].forEach((user) => {
beforeEach(() => {
apiAuth().as('api');
});
describe(`add "${user.addName}" with domain setting "${user.mustBeDomain}"`, () => {
beforeEach(`ensure it doesn't exist already`, function () {
ensureDomainPolicy(this.api, user.mustBeDomain, true, false);
ensureUserDoesntExist(this.api, user.addName);
cy.visit(humansPath);
beforeEach(`ensure it doesn't exist already`, () => {
cy.get<Context>('@ctx').then((ctx) => {
ensureUserDoesntExist(ctx.api, user.addName);
ensureDomainPolicy(ctx.api, user.mustBeDomain, true, false);
cy.visit(humansPath);
});
});
it('should add a user', () => {
@@ -44,19 +47,17 @@ describe('humans', () => {
});
describe(`remove "${user.removeName}" with domain setting "${user.mustBeDomain}"`, () => {
beforeEach('ensure it exists', function () {
ensureHumanUserExists(this.api, user.removeName);
beforeEach('ensure it exists', () => {
cy.get<Context>('@ctx').then((ctx) => {
ensureHumanUserExists(ctx.api, user.removeName);
});
cy.visit(humansPath);
});
let loginName = user.removeName;
if (user.mustBeDomain) {
loginName = loginname(user.removeName, Cypress.env('ORGANIZATION'));
}
it('should delete a human user', () => {
const rowSelector = `tr:contains(${user.removeName})`;
cy.get(rowSelector).find('[data-e2e="enabled-delete-button"]').click({ force: true });
cy.get('[data-e2e="confirm-dialog-input"]').focus().type(loginName);
cy.get('[data-e2e="confirm-dialog-input"]').focus().type(user.removeName);
cy.get('[data-e2e="confirm-dialog-button"]').click();
cy.shouldConfirmSuccess();
cy.shouldNotExist({

View File

@@ -0,0 +1,23 @@
import { getInstance } from 'support/api/instances';
import { ensureQuotaIsRemoved, removeQuota, Unit } from 'support/api/quota';
import { Context } from 'support/commands';
describe('api internationalization', () => {
beforeEach(() => {
cy.context()
.as('ctx')
.then((ctx) => {
ensureQuotaIsRemoved(ctx, Unit.ExecutionSeconds);
});
});
it('instance not found error should be translated', () => {
cy.get<Context>('@ctx').then((ctx) => {
removeQuota(ctx, Unit.ExecutionSeconds, false).then((res) => {
expect(res.body.message).to.contain('Quota not found for this unit');
});
getInstance(ctx.system, "this ID clearly doesn't exist", false).then((res) => {
expect(res.body.message).to.contain('Instance not found');
});
});
});
});

View File

@@ -1,25 +1,28 @@
import { apiAuth } from '../../support/api/apiauth';
import { ensureMachineUserExists, ensureUserDoesntExist } from '../../support/api/users';
import { loginname } from '../../support/login/users';
import { ensureDomainPolicy } from '../../support/api/policies';
import { Context } from 'support/commands';
describe('machines', () => {
const machinesPath = `/users?type=machine`;
beforeEach(() => {
apiAuth().as('api');
cy.context().as('ctx');
});
[
{ mustBeDomain: false, addName: 'e2emachineusernameaddGlobal', removeName: 'e2emachineusernameremoveGlobal' },
{ mustBeDomain: false, addName: 'e2emachineusernameadd@test.com', removeName: 'e2emachineusernameremove@test.com' },
{ mustBeDomain: true, addName: 'e2emachineusernameadd', removeName: 'e2emachineusernameremove' },
// TODO:Changing the policy return 409 User already exists (SQL-M0dsf)
// { mustBeDomain: true, addName: 'e2emachineusernameadd', removeName: 'e2emachineusernameremove' },
].forEach((machine) => {
describe(`add "${machine.addName}" with domain setting "${machine.mustBeDomain}"`, () => {
beforeEach(`ensure it doesn't exist already`, function () {
ensureDomainPolicy(this.api, machine.mustBeDomain, false, false);
ensureUserDoesntExist(this.api, machine.addName);
cy.visit(machinesPath);
beforeEach(`ensure it doesn't exist already`, () => {
cy.get<Context>('@ctx').then((ctx) => {
ensureUserDoesntExist(ctx.api, machine.addName);
ensureDomainPolicy(ctx.api, machine.mustBeDomain, false, false);
cy.visit(machinesPath);
});
});
it('should add a machine', () => {
@@ -41,9 +44,11 @@ describe('machines', () => {
});
describe(`remove "${machine.removeName}" with domain setting "${machine.mustBeDomain}"`, () => {
beforeEach('ensure it exists', function () {
ensureMachineUserExists(this.api, machine.removeName);
cy.visit(machinesPath);
beforeEach('ensure it exists', () => {
cy.get<Context>('@ctx').then((ctx) => {
ensureMachineUserExists(ctx.api, machine.removeName);
cy.visit(machinesPath);
});
});
let loginName = machine.removeName;

View File

@@ -1,26 +1,25 @@
import { ensureOrgExists, ensureOrgIsDefault, isDefaultOrg } from 'support/api/orgs';
import { apiAuth } from '../../support/api/apiauth';
import { v4 as uuidv4 } from 'uuid';
import { Context } from 'support/commands';
const orgPath = `/org`;
const orgNameOnCreation = 'e2eorgrename';
const testOrgNameChange = uuidv4();
beforeEach(() => {
cy.context().as('ctx');
});
describe('organizations', () => {
describe('rename', () => {
beforeEach(() => {
apiAuth()
.as('api')
.then((api) => {
ensureOrgExists(api, orgNameOnCreation)
.as('newOrgId')
.then((newOrgId) => {
cy.visit(`${orgPath}?org=${newOrgId}`).as('orgsite');
});
cy.get<Context>('@ctx').then((ctx) => {
ensureOrgExists(ctx, orgNameOnCreation).then((newOrgId) => {
cy.visit(`${orgPath}?org=${newOrgId}`).as('orgsite');
});
});
});
it('should rename the organization', () => {
cy.get('[data-e2e="actions"]').click();
cy.get('[data-e2e="rename"]', { timeout: 1000 }).should('be.visible').click();
@@ -31,24 +30,21 @@ describe('organizations', () => {
cy.visit(orgPath);
cy.get('[data-e2e="top-view-title"').should('contain', testOrgNameChange);
});
});
const orgOverviewPath = `/orgs`;
const initialDefaultOrg = 'e2eorgolddefault';
const orgNameForNewDefault = 'e2eorgnewdefault';
const orgOverviewPath = `/orgs`;
const initialDefaultOrg = 'e2eorgolddefault';
const orgNameForNewDefault = 'e2eorgnewdefault';
describe('set default org', () => {
beforeEach(() => {
apiAuth()
.as('api')
.then((api) => {
ensureOrgExists(api, orgNameForNewDefault)
describe('set default org', () => {
beforeEach(() => {
cy.get<Context>('@ctx').then((ctx) => {
ensureOrgExists(ctx, orgNameForNewDefault)
.as('newDefaultOrgId')
.then(() => {
ensureOrgExists(api, initialDefaultOrg)
ensureOrgExists(ctx, initialDefaultOrg)
.as('defaultOrg')
.then((id) => {
ensureOrgIsDefault(api, id)
ensureOrgIsDefault(ctx, id)
.as('orgWasDefault')
.then(() => {
cy.visit(`${orgOverviewPath}`).as('orgsite');
@@ -56,19 +52,24 @@ describe('organizations', () => {
});
});
});
});
it('should rename the organization', () => {
cy.get<Context>('@ctx').then((ctx) => {
const rowSelector = `tr:contains(${orgNameForNewDefault})`;
cy.get(rowSelector).find('[data-e2e="table-actions-button"]').click({ force: true });
cy.get('[data-e2e="set-default-button"]', { timeout: 1000 }).should('be.visible').click();
cy.shouldConfirmSuccess();
cy.get<string>('@newDefaultOrgId').then((newDefaultOrgId) => {
isDefaultOrg(ctx, newDefaultOrgId);
});
});
});
});
it('should rename the organization', function () {
const rowSelector = `tr:contains(${orgNameForNewDefault})`;
cy.get(rowSelector).find('[data-e2e="table-actions-button"]').click({ force: true });
cy.get('[data-e2e="set-default-button"]', { timeout: 1000 }).should('be.visible').click();
cy.shouldConfirmSuccess();
isDefaultOrg(this.api, this.newDefaultOrgId);
it('should add an organization with the personal account as org owner');
describe('changing the current organization', () => {
it('should update displayed organization details');
});
});
it('should add an organization with the personal account as org owner');
describe('changing the current organization', () => {
it('should update displayed organization details');
});
});

View File

@@ -6,36 +6,46 @@ import {
ensureHumanIsProjectMember,
} from 'support/api/members';
import { ensureOrgExists } from 'support/api/orgs';
import { ensureDomainPolicy } from 'support/api/policies';
import { ensureHumanUserExists, ensureUserDoesntExist } from 'support/api/users';
import { loginname } from 'support/login/users';
import { apiAuth } from '../../support/api/apiauth';
import { Context } from 'support/commands';
import { ensureProjectExists, ensureProjectResourceDoesntExist, Roles } from '../../support/api/projects';
describe('permissions', () => {
beforeEach(() => {
apiAuth().as('api');
cy.context()
.as('ctx')
.then((ctx) => {
ensureDomainPolicy(ctx.api, false, true, false);
});
});
describe('management', () => {
const testManagerLoginname = loginname('e2ehumanmanager', Cypress.env('ORGANIZATION'));
const testManagerUsername = 'e2ehumanmanager';
function testAuthorizations(
roles: string[],
beforeCreate: Mocha.HookFunction,
beforeMutate: Mocha.HookFunction,
navigate: Mocha.HookFunction,
beforeCreate: (ctx: Context) => void,
beforeMutate: (ctx: Context) => void,
navigate: () => void,
) {
beforeEach(function () {
ensureUserDoesntExist(this.api, testManagerLoginname);
ensureHumanUserExists(this.api, testManagerLoginname);
beforeEach(() => {
cy.get<Context>('@ctx').then((ctx) => {
ensureUserDoesntExist(ctx.api, testManagerUsername);
ensureHumanUserExists(ctx.api, testManagerUsername);
});
});
describe('create authorization', () => {
beforeEach(beforeCreate);
beforeEach(navigate);
beforeEach(() => {
cy.get<Context>('@ctx').then((ctx) => {
beforeCreate(ctx);
navigate();
});
});
it('should add a manager', () => {
cy.get('[data-e2e="add-member-button"]').click();
cy.get('[data-e2e="add-member-input"]').type(testManagerLoginname);
cy.get('[data-e2e="add-member-input"]').type(testManagerUsername);
cy.get('[data-e2e="user-option"]').first().click();
cy.contains('[data-e2e="role-checkbox"]', roles[0]).click();
cy.get('[data-e2e="confirm-add-member-button"]').click();
@@ -45,14 +55,15 @@ describe('permissions', () => {
});
describe('mutate authorization', () => {
const rowSelector = `tr:contains(${testManagerLoginname})`;
beforeEach(beforeMutate);
beforeEach(navigate);
const rowSelector = `tr:contains(${testManagerUsername})`;
beforeEach(() => {
cy.contains('[data-e2e="member-avatar"]', 'ee').click();
cy.get(rowSelector).as('managerRow');
cy.get<Context>('@ctx').then((ctx) => {
beforeMutate(ctx);
navigate();
cy.contains('[data-e2e="member-avatar"]', 'ee').click();
cy.get(rowSelector).as('managerRow');
});
});
it('should remove a manager', () => {
@@ -88,14 +99,14 @@ describe('permissions', () => {
testAuthorizations(
roles.map((role) => role.display),
function () {
ensureHumanIsNotOrgMember(this.api, testManagerLoginname);
function (ctx: Context) {
ensureHumanIsNotOrgMember(ctx.api, testManagerUsername);
},
function () {
ensureHumanIsNotOrgMember(this.api, testManagerLoginname);
function (ctx: Context) {
ensureHumanIsNotOrgMember(ctx.api, testManagerUsername);
ensureHumanIsOrgMember(
this.api,
testManagerLoginname,
ctx.api,
testManagerUsername,
roles.map((role) => role.internal),
);
},
@@ -108,12 +119,16 @@ describe('permissions', () => {
describe('projects', () => {
describe('owned projects', () => {
beforeEach(function () {
ensureProjectExists(this.api, 'e2eprojectpermission').as('projectId');
beforeEach(() => {
cy.get<Context>('@ctx').then((ctx) => {
ensureProjectExists(ctx.api, 'e2eprojectpermission').as('projectId');
});
});
const visitOwnedProject: Mocha.HookFunction = function () {
cy.visit(`/projects/${this.projectId}`);
const visitOwnedProject = () => {
cy.get<number>('@projectId').then((projectId) => {
cy.visit(`/projects/${projectId}`);
});
};
describe('authorizations', () => {
@@ -124,17 +139,21 @@ describe('permissions', () => {
testAuthorizations(
roles.map((role) => role.display),
function () {
ensureHumanIsNotProjectMember(this.api, this.projectId, testManagerLoginname);
function (ctx) {
cy.get<string>('@projectId').then((projectId) => {
ensureHumanIsNotProjectMember(ctx.api, projectId, testManagerUsername);
});
},
function () {
ensureHumanIsNotProjectMember(this.api, this.projectId, testManagerLoginname);
ensureHumanIsProjectMember(
this.api,
this.projectId,
testManagerLoginname,
roles.map((role) => role.internal),
);
function (ctx) {
cy.get<string>('@projectId').then((projectId) => {
ensureHumanIsNotProjectMember(ctx.api, projectId, testManagerUsername);
ensureHumanIsProjectMember(
ctx.api,
projectId,
testManagerUsername,
roles.map((role) => role.internal),
);
});
},
visitOwnedProject,
);
@@ -143,12 +162,15 @@ describe('permissions', () => {
describe('roles', () => {
const testRoleName = 'e2eroleundertestname';
beforeEach(function () {
ensureProjectResourceDoesntExist(this.api, this.projectId, Roles, testRoleName);
beforeEach(() => {
cy.get<Context>('@ctx').then((ctx) => {
cy.get<string>('@projectId').then((projectId) => {
ensureProjectResourceDoesntExist(ctx.api, projectId, Roles, testRoleName);
visitOwnedProject();
});
});
});
beforeEach(visitOwnedProject);
it('should add a role', () => {
cy.get('[data-e2e="sidenav-element-roles"]').click();
cy.get('[data-e2e="add-new-role"]').click();
@@ -164,21 +186,25 @@ describe('permissions', () => {
});
describe('granted projects', () => {
beforeEach(function () {
ensureOrgExists(this.api, 'e2eforeignorg')
.as('foreignOrgId')
.then((foreignOrgId) => {
ensureProjectExists(this.api, 'e2eprojectgrants', foreignOrgId)
.as('projectId')
.then((projectId) => {
ensureProjectGrantExists(this.api, foreignOrgId, projectId).as('grantId');
beforeEach(() => {
cy.get<Context>('@ctx').then((ctx) => {
ensureOrgExists(ctx, 'e2eforeignorg').then((foreignOrgId) => {
ensureProjectExists(ctx.api, 'e2eprojectgrants', foreignOrgId)
.as('foreignProjectId')
.then((foreignProjectId) => {
ensureProjectGrantExists(ctx, foreignOrgId, foreignProjectId).as('grantId');
});
});
});
});
const visitGrantedProject: Mocha.HookFunction = function () {
cy.visit(`/granted-projects/${this.projectId}/grant/${this.grantId}`);
};
function visitGrantedProject() {
cy.get<string>('@foreignProjectId').then((foreignProjectId) => {
cy.get<string>('@grantId').then((grantId) => {
cy.visit(`/granted-projects/${foreignProjectId}/grant/${grantId}`);
});
});
}
describe('authorizations', () => {
const roles = [
@@ -188,18 +214,26 @@ describe('permissions', () => {
testAuthorizations(
roles.map((role) => role.display),
function () {
ensureHumanIsNotProjectMember(this.api, this.projectId, testManagerLoginname, this.grantId);
function (ctx: Context) {
cy.get<string>('@foreignProjectId').then((foreignProjectId) => {
cy.get<string>('@grantId').then((grantId) => {
ensureHumanIsNotProjectMember(ctx.api, foreignProjectId, testManagerUsername, grantId);
});
});
},
function () {
ensureHumanIsNotProjectMember(this.api, this.projectId, testManagerLoginname, this.grantId);
ensureHumanIsProjectMember(
this.api,
this.projectId,
testManagerLoginname,
roles.map((role) => role.internal),
this.grantId,
);
function (ctx: Context) {
cy.get<string>('@foreignProjectId').then((foreignProjectId) => {
cy.get<string>('@grantId').then((grantId) => {
ensureHumanIsNotProjectMember(ctx.api, foreignProjectId, testManagerUsername, grantId);
ensureHumanIsProjectMember(
ctx.api,
foreignProjectId,
testManagerUsername,
roles.map((role) => role.internal),
grantId,
);
});
});
},
visitGrantedProject,
);
@@ -207,42 +241,42 @@ describe('permissions', () => {
});
});
});
});
describe('validations', () => {
describe('owned projects', () => {
describe('no ownership', () => {
it('a user without project global ownership can ...');
it('a user without project global ownership can not ...');
});
describe('project owner viewer global', () => {
it('a project owner viewer global additionally can ...');
it('a project owner viewer global still can not ...');
});
describe('project owner global', () => {
it('a project owner global additionally can ...');
it('a project owner global still can not ...');
});
describe('validations', () => {
describe('owned projects', () => {
describe('no ownership', () => {
it('a user without project global ownership can ...');
it('a user without project global ownership can not ...');
});
describe('project owner viewer global', () => {
it('a project owner viewer global additionally can ...');
it('a project owner viewer global still can not ...');
});
describe('project owner global', () => {
it('a project owner global additionally can ...');
it('a project owner global still can not ...');
});
});
describe('granted projects', () => {
describe('no ownership', () => {
it('a user without project grant ownership can ...');
it('a user without project grant ownership can not ...');
});
describe('project grant owner viewer', () => {
it('a project grant owner viewer additionally can ...');
it('a project grant owner viewer still can not ...');
});
describe('project grant owner', () => {
it('a project grant owner additionally can ...');
it('a project grant owner still can not ...');
});
describe('granted projects', () => {
describe('no ownership', () => {
it('a user without project grant ownership can ...');
it('a user without project grant ownership can not ...');
});
describe('organization', () => {
describe('org owner', () => {
it('a project owner global can ...');
it('a project owner global can not ...');
});
describe('project grant owner viewer', () => {
it('a project grant owner viewer additionally can ...');
it('a project grant owner viewer still can not ...');
});
describe('project grant owner', () => {
it('a project grant owner additionally can ...');
it('a project grant owner still can not ...');
});
});
describe('organization', () => {
describe('org owner', () => {
it('a project owner global can ...');
it('a project owner global can not ...');
});
});
});

View File

@@ -1,18 +1,20 @@
import { apiAuth } from '../../support/api/apiauth';
import { Context } from 'support/commands';
import { ensureProjectDoesntExist, ensureProjectExists } from '../../support/api/projects';
describe('projects', () => {
beforeEach(() => {
apiAuth().as('api');
cy.context().as('ctx');
});
const testProjectNameCreate = 'e2eprojectcreate';
const testProjectNameDelete = 'e2eprojectdelete';
describe('add project', () => {
beforeEach(`ensure it doesn't exist already`, function () {
ensureProjectDoesntExist(this.api, testProjectNameCreate);
cy.visit(`/projects`);
beforeEach(`ensure it doesn't exist already`, () => {
cy.get<Context>('@ctx').then((ctx) => {
ensureProjectDoesntExist(ctx.api, testProjectNameCreate);
cy.visit(`/projects`);
});
});
it('should add a project', () => {
@@ -26,9 +28,11 @@ describe('projects', () => {
});
describe('edit project', () => {
beforeEach('ensure it exists', function () {
ensureProjectExists(this.api, testProjectNameDelete);
cy.visit(`/projects`);
beforeEach('ensure it exists', () => {
cy.get<Context>('@ctx').then((ctx) => {
ensureProjectExists(ctx.api, testProjectNameDelete);
cy.visit(`/projects`);
});
});
describe('remove project', () => {

View File

@@ -0,0 +1,248 @@
import { addQuota, ensureQuotaIsAdded, ensureQuotaIsRemoved, removeQuota, Unit } from 'support/api/quota';
import { createHumanUser, ensureUserDoesntExist } from 'support/api/users';
import { Context } from 'support/commands';
import { ZITADELWebhookEvent } from 'support/types';
beforeEach(() => {
cy.context().as('ctx');
});
describe('quotas', () => {
describe('management', () => {
describe('add one quota', () => {
it('should add a quota only once per unit', () => {
cy.get<Context>('@ctx').then((ctx) => {
addQuota(ctx, Unit.AuthenticatedRequests, true, 1);
addQuota(ctx, Unit.AuthenticatedRequests, true, 1, undefined, undefined, undefined, false).then((res) => {
expect(res.status).to.equal(409);
});
});
});
describe('add two quotas', () => {
it('should add a quota for each unit', () => {
cy.get<Context>('@ctx').then((ctx) => {
addQuota(ctx, Unit.AuthenticatedRequests, true, 1);
addQuota(ctx, Unit.ExecutionSeconds, true, 1);
});
});
});
});
describe('edit', () => {
describe('remove one quota', () => {
beforeEach(() => {
cy.get<Context>('@ctx').then((ctx) => {
ensureQuotaIsAdded(ctx, Unit.AuthenticatedRequests, true, 1);
});
});
it('should remove a quota only once per unit', () => {
cy.get<Context>('@ctx').then((ctx) => {
removeQuota(ctx, Unit.AuthenticatedRequests);
});
cy.get<Context>('@ctx').then((ctx) => {
removeQuota(ctx, Unit.AuthenticatedRequests, false).then((res) => {
expect(res.status).to.equal(404);
});
});
});
describe('remove two quotas', () => {
beforeEach(() => {
cy.get<Context>('@ctx').then((ctx) => {
ensureQuotaIsAdded(ctx, Unit.AuthenticatedRequests, true, 1);
ensureQuotaIsAdded(ctx, Unit.ExecutionSeconds, true, 1);
});
});
it('should remove a quota for each unit', () => {
cy.get<Context>('@ctx').then((ctx) => {
removeQuota(ctx, Unit.AuthenticatedRequests);
removeQuota(ctx, Unit.ExecutionSeconds);
});
});
});
});
});
});
describe('usage', () => {
beforeEach(() => {
cy.get<Context>('@ctx')
.then((ctx) => {
return [
`${ctx.api.oidcBaseURL}/userinfo`,
`${ctx.api.authBaseURL}/users/me`,
`${ctx.api.mgmtBaseURL}/iam`,
`${ctx.api.adminBaseURL}/instances/me`,
`${ctx.api.oauthBaseURL}/keys`,
`${ctx.api.samlBaseURL}/certificate`,
];
})
.as('authenticatedUrls');
});
describe('authenticated requests', () => {
const testUserName = 'shouldNotBeCreated';
beforeEach(() => {
cy.get<Array<string>>('@authenticatedUrls').then((urls) => {
cy.get<Context>('@ctx').then((ctx) => {
ensureUserDoesntExist(ctx.api, testUserName);
ensureQuotaIsAdded(ctx, Unit.AuthenticatedRequests, true, urls.length);
cy.task('runSQL', `TRUNCATE logstore.access;`);
});
});
});
it('authenticated requests are limited', () => {
cy.get<Array<string>>('@authenticatedUrls').then((urls) => {
cy.get<Context>('@ctx').then((ctx) => {
const start = new Date();
urls.forEach((url) => {
cy.request({
url: url,
method: 'GET',
auth: {
bearer: ctx.api.token,
},
});
});
const expiresMax = new Date();
expiresMax.setMinutes(expiresMax.getMinutes() + 2);
cy.getCookie('zitadel.quota.limiting').then((cookie) => {
expect(cookie.value).to.equal('false');
const cookieExpiry = new Date();
cookieExpiry.setTime(cookie.expiry * 1000);
expect(cookieExpiry).to.be.within(start, expiresMax);
});
cy.request({
url: urls[0],
method: 'GET',
auth: {
bearer: ctx.api.token,
},
failOnStatusCode: false,
}).then((res) => {
expect(res.status).to.equal(429);
});
cy.getCookie('zitadel.quota.limiting').then((cookie) => {
expect(cookie.value).to.equal('true');
});
createHumanUser(ctx.api, testUserName, false).then((res) => {
expect(res.status).to.equal(429);
});
ensureQuotaIsRemoved(ctx, Unit.AuthenticatedRequests);
createHumanUser(ctx.api, testUserName);
});
});
});
});
describe('notifications', () => {
const callURL = `http://${Cypress.env('WEBHOOK_HANDLER_HOST')}:${Cypress.env('WEBHOOK_HANDLER_PORT')}/do_something`;
beforeEach(() => cy.task('resetWebhookEvents'));
const amount = 100;
const percent = 10;
const usage = 25;
describe('without repetition', () => {
beforeEach(() => {
cy.get<Context>('@ctx').then((ctx) => {
ensureQuotaIsAdded(ctx, Unit.AuthenticatedRequests, false, amount, [
{
callUrl: callURL,
percent: percent,
repeat: false,
},
]);
cy.task('runSQL', `TRUNCATE logstore.access;`);
});
});
it('fires once with the expected payload', () => {
cy.get<Array<string>>('@authenticatedUrls').then((urls) => {
cy.get<Context>('@ctx').then((ctx) => {
for (let i = 0; i < usage; i++) {
cy.request({
url: urls[0],
method: 'GET',
auth: {
bearer: ctx.api.token,
},
});
}
});
cy.waitUntil(() =>
cy.task<Array<ZITADELWebhookEvent>>('handledWebhookEvents').then((events) => {
if (events.length != 1) {
return false;
}
return Cypress._.matches(<ZITADELWebhookEvent>{
callURL: callURL,
threshold: percent,
unit: 1,
usage: percent,
})(events[0]);
}),
);
});
});
});
describe('with repetition', () => {
beforeEach(() => {
cy.get<Array<string>>('@authenticatedUrls').then((urls) => {
cy.get<Context>('@ctx').then((ctx) => {
ensureQuotaIsAdded(ctx, Unit.AuthenticatedRequests, false, amount, [
{
callUrl: callURL,
percent: percent,
repeat: true,
},
]);
cy.task('runSQL', `TRUNCATE logstore.access;`);
});
});
});
it('fires repeatedly with the expected payloads', () => {
cy.get<Array<string>>('@authenticatedUrls').then((urls) => {
cy.get<Context>('@ctx').then((ctx) => {
for (let i = 0; i < usage; i++) {
cy.request({
url: urls[0],
method: 'GET',
auth: {
bearer: ctx.api.token,
},
});
}
});
});
cy.waitUntil(() =>
cy.task<Array<ZITADELWebhookEvent>>('handledWebhookEvents').then((events) => {
if (events.length != 1) {
return false;
}
for (let i = 0; i < events.length; i++) {
const threshold = percent * (i + 1);
if (
!Cypress._.matches(<ZITADELWebhookEvent>{
callURL: callURL,
threshold: threshold,
unit: 1,
usage: threshold,
})(events[i])
) {
return false;
}
}
return true;
}),
);
});
});
});
});
});

View File

@@ -1,4 +1,5 @@
import { apiAuth, API } from '../../support/api/apiauth';
import { Context } from 'mocha';
import { apiAuth } from '../../support/api/apiauth';
import { Policy, resetPolicy } from '../../support/api/policies';
import { login, User } from '../../support/login/users';
@@ -7,8 +8,6 @@ describe('private labeling', () => {
[User.OrgOwner].forEach((user) => {
describe(`as user "${user}"`, () => {
let api: API;
beforeEach(() => {
login(user);
cy.visit(orgPath);
@@ -37,34 +36,27 @@ function customize(theme: string, user: User) {
});
it('should update a logo', () => {
cy.get('[data-e2e="image-part-logo"]')
.find('input')
.then(function (el) {
const blob = Cypress.Blob.base64StringToBlob(this.logo, 'image/png');
const file = new File([blob], 'images/logo.png', { type: 'image/png' });
const list = new DataTransfer();
cy.get<Context>('@ctx').then((ctx) => {
cy.get('[data-e2e="image-part-logo"]')
.find('input')
.then(function (el) {
const blob = Cypress.Blob.base64StringToBlob(ctx.logo, 'image/png');
const file = new File([blob], 'images/logo.png', { type: 'image/png' });
const list = new DataTransfer();
list.items.add(file);
const myFileList = list.files;
list.items.add(file);
const myFileList = list.files;
el[0].files = myFileList;
el[0].dispatchEvent(new Event('change', { bubbles: true }));
});
el[0].files = myFileList;
el[0].dispatchEvent(new Event('change', { bubbles: true }));
});
});
});
it('should delete a logo');
});
it('should update an icon');
it('should delete an icon');
it.skip('should update the background color', () => {
cy.contains('[data-e2e="color"]', 'Background Color').find('button').click(); // TODO: select data-e2e
cy.get('color-editable-input').find('input').clear().type('#ae44dc');
cy.get('[data-e2e="save-colors-button"]').click();
cy.get('[data-e2e="header-user-avatar"]').click();
cy.contains('Logout All Users').click(); // TODO: select data-e2e
login(User.LoginPolicyUser, undefined, true, null, () => {
cy.pause();
});
});
it('should update the background color');
it('should update the primary color');
it('should update the warning color');
it('should update the font color');

View File

@@ -1,21 +1,36 @@
import { login, User } from 'support/login/users';
import { API } from './types';
import { API, SystemAPI, Token } from './types';
const authHeaderKey = 'Authorization',
orgIdHeaderKey = 'x-zitadel-orgid';
orgIdHeaderKey = 'x-zitadel-orgid',
backendUrl = Cypress.env('BACKEND_URL');
export function apiAuth(): Cypress.Chainable<API> {
return login(User.IAMAdminUser, 'Password1!', false, true).then((token) => {
return <API>{
token: token,
mgmtBaseURL: `${Cypress.env('BACKEND_URL')}/management/v1`,
adminBaseURL: `${Cypress.env('BACKEND_URL')}/admin/v1`,
mgmtBaseURL: `${backendUrl}/management/v1`,
adminBaseURL: `${backendUrl}/admin/v1`,
authBaseURL: `${backendUrl}/auth/v1`,
assetsBaseURL: `${backendUrl}/assets/v1`,
oauthBaseURL: `${backendUrl}/oauth/v2`,
oidcBaseURL: `${backendUrl}/oidc/v1`,
samlBaseURL: `${backendUrl}/saml/v2`,
};
});
}
export function requestHeaders(api: API, orgId?: number): object {
const headers = { [authHeaderKey]: `Bearer ${api.token}` };
export function systemAuth(): Cypress.Chainable<SystemAPI> {
return cy.task('systemToken').then((token) => {
return <SystemAPI>{
token: token,
baseURL: `${backendUrl}/system/v1`,
};
});
}
export function requestHeaders(token: Token, orgId?: string): object {
const headers = { [authHeaderKey]: `Bearer ${token.token}` };
if (orgId) {
headers[orgIdHeaderKey] = orgId;
}

View File

@@ -1,20 +1,20 @@
import { requestHeaders } from './apiauth';
import { findFromList as mapFromList, searchSomething } from './search';
import { API, Entity, SearchResult } from './types';
import { API, Entity, SearchResult, Token } from './types';
export function ensureItemExists(
api: API,
token: Token,
searchPath: string,
findInList: (entity: Entity) => boolean,
createPath: string,
body: Entity,
orgId?: number,
orgId?: string,
newItemIdField: string = 'id',
searchItemIdField?: string,
): Cypress.Chainable<number> {
) {
return ensureSomething(
api,
() => searchSomething(api, searchPath, 'POST', mapFromList(findInList, searchItemIdField), orgId),
token,
() => searchSomething(token, searchPath, 'POST', mapFromList(findInList, searchItemIdField), orgId),
() => createPath,
'POST',
body,
@@ -25,15 +25,15 @@ export function ensureItemExists(
}
export function ensureItemDoesntExist(
api: API,
token: Token,
searchPath: string,
findInList: (entity: Entity) => boolean,
deletePath: (entity: Entity) => string,
orgId?: number,
orgId?: string,
): Cypress.Chainable<null> {
return ensureSomething(
api,
() => searchSomething(api, searchPath, 'POST', mapFromList(findInList), orgId),
token,
() => searchSomething(token, searchPath, 'POST', mapFromList(findInList), orgId),
deletePath,
'DELETE',
null,
@@ -47,8 +47,8 @@ export function ensureSetting(
mapResult: (entity: any) => SearchResult,
createPath: string,
body: any,
orgId?: number,
): Cypress.Chainable<number> {
orgId?: string,
): Cypress.Chainable<string> {
return ensureSomething(
api,
() => searchSomething(api, path, 'GET', mapResult, orgId),
@@ -79,38 +79,38 @@ function awaitDesired(
}
interface EnsuredResult {
id: number;
id: string;
sequence: number;
}
export function ensureSomething(
api: API,
token: Token,
search: () => Cypress.Chainable<SearchResult>,
apiPath: (entity: Entity) => string,
ensureMethod: string,
body: Entity,
expectEntity: (entity: Entity) => boolean,
mapId?: (body: any) => number,
orgId?: number,
): Cypress.Chainable<number> {
mapId?: (body: any) => string,
orgId?: string,
): Cypress.Chainable<string> {
return search()
.then<EnsuredResult>((sRes) => {
.then((sRes) => {
if (expectEntity(sRes.entity)) {
return cy.wrap({ id: sRes.id, sequence: sRes.sequence });
return cy.wrap(<EnsuredResult>{ id: sRes.id, sequence: sRes.sequence });
}
return cy
.request({
method: ensureMethod,
url: apiPath(sRes.entity),
headers: requestHeaders(api, orgId),
headers: requestHeaders(token, orgId),
body: body,
failOnStatusCode: false,
followRedirect: false,
})
.then((cRes) => {
expect(cRes.status).to.equal(200);
return {
return <EnsuredResult>{
id: mapId ? mapId(cRes.body) : undefined,
sequence: sRes.sequence,
};
@@ -118,7 +118,7 @@ export function ensureSomething(
})
.then((data) => {
return awaitDesired(90, expectEntity, search, data.sequence).then(() => {
return cy.wrap<number>(data.id);
return cy.wrap(data.id);
});
});
}

View File

@@ -1,18 +1,14 @@
import { Context } from 'support/commands';
import { ensureItemExists } from './ensure';
import { getOrgUnderTest } from './orgs';
import { API } from './types';
export function ensureProjectGrantExists(
api: API,
foreignOrgId: number,
foreignProjectId: number,
): Cypress.Chainable<number> {
return getOrgUnderTest(api).then((orgUnderTest) => {
export function ensureProjectGrantExists(ctx: Context, foreignOrgId: string, foreignProjectId: string) {
return getOrgUnderTest(ctx).then((orgUnderTest) => {
return ensureItemExists(
api,
`${api.mgmtBaseURL}/projectgrants/_search`,
ctx.api,
`${ctx.api.mgmtBaseURL}/projectgrants/_search`,
(grant: any) => grant.grantedOrgId == orgUnderTest && grant.projectId == foreignProjectId,
`${api.mgmtBaseURL}/projects/${foreignProjectId}/grants`,
`${ctx.api.mgmtBaseURL}/projects/${foreignProjectId}/grants`,
{ granted_org_id: orgUnderTest },
foreignOrgId,
'grantId',

View File

@@ -0,0 +1,31 @@
import { SystemAPI } from './types';
export function instanceUnderTest(api: SystemAPI): Cypress.Chainable<string> {
return cy
.request({
method: 'POST',
url: `${api.baseURL}/instances/_search`,
auth: {
bearer: api.token,
},
})
.then((res) => {
const instances = <Array<any>>res.body.result;
expect(instances.length).to.equal(
1,
'instanceUnderTest just supports running against an API with exactly one instance, yet',
);
return instances[0].id;
});
}
export function getInstance(api: SystemAPI, instanceId: string, failOnStatusCode = true) {
return cy.request({
method: 'GET',
url: `${api.baseURL}/instances/${instanceId}`,
auth: {
bearer: api.token,
},
failOnStatusCode: failOnStatusCode,
});
}

View File

@@ -2,7 +2,7 @@ import { ensureItemDoesntExist, ensureItemExists } from './ensure';
import { findFromList, searchSomething } from './search';
import { API } from './types';
export function ensureHumanIsNotOrgMember(api: API, username: string): Cypress.Chainable<number> {
export function ensureHumanIsNotOrgMember(api: API, username: string) {
return ensureItemDoesntExist(
api,
`${api.mgmtBaseURL}/orgs/me/members/_search`,
@@ -11,7 +11,7 @@ export function ensureHumanIsNotOrgMember(api: API, username: string): Cypress.C
);
}
export function ensureHumanIsOrgMember(api: API, username: string, roles: string[]): Cypress.Chainable<number> {
export function ensureHumanIsOrgMember(api: API, username: string, roles: string[]) {
return searchSomething(
api,
`${api.mgmtBaseURL}/users/_search`,
@@ -37,13 +37,13 @@ export function ensureHumanIsNotProjectMember(
api: API,
projectId: string,
username: string,
grantId?: number,
): Cypress.Chainable<number> {
grantId?: string,
): Cypress.Chainable<string> {
return ensureItemDoesntExist(
api,
`${api.mgmtBaseURL}/projects/${projectId}/${grantId ? `grants/${grantId}/` : ''}members/_search`,
(member: any) => (<string>member.preferredLoginName).startsWith(username),
(member) => `${api.mgmtBaseURL}/projects/${projectId}${grantId ? `grants/${grantId}/` : ''}/members/${member.userId}`,
(member) => `${api.mgmtBaseURL}/projects/${projectId}/${grantId ? `grants/${grantId}/` : ''}members/${member.userId}`,
);
}
@@ -52,8 +52,8 @@ export function ensureHumanIsProjectMember(
projectId: string,
username: string,
roles: string[],
grantId?: number,
): Cypress.Chainable<number> {
grantId?: string,
): Cypress.Chainable<string> {
return searchSomething(
api,
`${api.mgmtBaseURL}/users/_search`,

View File

@@ -7,7 +7,7 @@ export function ensureOIDCSettingsSet(
idTokenLifetime: number,
refreshTokenExpiration: number,
refreshTokenIdleExpiration: number,
): Cypress.Chainable<number> {
) {
return ensureSetting(
api,
`${api.adminBaseURL}/settings/oidc`,

View File

@@ -3,20 +3,21 @@ import { searchSomething } from './search';
import { API } from './types';
import { host } from '../login/users';
import { requestHeaders } from './apiauth';
import { Context } from 'support/commands';
export function ensureOrgExists(api: API, name: string): Cypress.Chainable<number> {
export function ensureOrgExists(ctx: Context, name: string) {
return ensureSomething(
api,
ctx.api,
() =>
searchSomething(
api,
encodeURI(`${api.mgmtBaseURL}/global/orgs/_by_domain?domain=${name}.${host(Cypress.config('baseUrl'))}`),
ctx.api,
encodeURI(`${ctx.api.mgmtBaseURL}/global/orgs/_by_domain?domain=${name}.${host(Cypress.config('baseUrl'))}`),
'GET',
(res) => {
return { entity: res.org, id: res.org?.id, sequence: parseInt(<string>res.org?.details?.sequence) };
},
),
() => `${api.mgmtBaseURL}/orgs`,
() => `${ctx.api.mgmtBaseURL}/orgs`,
'POST',
{ name: name },
(org) => org?.name === name,
@@ -24,13 +25,13 @@ export function ensureOrgExists(api: API, name: string): Cypress.Chainable<numbe
);
}
export function isDefaultOrg(api: API, orgId: number): Cypress.Chainable<boolean> {
export function isDefaultOrg(ctx: Context, orgId: string): Cypress.Chainable<boolean> {
console.log('huhu', orgId);
return cy
.request({
method: 'GET',
url: encodeURI(`${api.mgmtBaseURL}/iam`),
headers: requestHeaders(api, orgId),
url: encodeURI(`${ctx.api.mgmtBaseURL}/iam`),
headers: requestHeaders(ctx.api, orgId),
})
.then((res) => {
const { defaultOrgId } = res.body;
@@ -39,12 +40,12 @@ export function isDefaultOrg(api: API, orgId: number): Cypress.Chainable<boolean
});
}
export function ensureOrgIsDefault(api: API, orgId: number): Cypress.Chainable<boolean> {
export function ensureOrgIsDefault(ctx: Context, orgId: string): Cypress.Chainable<boolean> {
return cy
.request({
method: 'GET',
url: encodeURI(`${api.mgmtBaseURL}/iam`),
headers: requestHeaders(api, orgId),
url: encodeURI(`${ctx.api.mgmtBaseURL}/iam`),
headers: requestHeaders(ctx.api, orgId),
})
.then((res) => {
return res.body;
@@ -56,8 +57,8 @@ export function ensureOrgIsDefault(api: API, orgId: number): Cypress.Chainable<b
return cy
.request({
method: 'PUT',
url: `${api.adminBaseURL}/orgs/default/${orgId}`,
headers: requestHeaders(api, orgId),
url: `${ctx.api.adminBaseURL}/orgs/default/${orgId}`,
headers: requestHeaders(ctx.api, orgId),
failOnStatusCode: true,
followRedirect: false,
})
@@ -69,8 +70,8 @@ export function ensureOrgIsDefault(api: API, orgId: number): Cypress.Chainable<b
});
}
export function getOrgUnderTest(api: API): Cypress.Chainable<number> {
return searchSomething(api, `${api.mgmtBaseURL}/orgs/me`, 'GET', (res) => {
export function getOrgUnderTest(ctx: Context): Cypress.Chainable<number> {
return searchSomething(ctx.api, `${ctx.api.mgmtBaseURL}/orgs/me`, 'GET', (res) => {
return { entity: res.org, id: res.org.id, sequence: parseInt(<string>res.org.details.sequence) };
}).then((res) => res.entity.id);
}

View File

@@ -22,7 +22,7 @@ export function ensureDomainPolicy(
userLoginMustBeDomain: boolean,
validateOrgDomains: boolean,
smtpSenderAddressMatchesInstanceDomain: boolean,
): Cypress.Chainable<number> {
) {
return ensureSetting(
api,
`${api.adminBaseURL}/policies/domain`,

View File

@@ -1,7 +1,7 @@
import { ensureItemDoesntExist, ensureItemExists } from './ensure';
import { API } from './types';
import { API, Entity } from './types';
export function ensureProjectExists(api: API, projectName: string, orgId?: number): Cypress.Chainable<number> {
export function ensureProjectExists(api: API, projectName: string, orgId?: string) {
return ensureItemExists(
api,
`${api.mgmtBaseURL}/projects/_search`,
@@ -12,7 +12,7 @@ export function ensureProjectExists(api: API, projectName: string, orgId?: numbe
);
}
export function ensureProjectDoesntExist(api: API, projectName: string, orgId?: number): Cypress.Chainable<null> {
export function ensureProjectDoesntExist(api: API, projectName: string, orgId?: string) {
return ensureItemDoesntExist(
api,
`${api.mgmtBaseURL}/projects/_search`,
@@ -32,22 +32,22 @@ export const Roles = new ResourceType('roles', 'key', 'key');
export function ensureProjectResourceDoesntExist(
api: API,
projectId: number,
projectId: string,
resourceType: ResourceType,
resourceName: string,
orgId?: number,
orgId?: string,
): Cypress.Chainable<null> {
return ensureItemDoesntExist(
api,
`${api.mgmtBaseURL}/projects/${projectId}/${resourceType.resourcePath}/_search`,
(resource: any) => resource[resourceType.compareProperty] === resourceName,
(resource) =>
(resource: Entity) => resource[resourceType.compareProperty] === resourceName,
(resource: Entity) =>
`${api.mgmtBaseURL}/projects/${projectId}/${resourceType.resourcePath}/${resource[resourceType.identifierProperty]}`,
orgId,
);
}
export function ensureApplicationExists(api: API, projectId: number, appName: string): Cypress.Chainable<number> {
export function ensureApplicationExists(api: API, projectId: number, appName: string) {
return ensureItemExists(
api,
`${api.mgmtBaseURL}/projects/${projectId}/${Apps.resourcePath}/_search`,

View File

@@ -0,0 +1,84 @@
import { Context } from 'support/commands';
export enum Unit {
Unimplemented,
AuthenticatedRequests,
ExecutionSeconds,
}
interface notification {
percent: number;
repeat?: boolean;
callUrl: string;
}
export function addQuota(
ctx: Context,
unit: Unit = Unit.AuthenticatedRequests,
limit: boolean,
amount: number,
notifications?: Array<notification>,
from: Date = (() => {
const date = new Date();
date.setMonth(0, 1);
date.setMinutes(0, 0, 0);
// default to start of current year
return date;
})(),
intervalSeconds: string = `${315_576_000_000}s`, // proto max duration is 1000 years
failOnStatusCode = true,
): Cypress.Chainable<Cypress.Response<any>> {
return cy.request({
method: 'POST',
url: `${ctx.system.baseURL}/instances/${ctx.instanceId}/quotas`,
auth: {
bearer: ctx.system.token,
},
body: {
unit: unit,
amount: amount,
resetInterval: intervalSeconds,
limit: limit,
from: from,
notifications: notifications,
},
failOnStatusCode: failOnStatusCode,
});
}
export function ensureQuotaIsAdded(
ctx: Context,
unit: Unit,
limit: boolean,
amount?: number,
notifications?: Array<notification>,
from?: Date,
intervalSeconds?: string,
): Cypress.Chainable<null> {
return addQuota(ctx, unit, limit, amount, notifications, from, intervalSeconds, false).then((res) => {
if (!res.isOkStatusCode) {
expect(res.status).to.equal(409);
}
return null;
});
}
export function removeQuota(ctx: Context, unit: Unit, failOnStatusCode = true): Cypress.Chainable<Cypress.Response<any>> {
return cy.request({
method: 'DELETE',
url: `${ctx.system.baseURL}/instances/${ctx.instanceId}/quotas/${unit}`,
auth: {
bearer: ctx.system.token,
},
failOnStatusCode: failOnStatusCode,
});
}
export function ensureQuotaIsRemoved(ctx: Context, unit?: Unit): Cypress.Chainable<null> {
return removeQuota(ctx, unit, false).then((res) => {
if (!res.isOkStatusCode) {
expect(res.status).to.equal(404);
}
return null;
});
}

View File

@@ -1,18 +1,18 @@
import { requestHeaders } from './apiauth';
import { API, Entity, SearchResult } from './types';
import { API, Entity, SearchResult, Token } from './types';
export function searchSomething(
api: API,
token: Token,
searchPath: string,
method: string,
mapResult: (body: any) => SearchResult,
orgId?: number,
orgId?: string,
): Cypress.Chainable<SearchResult> {
return cy
.request({
method: method,
url: searchPath,
headers: requestHeaders(api, orgId),
headers: requestHeaders(token, orgId),
failOnStatusCode: method == 'POST',
})
.then((res) => {

View File

@@ -1,13 +1,25 @@
export interface API {
export interface Token {
token: string;
}
export interface API extends Token {
mgmtBaseURL: string;
adminBaseURL: string;
authBaseURL: string;
assetsBaseURL: string;
oidcBaseURL: string;
oauthBaseURL: string;
samlBaseURL: string;
}
export interface SystemAPI extends Token {
baseURL: string;
}
export type SearchResult = {
entity: Entity | null;
sequence: number;
id: number;
id: string;
};
// Entity is an object but not a function

View File

@@ -1,31 +1,23 @@
import { requestHeaders } from './apiauth';
import { ensureItemDoesntExist, ensureItemExists } from './ensure';
import { API } from './types';
export function ensureHumanUserExists(api: API, username: string): Cypress.Chainable<number> {
export function ensureHumanUserExists(api: API, username: string) {
return ensureItemExists(
api,
`${api.mgmtBaseURL}/users/_search`,
(user: any) => user.userName === username,
`${api.mgmtBaseURL}/users/human`,
{
...defaultHuman,
user_name: username,
profile: {
first_name: 'e2efirstName',
last_name: 'e2elastName',
},
email: {
email: 'e2e@email.ch',
},
phone: {
phone: '+41 123456789',
},
},
undefined,
'userId',
);
}
export function ensureMachineUserExists(api: API, username: string): Cypress.Chainable<number> {
export function ensureMachineUserExists(api: API, username: string) {
return ensureItemExists(
api,
`${api.mgmtBaseURL}/users/_search`,
@@ -41,7 +33,7 @@ export function ensureMachineUserExists(api: API, username: string): Cypress.Cha
);
}
export function ensureUserDoesntExist(api: API, username: string): Cypress.Chainable<null> {
export function ensureUserDoesntExist(api: API, username: string) {
return ensureItemDoesntExist(
api,
`${api.mgmtBaseURL}/users/_search`,
@@ -49,3 +41,31 @@ export function ensureUserDoesntExist(api: API, username: string): Cypress.Chain
(user) => `${api.mgmtBaseURL}/users/${user.id}`,
);
}
export function createHumanUser(api: API, username: string, failOnStatusCode = true) {
return cy.request({
method: 'POST',
url: `${api.mgmtBaseURL}/users/human`,
body: {
...defaultHuman,
user_name: username,
},
auth: {
bearer: api.token,
},
failOnStatusCode: failOnStatusCode,
});
}
const defaultHuman = {
profile: {
first_name: 'e2efirstName',
last_name: 'e2elastName',
},
email: {
email: 'e2e@email.ch',
},
phone: {
phone: '+41 123456789',
},
};

View File

@@ -1,29 +1,8 @@
import 'cypress-wait-until';
//
//namespace Cypress {
// interface Chainable {
// /**
// * Custom command that authenticates a user.
// *
// * @example cy.consolelogin('hodor', 'hodor1234')
// */
// consolelogin(username: string, password: string): void
// }
//}
//
//Cypress.Commands.add('consolelogin', { prevSubject: false }, (username: string, password: string) => {
//
// window.sessionStorage.removeItem("zitadel:access_token")
// cy.visit(Cypress.config('baseUrl')/ui/console).then(() => {
// // fill the fields and push button
// cy.get('#loginName').type(username, { log: false })
// cy.get('#submit-button').click()
// cy.get('#password').type(password, { log: false })
// cy.get('#submit-button').click()
// cy.location('pathname', {timeout: 5 * 1000}).should('eq', '/');
// })
//})
//
import { apiAuth, systemAuth } from './api/apiauth';
import { API, SystemAPI } from './api/types';
import { ensureQuotaIsRemoved, Unit } from './api/quota';
import { instanceUnderTest } from './api/instances';
interface ShouldNotExistOptions {
selector: string;
@@ -46,7 +25,13 @@ declare global {
/**
* Custom command that waits until the selector finds zero elements.
*/
shouldNotExist(options: ShouldNotExistOptions): Cypress.Chainable<null>;
shouldNotExist(options?: ShouldNotExistOptions): Cypress.Chainable<null>;
/**
* Custom command that ensures a reliable testing context and returns it
*/
context(): Cypress.Chainable<Context>;
/**
* Custom command that asserts success is printed after a change.
*/
@@ -55,6 +40,12 @@ declare global {
}
}
export interface Context {
api: API;
system: SystemAPI;
instanceId: number;
}
Cypress.Commands.add('clipboardMatches', { prevSubject: false }, (pattern: RegExp | string) => {
/* doesn't work reliably
return cy.window()
@@ -106,3 +97,35 @@ Cypress.Commands.add('shouldConfirmSuccess', { prevSubject: false }, () => {
cy.shouldNotExist({ selector: '.data-e2e-failure' });
cy.get('.data-e2e-success');
});
Cypress.Commands.add('context', { prevSubject: false }, () => {
return systemAuth().then((system) => {
return instanceUnderTest(system).then((instanceId) => {
return ensureQuotaIsRemoved(
{
system: system,
api: null,
instanceId: instanceId,
},
Unit.AuthenticatedRequests,
).then(() => {
return ensureQuotaIsRemoved(
{
system: system,
api: null,
instanceId: instanceId,
},
Unit.ExecutionSeconds,
).then(() => {
return apiAuth().then((api) => {
return {
system: system,
api: api,
instanceId: instanceId,
};
});
});
});
});
});
});

View File

@@ -15,6 +15,4 @@
// Import commands.js using ES2015 syntax:
import './commands';
// Alternatively you can use CommonJS syntax:
// require('./commands')
import './types';

View File

@@ -1,10 +0,0 @@
require('cypress-terminal-report/src/installLogsCollector')();
/**
* @type {Cypress.PluginConfig}
*/
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
};
//import './commands'

View File

@@ -82,8 +82,10 @@ export function login(
onAuthenticated ? onAuthenticated() : null;
cy.visit('/');
cy.get('[data-e2e=authenticated-welcome]', {
timeout: 10_000,
timeout: 50_000,
});
},
{

View File

@@ -0,0 +1,10 @@
let webhookEventSchema = {
unit: 0,
id: '',
callURL: '',
periodStart: new Date(),
threshold: 0,
usage: 0,
};
export type ZITADELWebhookEvent = typeof webhookEventSchema;

View File

@@ -0,0 +1,31 @@
FirstInstance:
Org:
Human:
PasswordChangeRequired: false
LogStore:
Access:
Database:
Enabled: true
Debounce:
MinFrequency: 0s
MaxBulkSize: 0
Execution:
Database:
Enabled: true
Stdout:
Enabled: false
Quotas:
Access:
ExhaustedCookieKey: "zitadel.quota.limiting"
ExhaustedCookieMaxAge: "60s"
DefaultInstance:
LoginPolicy:
MfaInitSkipLifetime: 0
SystemAPIUsers:
- cypress:
KeyData: "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF6aStGRlNKTDdmNXl3NEtUd3pnTQpQMzRlUEd5Y20vTStrVDBNN1Y0Q2d4NVYzRWFESXZUUUtUTGZCYUVCNDV6YjlMdGpJWHpEdzByWFJvUzJoTzZ0CmgrQ1lRQ3ozS0N2aDA5QzBJenhaaUIySVMzSC9hVCs1Qng5RUZZK3ZuQWtaamNjYnlHNVlOUnZtdE9sbnZJZUkKSDdxWjB0RXdrUGZGNUdFWk5QSlB0bXkzVUdWN2lvZmRWUVMxeFJqNzMrYU13NXJ2SDREOElkeWlBQzNWZWtJYgpwdDBWajBTVVgzRHdLdG9nMzM3QnpUaVBrM2FYUkYwc2JGaFFvcWRKUkk4TnFnWmpDd2pxOXlmSTV0eXhZc3duCitKR3pIR2RIdlczaWRPRGxtd0V0NUsycGFzaVJJV0syT0dmcSt3MEVjbHRRSGFidXFFUGdabG1oQ2tSZE5maXgKQndJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg=="

View File

@@ -2,20 +2,16 @@ version: '3.8'
services:
zitadel:
user: '$UID'
restart: 'always'
image: '${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:latest}'
command: 'start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled'
environment:
ZITADEL_DATABASE_COCKROACH_HOST: db
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED: false
ZITADEL_DEFAULTINSTANCE_LOGINPOLICY_MFAINITSKIPLIFETIME: 0
command: 'start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled --config /zitadel.yaml --steps /zitadel.yaml'
depends_on:
db:
condition: 'service_healthy'
ports:
- '8080:8080'
networks:
- zitadel
volumes:
- ./docker-compose-zitadel.yaml:/zitadel.yaml
network_mode: host
db:
restart: 'always'
@@ -27,11 +23,7 @@ services:
timeout: '30s'
retries: 5
start_period: '20s'
ports:
- '9090:8080'
- '26257:26257'
networks:
- zitadel
network_mode: host
prepare:
image: node:18-alpine3.15
@@ -43,7 +35,7 @@ services:
network_mode: host
e2e:
image: cypress/included:10.9.0
image: cypress/included:12.2.0
depends_on:
zitadel:
condition: 'service_started'

454
e2e/package-lock.json generated
View File

@@ -8,8 +8,11 @@
"name": "zitadel-e2e",
"version": "0.0.0",
"dependencies": {
"@types/pg": "^8.6.6",
"cypress-wait-until": "^1.7.2",
"jsonwebtoken": "^8.5.1",
"mochawesome": "^7.1.3",
"pg": "^8.8.0",
"prettier": "^2.7.1",
"typescript": "^4.8.4",
"uuid": "^9.0.0",
@@ -17,7 +20,7 @@
},
"devDependencies": {
"@types/node": "^18.8.3",
"cypress": "^10.9.0"
"cypress": "^12.2.0"
}
},
"node_modules/@colors/colors": {
@@ -121,8 +124,17 @@
"node_modules/@types/node": {
"version": "18.8.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.8.3.tgz",
"integrity": "sha512-0os9vz6BpGwxGe9LOhgP/ncvYN5Tx1fNcd2TM3rD/aCGBkysb+ZWpXEocG24h6ZzOi13+VB8HndAQFezsSOw1w==",
"dev": true
"integrity": "sha512-0os9vz6BpGwxGe9LOhgP/ncvYN5Tx1fNcd2TM3rD/aCGBkysb+ZWpXEocG24h6ZzOi13+VB8HndAQFezsSOw1w=="
},
"node_modules/@types/pg": {
"version": "8.6.6",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.6.tgz",
"integrity": "sha512-O2xNmXebtwVekJDD+02udOncjVcMZQuTEQEMpKJ0ZRf5E7/9JJX3izhKUcUifBkyKpljyUM6BTgy2trmviKlpw==",
"dependencies": {
"@types/node": "*",
"pg-protocol": "*",
"pg-types": "^2.2.0"
}
},
"node_modules/@types/sinonjs__fake-timers": {
"version": "8.1.1",
@@ -436,6 +448,19 @@
"node": "*"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
},
"node_modules/buffer-writer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
"integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==",
"engines": {
"node": ">=4"
}
},
"node_modules/cachedir": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz",
@@ -671,9 +696,9 @@
}
},
"node_modules/cypress": {
"version": "10.9.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.9.0.tgz",
"integrity": "sha512-MjIWrRpc+bQM9U4kSSdATZWZ2hUqHGFEQTF7dfeZRa4MnalMtc88FIE49USWP2ZVtfy5WPBcgfBX+YorFqGElA==",
"version": "12.2.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-12.2.0.tgz",
"integrity": "sha512-kvl95ri95KK8mAy++tEU/wUgzAOMiIciZSL97LQvnOinb532m7dGvwN0mDSIGbOd71RREtmT9o4h088RjK5pKw==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
@@ -724,7 +749,7 @@
"cypress": "bin/cypress"
},
"engines": {
"node": ">=12.0.0"
"node": "^14.0.0 || ^16.0.0 || >=18.0.0"
}
},
"node_modules/cypress-wait-until": {
@@ -819,6 +844,14 @@
"safer-buffer": "^2.1.0"
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@@ -1483,6 +1516,35 @@
"graceful-fs": "^4.1.6"
}
},
"node_modules/jsonwebtoken": {
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz",
"integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==",
"dependencies": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^5.6.0"
},
"engines": {
"node": ">=4",
"npm": ">=1.4.28"
}
},
"node_modules/jsonwebtoken/node_modules/semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"bin": {
"semver": "bin/semver"
}
},
"node_modules/jsprim": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz",
@@ -1498,6 +1560,25 @@
"verror": "1.10.0"
}
},
"node_modules/jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
"dependencies": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"dependencies": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/lazy-ass": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz",
@@ -1554,6 +1635,16 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
},
"node_modules/lodash.isempty": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz",
@@ -1564,11 +1655,26 @@
"resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz",
"integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw=="
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
},
"node_modules/lodash.isobject": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz",
"integrity": "sha512-3/Qptq2vr7WeJbB4KHUSKlq8Pl7ASXi3UG6CMbBm8WRtXi8+GHm7mKaU3urfpSEzWe2wCIChs6/sdocUsTKJiA=="
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
@@ -1577,8 +1683,7 @@
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"dev": true
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
},
"node_modules/log-symbols": {
"version": "4.1.0",
@@ -2004,6 +2109,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/packet-reader": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
"integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ=="
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -2042,6 +2152,80 @@
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"dev": true
},
"node_modules/pg": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.8.0.tgz",
"integrity": "sha512-UXYN0ziKj+AeNNP7VDMwrehpACThH7LUl/p8TDFpEUuSejCUIwGSfxpHsPvtM6/WXFy6SU4E5RG4IJV/TZAGjw==",
"dependencies": {
"buffer-writer": "2.0.0",
"packet-reader": "1.0.0",
"pg-connection-string": "^2.5.0",
"pg-pool": "^3.5.2",
"pg-protocol": "^1.5.0",
"pg-types": "^2.1.0",
"pgpass": "1.x"
},
"engines": {
"node": ">= 8.0.0"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-connection-string": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz",
"integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ=="
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.2.tgz",
"integrity": "sha512-His3Fh17Z4eg7oANLob6ZvH8xIVen3phEZh2QuyrIl4dQSDVEabNducv6ysROKpDNPSD+12tONZVWfSgMvDD9w==",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz",
"integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ=="
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
@@ -2063,6 +2247,41 @@
"node": ">=0.10.0"
}
},
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"engines": {
"node": ">=4"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/prettier": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
@@ -2314,6 +2533,14 @@
"node": ">=8"
}
},
"node_modules/split2": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz",
"integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/sshpk": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz",
@@ -2614,6 +2841,14 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"engines": {
"node": ">=0.4"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
@@ -2790,8 +3025,17 @@
"@types/node": {
"version": "18.8.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.8.3.tgz",
"integrity": "sha512-0os9vz6BpGwxGe9LOhgP/ncvYN5Tx1fNcd2TM3rD/aCGBkysb+ZWpXEocG24h6ZzOi13+VB8HndAQFezsSOw1w==",
"dev": true
"integrity": "sha512-0os9vz6BpGwxGe9LOhgP/ncvYN5Tx1fNcd2TM3rD/aCGBkysb+ZWpXEocG24h6ZzOi13+VB8HndAQFezsSOw1w=="
},
"@types/pg": {
"version": "8.6.6",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.6.tgz",
"integrity": "sha512-O2xNmXebtwVekJDD+02udOncjVcMZQuTEQEMpKJ0ZRf5E7/9JJX3izhKUcUifBkyKpljyUM6BTgy2trmviKlpw==",
"requires": {
"@types/node": "*",
"pg-protocol": "*",
"pg-types": "^2.2.0"
}
},
"@types/sinonjs__fake-timers": {
"version": "8.1.1",
@@ -3018,6 +3262,16 @@
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"dev": true
},
"buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
},
"buffer-writer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
"integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw=="
},
"cachedir": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz",
@@ -3191,9 +3445,9 @@
}
},
"cypress": {
"version": "10.9.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.9.0.tgz",
"integrity": "sha512-MjIWrRpc+bQM9U4kSSdATZWZ2hUqHGFEQTF7dfeZRa4MnalMtc88FIE49USWP2ZVtfy5WPBcgfBX+YorFqGElA==",
"version": "12.2.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-12.2.0.tgz",
"integrity": "sha512-kvl95ri95KK8mAy++tEU/wUgzAOMiIciZSL97LQvnOinb532m7dGvwN0mDSIGbOd71RREtmT9o4h088RjK5pKw==",
"dev": true,
"requires": {
"@cypress/request": "^2.88.10",
@@ -3308,6 +3562,14 @@
"safer-buffer": "^2.1.0"
}
},
"ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"requires": {
"safe-buffer": "^5.0.1"
}
},
"emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@@ -3791,6 +4053,30 @@
"universalify": "^2.0.0"
}
},
"jsonwebtoken": {
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz",
"integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==",
"requires": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^5.6.0"
},
"dependencies": {
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
}
}
},
"jsprim": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz",
@@ -3803,6 +4089,25 @@
"verror": "1.10.0"
}
},
"jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
"requires": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"requires": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"lazy-ass": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz",
@@ -3839,6 +4144,16 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
},
"lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
},
"lodash.isempty": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz",
@@ -3849,11 +4164,26 @@
"resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz",
"integrity": "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw=="
},
"lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
},
"lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
},
"lodash.isobject": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz",
"integrity": "sha512-3/Qptq2vr7WeJbB4KHUSKlq8Pl7ASXi3UG6CMbBm8WRtXi8+GHm7mKaU3urfpSEzWe2wCIChs6/sdocUsTKJiA=="
},
"lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
},
"lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
@@ -3862,8 +4192,7 @@
"lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"dev": true
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
},
"log-symbols": {
"version": "4.1.0",
@@ -4178,6 +4507,11 @@
"aggregate-error": "^3.0.0"
}
},
"packet-reader": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
"integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ=="
},
"path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -4207,6 +4541,61 @@
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"dev": true
},
"pg": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.8.0.tgz",
"integrity": "sha512-UXYN0ziKj+AeNNP7VDMwrehpACThH7LUl/p8TDFpEUuSejCUIwGSfxpHsPvtM6/WXFy6SU4E5RG4IJV/TZAGjw==",
"requires": {
"buffer-writer": "2.0.0",
"packet-reader": "1.0.0",
"pg-connection-string": "^2.5.0",
"pg-pool": "^3.5.2",
"pg-protocol": "^1.5.0",
"pg-types": "^2.1.0",
"pgpass": "1.x"
}
},
"pg-connection-string": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz",
"integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ=="
},
"pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="
},
"pg-pool": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.2.tgz",
"integrity": "sha512-His3Fh17Z4eg7oANLob6ZvH8xIVen3phEZh2QuyrIl4dQSDVEabNducv6ysROKpDNPSD+12tONZVWfSgMvDD9w==",
"requires": {}
},
"pg-protocol": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz",
"integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ=="
},
"pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"requires": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
}
},
"pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"requires": {
"split2": "^4.1.0"
}
},
"picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
@@ -4219,6 +4608,29 @@
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
"dev": true
},
"postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="
},
"postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="
},
"postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="
},
"postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"requires": {
"xtend": "^4.0.0"
}
},
"prettier": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
@@ -4405,6 +4817,11 @@
"is-fullwidth-code-point": "^3.0.0"
}
},
"split2": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz",
"integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ=="
},
"sshpk": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz",
@@ -4618,6 +5035,11 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
},
"y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View File

@@ -11,8 +11,11 @@
},
"private": true,
"dependencies": {
"@types/pg": "^8.6.6",
"cypress-wait-until": "^1.7.2",
"jsonwebtoken": "^8.5.1",
"mochawesome": "^7.1.3",
"pg": "^8.8.0",
"prettier": "^2.7.1",
"typescript": "^4.8.4",
"uuid": "^9.0.0",
@@ -20,6 +23,6 @@
},
"devDependencies": {
"@types/node": "^18.8.3",
"cypress": "^10.9.0"
"cypress": "^12.2.0"
}
}