mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 01:47:33 +00:00
feat: add quotas (#4779)
adds possibilities to cap authenticated requests and execution seconds of actions on a defined intervall
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@@ -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',
|
||||
|
31
e2e/cypress/support/api/instances.ts
Normal file
31
e2e/cypress/support/api/instances.ts
Normal 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,
|
||||
});
|
||||
}
|
@@ -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`,
|
||||
|
@@ -7,7 +7,7 @@ export function ensureOIDCSettingsSet(
|
||||
idTokenLifetime: number,
|
||||
refreshTokenExpiration: number,
|
||||
refreshTokenIdleExpiration: number,
|
||||
): Cypress.Chainable<number> {
|
||||
) {
|
||||
return ensureSetting(
|
||||
api,
|
||||
`${api.adminBaseURL}/settings/oidc`,
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -22,7 +22,7 @@ export function ensureDomainPolicy(
|
||||
userLoginMustBeDomain: boolean,
|
||||
validateOrgDomains: boolean,
|
||||
smtpSenderAddressMatchesInstanceDomain: boolean,
|
||||
): Cypress.Chainable<number> {
|
||||
) {
|
||||
return ensureSetting(
|
||||
api,
|
||||
`${api.adminBaseURL}/policies/domain`,
|
||||
|
@@ -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`,
|
||||
|
84
e2e/cypress/support/api/quota.ts
Normal file
84
e2e/cypress/support/api/quota.ts
Normal 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;
|
||||
});
|
||||
}
|
@@ -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) => {
|
||||
|
@@ -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
|
||||
|
@@ -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',
|
||||
},
|
||||
};
|
||||
|
@@ -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,
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -15,6 +15,4 @@
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands';
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
import './types';
|
||||
|
@@ -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'
|
@@ -82,8 +82,10 @@ export function login(
|
||||
|
||||
onAuthenticated ? onAuthenticated() : null;
|
||||
|
||||
cy.visit('/');
|
||||
|
||||
cy.get('[data-e2e=authenticated-welcome]', {
|
||||
timeout: 10_000,
|
||||
timeout: 50_000,
|
||||
});
|
||||
},
|
||||
{
|
||||
|
10
e2e/cypress/support/types.ts
Normal file
10
e2e/cypress/support/types.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
let webhookEventSchema = {
|
||||
unit: 0,
|
||||
id: '',
|
||||
callURL: '',
|
||||
periodStart: new Date(),
|
||||
threshold: 0,
|
||||
usage: 0,
|
||||
};
|
||||
|
||||
export type ZITADELWebhookEvent = typeof webhookEventSchema;
|
Reference in New Issue
Block a user