test(e2e): test authorizations (#4342)

* add specs that cover the b2b demo

* update cypress

* test handling manager roles

* use shared mocha contexts

* use beforeEach instead of before

* improve readability

* improve application test

* remove static waits

* remove old awaitDesired

* test owned project authorizations

* simplify ensure.ts

* test granted projects authz

* disable prevSubject for shouldNotExist

* await non-existence, then expect no error

* update dependencies

* fix tests from scratch

* fix settings tests from scratch

* Apply suggestions from code review

Co-authored-by: Max Peintner <max@caos.ch>

* Implement code review suggestions

* use spread operator

* settings properties must match

* add check settings object

* revert spread operator

Co-authored-by: Max Peintner <max@caos.ch>
This commit is contained in:
Elio Bischof
2022-10-11 15:29:23 +02:00
committed by GitHub
parent 6daf44a34a
commit 51febd7e4e
35 changed files with 884 additions and 546 deletions

View File

@@ -1,17 +1,23 @@
import { login, User } from 'support/login/users';
import { API } from './types';
export interface apiCallProperties {
authHeader: string;
mgntBaseURL: string;
adminBaseURL: string;
}
const authHeaderKey = 'Authorization',
orgIdHeaderKey = 'x-zitadel-orgid';
export function apiAuth(): Cypress.Chainable<apiCallProperties> {
export function apiAuth(): Cypress.Chainable<API> {
return login(User.IAMAdminUser, 'Password1!', false, true).then((token) => {
return <apiCallProperties>{
authHeader: `Bearer ${token}`,
mgntBaseURL: `${Cypress.env('BACKEND_URL')}/management/v1/`,
adminBaseURL: `${Cypress.env('BACKEND_URL')}/admin/v1/`,
return <API>{
token: token,
mgmtBaseURL: `${Cypress.env('BACKEND_URL')}/management/v1`,
adminBaseURL: `${Cypress.env('BACKEND_URL')}/admin/v1`,
};
});
}
export function requestHeaders(api: API, orgId?: number): object {
const headers = { [authHeaderKey]: `Bearer ${api.token}` };
if (orgId) {
headers[orgIdHeaderKey] = orgId;
}
return headers;
}

View File

@@ -1,196 +1,123 @@
import { apiCallProperties } from './apiauth';
import { requestHeaders } from './apiauth';
import { findFromList as mapFromList, searchSomething } from './search';
import { API, Entity, SearchResult } from './types';
export function ensureSomethingExists(
api: apiCallProperties,
export function ensureItemExists(
api: API,
searchPath: string,
find: (entity: any) => boolean,
findInList: (entity: Entity) => boolean,
createPath: string,
body: any,
body: Entity,
orgId?: number,
newItemIdField: string = 'id',
searchItemIdField?: string,
): Cypress.Chainable<number> {
return searchSomething(api, searchPath, find)
.then((sRes) => {
if (sRes.entity) {
return cy.wrap({
id: sRes.entity.id,
initialSequence: 0,
});
}
return cy
.request({
method: 'POST',
url: `${api.mgntBaseURL}${createPath}`,
headers: {
Authorization: api.authHeader,
},
body: body,
failOnStatusCode: false,
followRedirect: false,
})
.then((cRes) => {
expect(cRes.status).to.equal(200);
return {
id: cRes.body.id,
initialSequence: sRes.sequence,
};
});
})
.then((data) => {
awaitDesired(90, (entity) => !!entity, data.initialSequence, api, searchPath, find);
return cy.wrap<number>(data.id);
});
return ensureSomething(
api,
() => searchSomething(api, searchPath, 'POST', mapFromList(findInList, searchItemIdField), orgId),
() => createPath,
'POST',
body,
(entity) => !!entity,
(body) => body[newItemIdField],
orgId,
);
}
export function ensureSomethingIsSet(
api: apiCallProperties,
path: string,
find: (entity: any) => SearchResult,
createPath: string,
body: any,
): Cypress.Chainable<number> {
return getSomething(api, path, find)
.then((sRes) => {
if (sRes.entity) {
return cy.wrap({
id: sRes.entity.id,
initialSequence: 0,
});
}
return cy
.request({
method: 'PUT',
url: createPath,
headers: {
Authorization: api.authHeader,
},
body: body,
failOnStatusCode: false,
followRedirect: false,
})
.then((cRes) => {
expect(cRes.status).to.equal(200);
return {
id: cRes.body.id,
initialSequence: sRes.sequence,
};
});
})
.then((data) => {
awaitDesiredById(90, (entity) => !!entity, data.initialSequence, api, path, find);
return cy.wrap<number>(data.id);
});
}
export function ensureSomethingDoesntExist(
api: apiCallProperties,
export function ensureItemDoesntExist(
api: API,
searchPath: string,
find: (entity: any) => boolean,
deletePath: (entity: any) => string,
findInList: (entity: Entity) => boolean,
deletePath: (entity: Entity) => string,
orgId?: number,
): Cypress.Chainable<null> {
return searchSomething(api, searchPath, find)
.then((sRes) => {
if (!sRes.entity) {
return cy.wrap(0);
}
return cy
.request({
method: 'DELETE',
url: `${api.mgntBaseURL}${deletePath(sRes.entity)}`,
headers: {
Authorization: api.authHeader,
},
failOnStatusCode: false,
})
.then((dRes) => {
expect(dRes.status).to.equal(200);
return sRes.sequence;
});
})
.then((initialSequence) => {
awaitDesired(90, (entity) => !entity, initialSequence, api, searchPath, find);
return null;
});
return ensureSomething(
api,
() => searchSomething(api, searchPath, 'POST', mapFromList(findInList), orgId),
deletePath,
'DELETE',
null,
(entity) => !entity,
).then(() => null);
}
type SearchResult = {
entity: any;
sequence: number;
};
function searchSomething(
api: apiCallProperties,
searchPath: string,
find: (entity: any) => boolean,
): Cypress.Chainable<SearchResult> {
return cy
.request({
method: 'POST',
url: `${api.mgntBaseURL}${searchPath}`,
headers: {
Authorization: api.authHeader,
},
})
.then((res) => {
return {
entity: res.body.result?.find(find) || null,
sequence: res.body.details.processedSequence,
};
});
}
function getSomething(
api: apiCallProperties,
searchPath: string,
find: (entity: any) => SearchResult,
): Cypress.Chainable<SearchResult> {
return cy
.request({
method: 'GET',
url: searchPath,
headers: {
Authorization: api.authHeader,
},
})
.then((res) => {
return find(res.body);
});
export function ensureSetting(
api: API,
path: string,
mapResult: (entity: any) => SearchResult,
createPath: string,
body: any,
orgId?: number,
): Cypress.Chainable<number> {
return ensureSomething(
api,
() => searchSomething(api, path, 'GET', mapResult, orgId),
() => createPath,
'PUT',
body,
(entity) => !!entity,
(body) => body?.settings?.id,
);
}
function awaitDesired(
trials: number,
expectEntity: (entity: any) => boolean,
initialSequence: number,
api: apiCallProperties,
searchPath: string,
find: (entity: any) => boolean,
expectEntity: (entity: Entity) => boolean,
search: () => Cypress.Chainable<SearchResult>,
initialSequence?: number,
) {
searchSomething(api, searchPath, find).then((resp) => {
search().then((resp) => {
const foundExpectedEntity = expectEntity(resp.entity);
const foundExpectedSequence = resp.sequence > initialSequence;
const foundExpectedSequence = !initialSequence || resp.sequence >= initialSequence;
if (!foundExpectedEntity || !foundExpectedSequence) {
expect(trials, `trying ${trials} more times`).to.be.greaterThan(0);
cy.wait(1000);
awaitDesired(trials - 1, expectEntity, initialSequence, api, searchPath, find);
awaitDesired(trials - 1, expectEntity, search, initialSequence);
}
});
}
function awaitDesiredById(
trials: number,
expectEntity: (entity: any) => boolean,
initialSequence: number,
api: apiCallProperties,
path: string,
find: (entity: any) => SearchResult,
) {
getSomething(api, path, find).then((resp) => {
const foundExpectedEntity = expectEntity(resp.entity);
const foundExpectedSequence = resp.sequence > initialSequence;
if (!foundExpectedEntity || !foundExpectedSequence) {
expect(trials, `trying ${trials} more times`).to.be.greaterThan(0);
cy.wait(1000);
awaitDesiredById(trials - 1, expectEntity, initialSequence, api, path, find);
}
});
interface EnsuredResult {
id: number;
sequence: number;
}
export function ensureSomething(
api: API,
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> {
return search()
.then<EnsuredResult>((sRes) => {
if (expectEntity(sRes.entity)) {
return cy.wrap({ id: sRes.id, sequence: sRes.sequence });
}
return cy
.request({
method: ensureMethod,
url: apiPath(sRes.entity),
headers: requestHeaders(api, orgId),
body: body,
failOnStatusCode: false,
followRedirect: false,
})
.then((cRes) => {
expect(cRes.status).to.equal(200);
return {
id: mapId ? mapId(cRes.body) : undefined,
sequence: sRes.sequence,
};
});
})
.then((data) => {
awaitDesired(90, expectEntity, search, data.sequence);
return cy.wrap<number>(data.id);
});
}

View File

@@ -0,0 +1,22 @@
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) => {
return ensureItemExists(
api,
`${api.mgmtBaseURL}/projectgrants/_search`,
(grant: any) => grant.grantedOrgId == orgUnderTest && grant.projectId == foreignProjectId,
`${api.mgmtBaseURL}/projects/${foreignProjectId}/grants`,
{ granted_org_id: orgUnderTest },
foreignOrgId,
'grantId',
'grantId',
);
});
}

View File

@@ -0,0 +1,76 @@
import { ensureItemDoesntExist, ensureItemExists } from './ensure';
import { findFromList, searchSomething } from './search';
import { API } from './types';
export function ensureHumanIsNotOrgMember(api: API, username: string): Cypress.Chainable<number> {
return ensureItemDoesntExist(
api,
`${api.mgmtBaseURL}/orgs/me/members/_search`,
(member: any) => (<string>member.preferredLoginName).startsWith(username),
(member) => `${api.mgmtBaseURL}/orgs/me/members/${member.userId}`,
);
}
export function ensureHumanIsOrgMember(api: API, username: string, roles: string[]): Cypress.Chainable<number> {
return searchSomething(
api,
`${api.mgmtBaseURL}/users/_search`,
'POST',
findFromList((user) => {
return user.userName == username;
}),
).then((user) => {
return ensureItemExists(
api,
`${api.mgmtBaseURL}/orgs/me/members/_search`,
(member: any) => member.userId == user.entity.id,
`${api.mgmtBaseURL}/orgs/me/members`,
{
userId: user.entity.id,
roles: roles,
},
);
});
}
export function ensureHumanIsNotProjectMember(
api: API,
projectId: string,
username: string,
grantId?: number,
): Cypress.Chainable<number> {
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}`,
);
}
export function ensureHumanIsProjectMember(
api: API,
projectId: string,
username: string,
roles: string[],
grantId?: number,
): Cypress.Chainable<number> {
return searchSomething(
api,
`${api.mgmtBaseURL}/users/_search`,
'POST',
findFromList((user) => {
return user.userName == username;
}),
).then((user) => {
return ensureItemExists(
api,
`${api.mgmtBaseURL}/projects/${projectId}/${grantId ? `grants/${grantId}/` : ''}members/_search`,
(member: any) => member.userId == user.entity.id,
`${api.mgmtBaseURL}/projects/${projectId}/${grantId ? `grants/${grantId}/` : ''}members`,
{
userId: user.entity.id,
roles: roles,
},
);
});
}

View File

@@ -1,32 +1,35 @@
import { apiCallProperties } from './apiauth';
import { ensureSomethingIsSet } from './ensure';
import { ensureSetting } from './ensure';
import { API } from './types';
export function ensureOIDCSettingsSet(
api: apiCallProperties,
accessTokenLifetime,
idTokenLifetime,
refreshTokenExpiration,
api: API,
accessTokenLifetime: number,
idTokenLifetime: number,
refreshTokenExpiration: number,
refreshTokenIdleExpiration: number,
): Cypress.Chainable<number> {
return ensureSomethingIsSet(
return ensureSetting(
api,
`${api.adminBaseURL}settings/oidc`,
(settings: any) => {
let entity = null;
if (
settings.settings?.accessTokenLifetime === hoursToDuration(accessTokenLifetime) &&
settings.settings?.idTokenLifetime === hoursToDuration(idTokenLifetime) &&
settings.settings?.refreshTokenExpiration === daysToDuration(refreshTokenExpiration) &&
settings.settings?.refreshTokenIdleExpiration === daysToDuration(refreshTokenIdleExpiration)
) {
entity = settings.settings;
}
return {
entity: entity,
sequence: settings.settings?.details?.sequence,
`${api.adminBaseURL}/settings/oidc`,
(body: any) => {
const result = {
sequence: body.settings?.details?.sequence,
id: body.settings.id,
entity: null,
};
if (
body.settings &&
body.settings.accessTokenLifetime === hoursToDuration(accessTokenLifetime) &&
body.settings.idTokenLifetime === hoursToDuration(idTokenLifetime) &&
body.settings.refreshTokenExpiration === daysToDuration(refreshTokenExpiration) &&
body.settings.refreshTokenIdleExpiration === daysToDuration(refreshTokenIdleExpiration)
) {
return { ...result, entity: body.settings };
}
return result;
},
`${api.adminBaseURL}settings/oidc`,
`${api.adminBaseURL}/settings/oidc`,
{
accessTokenLifetime: hoursToDuration(accessTokenLifetime),
idTokenLifetime: hoursToDuration(idTokenLifetime),

View File

@@ -0,0 +1,30 @@
import { ensureSomething } from './ensure';
import { searchSomething } from './search';
import { API } from './types';
import { host } from '../login/users';
export function ensureOrgExists(api: API, name: string): Cypress.Chainable<number> {
return ensureSomething(
api,
() =>
searchSomething(
api,
encodeURI(`${api.mgmtBaseURL}/global/orgs/_by_domain?domain=${name}.${host(Cypress.config('baseUrl'))}`),
'GET',
(res) => {
return { entity: res.org, id: res.org?.id, sequence: res.org?.details?.sequence };
},
),
() => `${api.mgmtBaseURL}/orgs`,
'POST',
{ name: name },
(org: any) => org?.name === name,
(res) => res.id,
);
}
export function getOrgUnderTest(api: API): Cypress.Chainable<number> {
return searchSomething(api, `${api.mgmtBaseURL}/orgs/me`, 'GET', (res) => {
return { entity: res.org, id: res.org.id, sequence: res.org.details.sequence };
}).then((res) => res.entity.id);
}

View File

@@ -1,16 +1,15 @@
import { apiCallProperties } from './apiauth';
import { requestHeaders } from './apiauth';
import { API } from './types';
export enum Policy {
Label = 'label',
}
export function resetPolicy(api: apiCallProperties, policy: Policy) {
export function resetPolicy(api: API, policy: Policy) {
cy.request({
method: 'DELETE',
url: `${api.mgntBaseURL}/policies/${policy}`,
headers: {
Authorization: api.authHeader,
},
url: `${api.mgmtBaseURL}/policies/${policy}`,
headers: requestHeaders(api),
}).then((res) => {
expect(res.status).to.equal(200);
return null;

View File

@@ -1,18 +1,24 @@
import { apiCallProperties } from './apiauth';
import { ensureSomethingDoesntExist, ensureSomethingExists } from './ensure';
import { ensureItemDoesntExist, ensureItemExists } from './ensure';
import { API } from './types';
export function ensureProjectExists(api: apiCallProperties, projectName: string): Cypress.Chainable<number> {
return ensureSomethingExists(api, `projects/_search`, (project: any) => project.name === projectName, 'projects', {
name: projectName,
});
export function ensureProjectExists(api: API, projectName: string, orgId?: number): Cypress.Chainable<number> {
return ensureItemExists(
api,
`${api.mgmtBaseURL}/projects/_search`,
(project: any) => project.name === projectName,
`${api.mgmtBaseURL}/projects`,
{ name: projectName },
orgId,
);
}
export function ensureProjectDoesntExist(api: apiCallProperties, projectName: string): Cypress.Chainable<null> {
return ensureSomethingDoesntExist(
export function ensureProjectDoesntExist(api: API, projectName: string, orgId?: number): Cypress.Chainable<null> {
return ensureItemDoesntExist(
api,
`projects/_search`,
`${api.mgmtBaseURL}/projects/_search`,
(project: any) => project.name === projectName,
(project) => `projects/${project.id}`,
(project) => `${api.mgmtBaseURL}/projects/${project.id}`,
orgId,
);
}
@@ -25,33 +31,28 @@ export const Roles = new ResourceType('roles', 'key', 'key');
//export const Grants = new ResourceType('apps', 'name')
export function ensureProjectResourceDoesntExist(
api: apiCallProperties,
api: API,
projectId: number,
resourceType: ResourceType,
resourceName: string,
orgId?: number,
): Cypress.Chainable<null> {
return ensureSomethingDoesntExist(
return ensureItemDoesntExist(
api,
`projects/${projectId}/${resourceType.resourcePath}/_search`,
(resource: any) => {
return resource[resourceType.compareProperty] === resourceName;
},
(resource) => {
return `projects/${projectId}/${resourceType.resourcePath}/${resource[resourceType.identifierProperty]}`;
},
`${api.mgmtBaseURL}/projects/${projectId}/${resourceType.resourcePath}/_search`,
(resource: any) => resource[resourceType.compareProperty] === resourceName,
(resource) =>
`${api.mgmtBaseURL}/projects/${projectId}/${resourceType.resourcePath}/${resource[resourceType.identifierProperty]}`,
orgId,
);
}
export function ensureApplicationExists(
api: apiCallProperties,
projectId: number,
appName: string,
): Cypress.Chainable<number> {
return ensureSomethingExists(
export function ensureApplicationExists(api: API, projectId: number, appName: string): Cypress.Chainable<number> {
return ensureItemExists(
api,
`projects/${projectId}/${Apps.resourcePath}/_search`,
`${api.mgmtBaseURL}/projects/${projectId}/${Apps.resourcePath}/_search`,
(resource: any) => resource.name === appName,
`projects/${projectId}/${Apps.resourcePath}/oidc`,
`${api.mgmtBaseURL}/projects/${projectId}/${Apps.resourcePath}/oidc`,
{
name: appName,
redirectUris: ['https://e2eredirecturl.org'],
@@ -59,11 +60,6 @@ export function ensureApplicationExists(
grantTypes: ['OIDC_GRANT_TYPE_AUTHORIZATION_CODE'],
authMethodType: 'OIDC_AUTH_METHOD_TYPE_NONE',
postLogoutRedirectUris: ['https://e2elogoutredirecturl.org'],
/* "clientId": "129383004379407963@e2eprojectpermission",
"clockSkew": "0s",
"allowedOrigins": [
"https://testurl.org"
]*/
},
);
}

View File

@@ -0,0 +1,32 @@
import { requestHeaders } from './apiauth';
import { API, Entity, SearchResult } from './types';
export function searchSomething(
api: API,
searchPath: string,
method: string,
mapResult: (body: any) => SearchResult,
orgId?: number,
): Cypress.Chainable<SearchResult> {
return cy
.request({
method: method,
url: searchPath,
headers: requestHeaders(api, orgId),
failOnStatusCode: method == 'POST',
})
.then((res) => {
return mapResult(res.body);
});
}
export function findFromList(find: (entity: Entity) => boolean, idField: string = 'id'): (body: any) => SearchResult {
return (b) => {
const entity = b.result?.find(find);
return {
entity: entity,
sequence: parseInt(<string>b.details.processedSequence),
id: entity?.[idField],
};
};
}

View File

@@ -0,0 +1,14 @@
export interface API {
token: string;
mgmtBaseURL: string;
adminBaseURL: string;
}
export type SearchResult = {
entity: Entity | null;
sequence: number;
id: number;
};
// Entity is an object but not a function
export type Entity = { [k: string]: any } & ({ bind?: never } | { call?: never });

View File

@@ -1,35 +1,51 @@
import { apiCallProperties } from './apiauth';
import { ensureSomethingDoesntExist, ensureSomethingExists } from './ensure';
import { ensureItemDoesntExist, ensureItemExists } from './ensure';
import { API } from './types';
export function ensureHumanUserExists(api: apiCallProperties, username: string): Cypress.Chainable<number> {
return ensureSomethingExists(api, 'users/_search', (user: any) => user.userName === username, 'users/human', {
user_name: username,
profile: {
first_name: 'e2efirstName',
last_name: 'e2elastName',
},
email: {
email: 'e2e@email.ch',
},
phone: {
phone: '+41 123456789',
},
});
}
export function ensureMachineUserExists(api: apiCallProperties, username: string): Cypress.Chainable<number> {
return ensureSomethingExists(api, 'users/_search', (user: any) => user.userName === username, 'users/machine', {
user_name: username,
name: 'e2emachinename',
description: 'e2emachinedescription',
});
}
export function ensureUserDoesntExist(api: apiCallProperties, username: string): Cypress.Chainable<null> {
return ensureSomethingDoesntExist(
export function ensureHumanUserExists(api: API, username: string): Cypress.Chainable<number> {
return ensureItemExists(
api,
'users/_search',
`${api.mgmtBaseURL}/users/_search`,
(user: any) => user.userName === username,
(user) => `users/${user.id}`,
`${api.mgmtBaseURL}/users/human`,
{
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> {
return ensureItemExists(
api,
`${api.mgmtBaseURL}/users/_search`,
(user: any) => user.userName === username,
`${api.mgmtBaseURL}/users/machine`,
{
user_name: username,
name: 'e2emachinename',
description: 'e2emachinedescription',
},
undefined,
'userId',
);
}
export function ensureUserDoesntExist(api: API, username: string): Cypress.Chainable<null> {
return ensureItemDoesntExist(
api,
`${api.mgmtBaseURL}/users/_search`,
(user: any) => user.userName === username,
(user) => `${api.mgmtBaseURL}/users/${user.id}`,
);
}

View File

@@ -1,26 +1,80 @@
/*
namespace Cypress {
interface Chainable {
*/
/**
* Custom command that authenticates a user.
*
* @example cy.consolelogin('hodor', 'hodor1234')
*/
/* consolelogin(username: string, password: string): void
}
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', '/');
// })
//})
//
interface ShouldNotExistOptions {
selector?: string;
timeout?: number;
}
Cypress.Commands.add('consolelogin', { prevSubject: false }, (username: string, password: string) => {
declare global {
namespace Cypress {
interface Chainable {
/**
* Custom command that asserts on clipboard text.
*
* @example cy.clipboardMatches('hodor', 'hodor1234')
*/
clipboardMatches(pattern: RegExp | string): Cypress.Chainable<null>;
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', '/');
})
})
*/
/**
* Custom command that waits until the selector finds zero elements.
*/
shouldNotExist(options?: ShouldNotExistOptions): Cypress.Chainable<null>;
}
}
}
Cypress.Commands.add('clipboardMatches', { prevSubject: false }, (pattern: RegExp | string) => {
/* doesn't work reliably
return cy.window()
.then(win => {
win.focus()
return cy.waitUntil(() => win.navigator.clipboard.readText()
.then(clipboadText => {
win.focus()
const matches = typeof pattern === "string"
? clipboadText.includes(pattern)
: pattern.test(clipboadText)
if (!matches) {
cy.log(`text in clipboard ${clipboadText} doesn't match the pattern ${pattern}, yet`)
}
return matches
})
)
})
.then(() => null)
*/
});
Cypress.Commands.add('shouldNotExist', { prevSubject: false }, (options?: ShouldNotExistOptions) => {
return cy.waitUntil(
() => {
return Cypress.$(options?.selector).length === 0;
},
{ timeout: typeof options?.timeout === 'number' ? options.timeout : 500 },
);
});