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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 884 additions and 546 deletions

View File

@ -142,7 +142,7 @@ goreleaser build --id dev --snapshot --single-target --rm-dist --output .artifac
> For speeding up rebuilds, you can reexecute only specific steps you think are necessary based on your changes. > For speeding up rebuilds, you can reexecute only specific steps you think are necessary based on your changes.
> Generating gRPC stubs: `DOCKER_BUILDKIT=1 docker build -f build/zitadel/Dockerfile . --target go-copy -o .` > Generating gRPC stubs: `DOCKER_BUILDKIT=1 docker build -f build/zitadel/Dockerfile . --target go-copy -o .`
> Running unit tests: `DOCKER_BUILDKIT=1 docker build -f build/zitadel/Dockerfile . --target go-codecov` > Running unit tests: `DOCKER_BUILDKIT=1 docker build -f build/zitadel/Dockerfile . --target go-codecov`
> Generating the console: `DOCKER_BUILDKIT=1 docker build -f build/console/Dockerfile . -t zitadel-npm-console --target angular-export -o internal/api/ui/console/static/` > Generating the console: `DOCKER_BUILDKIT=1 docker build -f build/console/Dockerfile . --target angular-export -o internal/api/ui/console/static/`
> Build the binary: `goreleaser build --id dev --snapshot --single-target --rm-dist --output .artifacts/zitadel/zitadel --skip-before` > Build the binary: `goreleaser build --id dev --snapshot --single-target --rm-dist --output .artifacts/zitadel/zitadel --skip-before`
You can now run and debug the binary in .artifacts/zitadel/zitadel using your favourite IDE, for example GoLand. You can now run and debug the binary in .artifacts/zitadel/zitadel using your favourite IDE, for example GoLand.

View File

@ -7,7 +7,7 @@ export class CopyToClipboardDirective {
@Input() valueToCopy: string = ''; @Input() valueToCopy: string = '';
@Output() copiedValue: EventEmitter<string> = new EventEmitter(); @Output() copiedValue: EventEmitter<string> = new EventEmitter();
@HostListener('click', ['$event']) onMouseEnter($event: any): void { @HostListener('click', ['$event']) onClick($event: any): void {
$event.preventDefault(); $event.preventDefault();
$event.stopPropagation(); $event.stopPropagation();
this.copytoclipboard(this.valueToCopy); this.copytoclipboard(this.valueToCopy);

View File

@ -44,7 +44,7 @@
> >
<div class="role-cb-content"> <div class="role-cb-content">
<div class="cnsl-chip-dot" [style.background]="getColor(role)"></div> <div class="cnsl-chip-dot" [style.background]="getColor(role)"></div>
<span>{{ role | roletransform }}</span> <span data-e2e="role-checkbox">{{ role | roletransform }}</span>
<i class="info-hover las la-question-circle" matTooltip="{{ 'MEMBERROLES.' + role | translate }}"></i> <i class="info-hover las la-question-circle" matTooltip="{{ 'MEMBERROLES.' + role | translate }}"></i>
</div> </div>
</mat-checkbox> </mat-checkbox>
@ -61,6 +61,7 @@
mat-raised-button mat-raised-button
class="ok-button" class="ok-button"
(click)="closeDialogWithSuccess()" (click)="closeDialogWithSuccess()"
data-e2e="confirm-add-member-button"
> >
{{ 'ACTIONS.ADD' | translate }} {{ 'ACTIONS.ADD' | translate }}
</button> </button>

View File

@ -10,6 +10,7 @@
class="contributor-avatar-circle" class="contributor-avatar-circle"
matTooltip="{{ member.displayName }} | {{ member.rolesList | roletransform }}" matTooltip="{{ member.displayName }} | {{ member.rolesList | roletransform }}"
[ngStyle]="{ 'z-index': 20 - i }" [ngStyle]="{ 'z-index': 20 - i }"
data-e2e="member-avatar"
> >
<cnsl-avatar <cnsl-avatar
*ngIf="member && member.displayName && member.firstName && member.lastName; else cog" *ngIf="member && member.displayName && member.firstName && member.lastName; else cog"
@ -40,6 +41,7 @@
[disabled]="disabled" [disabled]="disabled"
mat-icon-button mat-icon-button
aria-label="Add member" aria-label="Add member"
data-e2e="add-member-button"
> >
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
</button> </button>

View File

@ -41,6 +41,7 @@
cnslCopyToClipboard cnslCopyToClipboard
[valueToCopy]="login" [valueToCopy]="login"
(copiedValue)="copied = $event" (copiedValue)="copied = $event"
data-e2e="copy-loginname"
> >
{{ login }} {{ login }}
</button> </button>

View File

@ -93,6 +93,7 @@
(click)="$event.stopPropagation(); triggerDeleteMember(member)" (click)="$event.stopPropagation(); triggerDeleteMember(member)"
mat-icon-button mat-icon-button
[disabled]="canDelete === false" [disabled]="canDelete === false"
data-e2e="remove-member-button"
> >
<i class="las la-trash"></i> <i class="las la-trash"></i>
</button> </button>
@ -114,10 +115,11 @@
[removable]="canWrite" [removable]="canWrite"
[selectable]="false" [selectable]="false"
(removed)="removeRole(member, role)" (removed)="removeRole(member, role)"
data-e2e="role"
> >
<div class="cnsl-chip-dot" [style.background]="getColor(role)"></div> <div class="cnsl-chip-dot" [style.background]="getColor(role)"></div>
<span>{{ role | roletransform }}</span> <span>{{ role | roletransform }}</span>
<button *ngIf="canWrite" matChipRemove> <button *ngIf="canWrite" matChipRemove data-e2e="remove-role-button">
<mat-icon>cancel</mat-icon> <mat-icon>cancel</mat-icon>
</button> </button>
</mat-chip> </mat-chip>

View File

@ -65,6 +65,7 @@
[formControl]="myControl" [formControl]="myControl"
placeholder="johndoe@domain.com" placeholder="johndoe@domain.com"
[matAutocomplete]="auto" [matAutocomplete]="auto"
data-e2e="add-member-input"
/> />
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)" [displayWith]="displayFn"> <mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)" [displayWith]="displayFn">
@ -72,7 +73,7 @@
<mat-spinner diameter="30"></mat-spinner> <mat-spinner diameter="30"></mat-spinner>
</mat-option> </mat-option>
<mat-option *ngFor="let user of filteredUsers" [value]="user"> <mat-option *ngFor="let user of filteredUsers" [value]="user">
<div class="user-option"> <div class="user-option" data-e2e="user-option">
<div class="circle"> <div class="circle">
<cnsl-avatar <cnsl-avatar
*ngIf=" *ngIf="

View File

@ -35,6 +35,7 @@
" "
class="sidenav-setting-list-element hide-on-mobile" class="sidenav-setting-list-element hide-on-mobile"
[ngClass]="{ active: currentSetting === setting.id, show: currentSetting === undefined }" [ngClass]="{ active: currentSetting === setting.id, show: currentSetting === undefined }"
[attr.data-e2e]="'sidenav-element-' + setting.id"
> >
<span>{{ setting.i18nKey | translate }}</span> <span>{{ setting.i18nKey | translate }}</span>
<mat-icon *ngIf="setting.showWarn" class="warn-icon" svgIcon="mdi_shield_alert"></mat-icon> <mat-icon *ngIf="setting.showWarn" class="warn-icon" svgIcon="mdi_shield_alert"></mat-icon>

View File

@ -58,8 +58,10 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
breadcrumbService.setBreadcrumb([bread]); breadcrumbService.setBreadcrumb([bread]);
auth.activeOrgChanged.pipe(takeUntil(this.destroy$)).subscribe((org) => { auth.activeOrgChanged.pipe(takeUntil(this.destroy$)).subscribe((org) => {
if (this.org && org) {
this.getData(); this.getData();
this.loadMetadata(); this.loadMetadata();
}
}); });
} }

View File

@ -117,6 +117,7 @@
[urisList]="oidcAppRequest.toObject().redirectUrisList" [urisList]="oidcAppRequest.toObject().redirectUrisList"
[getValues]="requestRedirectValuesSubject$" [getValues]="requestRedirectValuesSubject$"
title="{{ 'APP.OIDC.REDIRECT' | translate }}" title="{{ 'APP.OIDC.REDIRECT' | translate }}"
data-e2e="redirect-uris"
> >
</cnsl-redirect-uris> </cnsl-redirect-uris>
@ -145,6 +146,7 @@
title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}" title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}"
[getValues]="requestRedirectValuesSubject$" [getValues]="requestRedirectValuesSubject$"
[isNative]="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_NATIVE" [isNative]="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_NATIVE"
data-e2e="postlogout-uris"
> >
</cnsl-redirect-uris> </cnsl-redirect-uris>

View File

@ -4,7 +4,7 @@
<p class="desc cnsl-secondary-text">{{ 'APP.OIDC.CLIENTSECRET_DESCRIPTION' | translate }}</p> <p class="desc cnsl-secondary-text">{{ 'APP.OIDC.CLIENTSECRET_DESCRIPTION' | translate }}</p>
<div mat-dialog-content> <div mat-dialog-content>
<div class="flex" *ngIf="data.clientId"> <div class="flex" *ngIf="data.clientId">
<span class="overflow-auto"><span class="desc">ClientId:</span> {{ data.clientId }}</span> <span class="overflow-auto" data-e2e="client-id"><span class="desc">ClientId:</span> {{ data.clientId }}</span>
<button <button
color="primary" color="primary"
[disabled]="copied === data.clientId" [disabled]="copied === data.clientId"
@ -13,6 +13,7 @@
[valueToCopy]="data.clientId" [valueToCopy]="data.clientId"
(copiedValue)="this.copied = $event" (copiedValue)="this.copied = $event"
mat-icon-button mat-icon-button
data-e2e="client-id-copy"
> >
<i *ngIf="copied !== data.clientId" class="las la-clipboard"></i> <i *ngIf="copied !== data.clientId" class="las la-clipboard"></i>
<i *ngIf="copied === data.clientId" class="las la-clipboard-check"></i> <i *ngIf="copied === data.clientId" class="las la-clipboard-check"></i>

View File

@ -1,4 +1,4 @@
<form class="redirect-uris-form" (ngSubmit)="add(redInput)" data-e2e="redirect-uris"> <form class="redirect-uris-form" (ngSubmit)="add(redInput)">
<cnsl-form-field class="formfield"> <cnsl-form-field class="formfield">
<cnsl-label>{{ title }}</cnsl-label> <cnsl-label>{{ title }}</cnsl-label>

View File

@ -5,37 +5,43 @@ describe('applications', () => {
const testProjectName = 'e2eprojectapplication'; const testProjectName = 'e2eprojectapplication';
const testAppName = 'e2eappundertest'; const testAppName = 'e2eappundertest';
beforeEach(`ensure it doesn't exist already`, () => { beforeEach(() => {
apiAuth().then((api) => { apiAuth()
ensureProjectExists(api, testProjectName).then((projectID) => { .as('api')
ensureProjectResourceDoesntExist(api, projectID, Apps, testAppName).then(() => { .then((api) => {
cy.visit(`/projects/${projectID}`); ensureProjectExists(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}`);
}); });
it('add app', () => { it('add app', () => {
cy.get('[data-e2e="app-card-add"]').should('be.visible').click(); cy.get('[data-e2e="app-card-add"]').should('be.visible').click();
// select webapp cy.get('[formcontrolname="name"]').focus().type(testAppName);
cy.get('[formcontrolname="name"]').type(testAppName);
cy.get('[for="WEB"]').click(); cy.get('[for="WEB"]').click();
cy.get('[data-e2e="continue-button-nameandtype"]').click(); cy.get('[data-e2e="continue-button-nameandtype"]').click();
//select authentication cy.get('[for="PKCE"]').should('be.visible').click();
cy.get('[for="PKCE"]').click();
cy.get('[data-e2e="continue-button-authmethod"]').click(); cy.get('[data-e2e="continue-button-authmethod"]').click();
//enter URL cy.get('[data-e2e="redirect-uris"] input').focus().type('http://localhost:3000/api/auth/callback/zitadel');
cy.get('cnsl-redirect-uris').eq(0).type('https://testurl.org'); cy.get('[data-e2e="postlogout-uris"] input').focus().type('http://localhost:3000');
cy.get('cnsl-redirect-uris').eq(1).type('https://testlogouturl.org');
cy.get('[data-e2e="continue-button-redirecturis"]').click(); cy.get('[data-e2e="continue-button-redirecturis"]').click();
cy.get('[data-e2e="create-button"]') cy.get('[data-e2e="create-button"]').click();
.click()
.then(() => {
cy.get('[id*=overlay]').should('exist'); cy.get('[id*=overlay]').should('exist');
});
cy.get('.data-e2e-success'); cy.get('.data-e2e-success');
cy.wait(200); const expectClientId = new RegExp(`^.*[0-9]+\\@${testProjectName}.*$`);
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist'); cy.get('[data-e2e="client-id-copy"]').click();
//TODO: check client ID/Secret cy.contains('[data-e2e="client-id"]', expectClientId);
cy.clipboardMatches(expectClientId);
cy.shouldNotExist({ selector: '.data-e2e-failure' });
});
});
describe('edit app', () => {
it('should configure an application to enable dev mode');
it('should configure an application to put user roles and info inside id token');
}); });
}); });

View File

@ -7,19 +7,20 @@ describe('humans', () => {
const testHumanUserNameAdd = 'e2ehumanusernameadd'; const testHumanUserNameAdd = 'e2ehumanusernameadd';
const testHumanUserNameRemove = 'e2ehumanusernameremove'; const testHumanUserNameRemove = 'e2ehumanusernameremove';
beforeEach(() => {
apiAuth().as('api');
});
describe('add', () => { describe('add', () => {
before(`ensure it doesn't exist already`, () => { beforeEach(`ensure it doesn't exist already`, function () {
apiAuth().then((apiCallProperties) => { ensureUserDoesntExist(this.api, loginname(testHumanUserNameAdd, Cypress.env('ORGANIZATION')));
ensureUserDoesntExist(apiCallProperties, testHumanUserNameAdd).then(() => {
cy.visit(humansPath); cy.visit(humansPath);
}); });
});
});
it('should add a user', () => { it('should add a user', () => {
cy.get('[data-e2e="create-user-button"]').click(); cy.get('[data-e2e="create-user-button"]').click();
cy.url().should('contain', 'users/create'); cy.url().should('contain', 'users/create');
cy.get('[formcontrolname="email"]').type(loginname('e2ehuman', Cypress.env('ORGANIZATION'))); cy.get('[formcontrolname="email"]').type('dummy@dummy.com');
//force needed due to the prefilled username prefix //force needed due to the prefilled username prefix
cy.get('[formcontrolname="userName"]').type(loginname(testHumanUserNameAdd, Cypress.env('ORGANIZATION'))); cy.get('[formcontrolname="userName"]').type(loginname(testHumanUserNameAdd, Cypress.env('ORGANIZATION')));
cy.get('[formcontrolname="firstName"]').type('e2ehumanfirstname'); cy.get('[formcontrolname="firstName"]').type('e2ehumanfirstname');
@ -27,33 +28,29 @@ describe('humans', () => {
cy.get('[formcontrolname="phone"]').type('+41 123456789'); cy.get('[formcontrolname="phone"]').type('+41 123456789');
cy.get('[data-e2e="create-button"]').click(); cy.get('[data-e2e="create-button"]').click();
cy.get('.data-e2e-success'); cy.get('.data-e2e-success');
cy.wait(200); const loginName = loginname(testHumanUserNameAdd, Cypress.env('ORGANIZATION'));
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist'); cy.contains('[data-e2e="copy-loginname"]', loginName).click();
cy.clipboardMatches(loginName);
cy.shouldNotExist({ selector: '.data-e2e-failure' });
}); });
}); });
describe('remove', () => { describe('remove', () => {
before('ensure it exists', () => { beforeEach('ensure it exists', function () {
apiAuth().then((api) => { ensureHumanUserExists(this.api, loginname(testHumanUserNameRemove, Cypress.env('ORGANIZATION')));
ensureHumanUserExists(api, loginname(testHumanUserNameRemove, Cypress.env('ORGANIZATION'))).then(() => {
cy.visit(humansPath); cy.visit(humansPath);
}); });
});
});
it('should delete a human user', () => { it('should delete a human user', () => {
cy.contains('tr', testHumanUserNameRemove) const rowSelector = `tr:contains(${testHumanUserNameRemove})`;
// doesn't work, need to force click. cy.get(rowSelector).find('[data-e2e="enabled-delete-button"]').click({ force: true });
// .trigger('mouseover')
.find('[data-e2e="enabled-delete-button"]')
.click({ force: true });
cy.get('[data-e2e="confirm-dialog-input"]') cy.get('[data-e2e="confirm-dialog-input"]')
.focus() .focus()
.type(loginname(testHumanUserNameRemove, Cypress.env('ORGANIZATION'))); .type(loginname(testHumanUserNameRemove, Cypress.env('ORGANIZATION')));
cy.get('[data-e2e="confirm-dialog-button"]').click(); cy.get('[data-e2e="confirm-dialog-button"]').click();
cy.get('.data-e2e-success'); cy.get('.data-e2e-success');
cy.wait(200); cy.shouldNotExist({ selector: rowSelector, timeout: 2000 });
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist'); cy.shouldNotExist({ selector: '.data-e2e-failure' });
}); });
}); });
}); });

View File

@ -3,18 +3,19 @@ import { ensureMachineUserExists, ensureUserDoesntExist } from '../../support/ap
import { loginname } from '../../support/login/users'; import { loginname } from '../../support/login/users';
describe('machines', () => { describe('machines', () => {
beforeEach(() => {
apiAuth().as('api');
});
const machinesPath = `/users?type=machine`; const machinesPath = `/users?type=machine`;
const testMachineUserNameAdd = 'e2emachineusernameadd'; const testMachineUserNameAdd = 'e2emachineusernameadd';
const testMachineUserNameRemove = 'e2emachineusernameremove'; const testMachineUserNameRemove = 'e2emachineusernameremove';
describe('add', () => { describe('add', () => {
before(`ensure it doesn't exist already`, () => { beforeEach(`ensure it doesn't exist already`, function () {
apiAuth().then((apiCallProperties) => { ensureUserDoesntExist(this.api, testMachineUserNameAdd);
ensureUserDoesntExist(apiCallProperties, testMachineUserNameAdd).then(() => {
cy.visit(machinesPath); cy.visit(machinesPath);
}); });
});
});
it('should add a machine', () => { it('should add a machine', () => {
cy.get('[data-e2e="create-user-button"]').click(); cy.get('[data-e2e="create-user-button"]').click();
@ -25,31 +26,28 @@ describe('machines', () => {
cy.get('[formcontrolname="description"]').type('e2emachinedescription'); cy.get('[formcontrolname="description"]').type('e2emachinedescription');
cy.get('[data-e2e="create-button"]').click(); cy.get('[data-e2e="create-button"]').click();
cy.get('.data-e2e-success'); cy.get('.data-e2e-success');
cy.wait(200); cy.contains('[data-e2e="copy-loginname"]', testMachineUserNameAdd).click();
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist'); cy.clipboardMatches(testMachineUserNameAdd);
cy.shouldNotExist({ selector: '.data-e2e-failure' });
}); });
}); });
describe('remove', () => { describe('edit', () => {
before('ensure it exists', () => { beforeEach('ensure it exists', function () {
apiAuth().then((api) => { ensureMachineUserExists(this.api, testMachineUserNameRemove);
ensureMachineUserExists(api, testMachineUserNameRemove).then(() => {
cy.visit(machinesPath); cy.visit(machinesPath);
}); });
});
});
it('should delete a machine', () => { it('should delete a machine', () => {
cy.contains('tr', testMachineUserNameRemove) const rowSelector = `tr:contains(${testMachineUserNameRemove})`;
// doesn't work, need to force click. cy.get(rowSelector).find('[data-e2e="enabled-delete-button"]').click({ force: true });
// .trigger('mouseover')
.find('[data-e2e="enabled-delete-button"]')
.click({ force: true });
cy.get('[data-e2e="confirm-dialog-input"]').focus().type(testMachineUserNameRemove); cy.get('[data-e2e="confirm-dialog-input"]').focus().type(testMachineUserNameRemove);
cy.get('[data-e2e="confirm-dialog-button"]').click(); cy.get('[data-e2e="confirm-dialog-button"]').click();
cy.get('.data-e2e-success'); cy.get('.data-e2e-success');
cy.wait(200); cy.shouldNotExist({ selector: rowSelector, timeout: 2000 });
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist'); cy.shouldNotExist({ selector: '.data-e2e-failure' });
}); });
it('should create a personal access token');
}); });
}); });

View File

@ -0,0 +1,6 @@
describe('organizations', () => {
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

@ -1,110 +1,249 @@
import { ensureProjectGrantExists } from 'support/api/grants';
import {
ensureHumanIsOrgMember,
ensureHumanIsNotOrgMember,
ensureHumanIsNotProjectMember,
ensureHumanIsProjectMember,
} from 'support/api/members';
import { ensureOrgExists } from 'support/api/orgs';
import { ensureHumanUserExists, ensureUserDoesntExist } from 'support/api/users';
import { loginname } from 'support/login/users';
import { apiAuth } from '../../support/api/apiauth'; import { apiAuth } from '../../support/api/apiauth';
import { ensureProjectExists, ensureProjectResourceDoesntExist, Roles } from '../../support/api/projects'; import { ensureProjectExists, ensureProjectResourceDoesntExist, Roles } from '../../support/api/projects';
describe('permissions', () => { describe('permissions', () => {
const testProjectName = 'e2eprojectpermission'; beforeEach(() => {
const testAppName = 'e2eapppermission'; apiAuth().as('api');
});
describe('management', () => {
const testManagerLoginname = loginname('e2ehumanmanager', Cypress.env('ORGANIZATION'));
function testAuthorizations(
roles: string[],
beforeCreate: Mocha.HookFunction,
beforeMutate: Mocha.HookFunction,
navigate: Mocha.HookFunction,
) {
beforeEach(function () {
ensureUserDoesntExist(this.api, testManagerLoginname);
ensureHumanUserExists(this.api, testManagerLoginname);
});
describe('create authorization', () => {
beforeEach(beforeCreate);
beforeEach(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="user-option"]').click();
cy.contains('[data-e2e="role-checkbox"]', roles[0]).click();
cy.get('[data-e2e="confirm-add-member-button"]').click();
cy.get('.data-e2e-success');
cy.contains('[data-e2e="member-avatar"]', 'ee');
cy.shouldNotExist({ selector: '.data-e2e-failure' });
});
});
describe('mutate authorization', () => {
const rowSelector = `tr:contains(${testManagerLoginname})`;
beforeEach(beforeMutate);
beforeEach(navigate);
beforeEach(() => {
cy.contains('[data-e2e="member-avatar"]', 'ee').click();
cy.get(rowSelector).as('managerRow');
});
it('should remove a manager', () => {
cy.get('@managerRow').find('[data-e2e="remove-member-button"]').click({ force: true });
cy.get('[data-e2e="confirm-dialog-button"]').click();
cy.get('.data-e2e-success');
cy.shouldNotExist({ selector: rowSelector, timeout: 2000 });
cy.shouldNotExist({ selector: '.data-e2e-failure' });
});
it('should remove a managers authorization', () => {
cy.get('@managerRow').find('[data-e2e="role"]').should('have.length', roles.length);
cy.get('@managerRow')
.contains('[data-e2e="role"]', roles[0])
.find('[data-e2e="remove-role-button"]')
.click({ force: true }); // TODO: Is this a bug?
cy.get('[data-e2e="confirm-dialog-button"]').click();
cy.get('.data-e2e-success');
cy.get('@managerRow')
.find('[data-e2e="remove-role-button"]')
.should('have.length', roles.length - 1);
cy.shouldNotExist({ selector: '.data-e2e-failure' });
});
});
}
describe('organizations', () => {
const roles = [
{ internal: 'ORG_OWNER', display: 'Org Owner' },
{ internal: 'ORG_OWNER_VIEWER', display: 'Org Owner Viewer' },
];
testAuthorizations(
roles.map((role) => role.display),
function () {
ensureHumanIsNotOrgMember(this.api, testManagerLoginname);
},
function () {
ensureHumanIsNotOrgMember(this.api, testManagerLoginname);
ensureHumanIsOrgMember(
this.api,
testManagerLoginname,
roles.map((role) => role.internal),
);
},
() => {
cy.visit('/orgs');
cy.contains('tr', Cypress.env('ORGANIZATION')).click();
},
);
});
describe('projects', () => {
describe('owned projects', () => {
beforeEach(function () {
ensureProjectExists(this.api, 'e2eprojectpermission').as('projectId');
});
const visitOwnedProject: Mocha.HookFunction = function () {
cy.visit(`/projects/${this.projectId}`);
};
describe('authorizations', () => {
const roles = [
{ internal: 'PROJECT_OWNER_GLOBAL', display: 'Project Owner Global' },
{ internal: 'PROJECT_OWNER_VIEWER_GLOBAL', display: 'Project Owner Viewer Global' },
];
testAuthorizations(
roles.map((role) => role.display),
function () {
ensureHumanIsNotProjectMember(this.api, this.projectId, testManagerLoginname);
},
function () {
ensureHumanIsNotProjectMember(this.api, this.projectId, testManagerLoginname);
ensureHumanIsProjectMember(
this.api,
this.projectId,
testManagerLoginname,
roles.map((role) => role.internal),
);
},
visitOwnedProject,
);
});
describe('roles', () => {
const testRoleName = 'e2eroleundertestname'; const testRoleName = 'e2eroleundertestname';
const testRoleDisplay = 'e2eroleundertestdisplay';
const testRoleGroup = 'e2eroleundertestgroup';
const testGrantName = 'e2egrantundertest';
var projectId: number; beforeEach(function () {
ensureProjectResourceDoesntExist(this.api, this.projectId, Roles, testRoleName);
beforeEach(() => {
apiAuth().then((apiCalls) => {
ensureProjectExists(apiCalls, testProjectName).then((projId) => {
projectId = projId;
});
});
}); });
describe('add role', () => { beforeEach(visitOwnedProject);
beforeEach(() => {
apiAuth().then((api) => {
ensureProjectResourceDoesntExist(api, projectId, Roles, testRoleName);
cy.visit(`/projects/${projectId}?id=roles`);
});
});
it('should add a role', () => { it('should add a role', () => {
cy.get('[data-e2e="sidenav-element-roles"]').click();
cy.get('[data-e2e="add-new-role"]').click(); cy.get('[data-e2e="add-new-role"]').click();
cy.get('[formcontrolname="key"]').type(testRoleName); cy.get('[formcontrolname="key"]').type(testRoleName);
cy.get('[formcontrolname="displayName"]').type(testRoleDisplay); cy.get('[formcontrolname="displayName"]').type('e2eroleundertestdisplay');
cy.get('[formcontrolname="group"]').type(testRoleGroup); cy.get('[formcontrolname="group"]').type('e2eroleundertestgroup');
cy.get('[data-e2e="save-button"]').click(); cy.get('[data-e2e="save-button"]').click();
cy.get('.data-e2e-success'); cy.get('.data-e2e-success');
cy.wait(200); cy.contains('tr', testRoleName);
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist'); cy.shouldNotExist({ selector: '.data-e2e-failure' });
});
it('should remove a role');
});
});
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');
}); });
}); });
}); });
/*
describe('permissions', () => { const visitGrantedProject: Mocha.HookFunction = function () {
cy.visit(`/granted-projects/${this.projectId}/grant/${this.grantId}`);
};
before(()=> { describe('authorizations', () => {
// cy.consolelogin(Cypress.env('username'), Cypress.env('password'), Cypress.config('baseUrl')/ui/console) const roles = [
}) { internal: 'PROJECT_GRANT_OWNER', display: 'Project Grant Owner' },
{ internal: 'PROJECT_GRANT_OWNER_VIEWER', display: 'Project Grant Owner Viewer' },
];
it('should show projects ', () => { testAuthorizations(
cy.visit(Cypress.config('baseUrl')/ui/console + '/projects') roles.map((role) => role.display),
cy.url().should('contain', '/projects') function () {
}) ensureHumanIsNotProjectMember(this.api, this.projectId, testManagerLoginname, this.grantId);
},
it('should add a role', () => { function () {
cy.visit(Cypress.config('baseUrl')/ui/console + '/org').then(() => { ensureHumanIsNotProjectMember(this.api, this.projectId, testManagerLoginname, this.grantId);
cy.url().should('contain', '/org'); ensureHumanIsProjectMember(
}) this.api,
cy.visit(Cypress.config('baseUrl')/ui/console + '/projects').then(() => { this.projectId,
cy.url().should('contain', '/projects'); testManagerLoginname,
cy.get('.card').should('contain.text', "newProjectToTest") roles.map((role) => role.internal),
}) this.grantId,
cy.get('.card').filter(':contains("newProjectToTest")').click() );
cy.get('.app-container').filter(':contains("newAppToTest")').should('be.visible').click() },
let projectID visitGrantedProject,
cy.url().then(url => { );
cy.log(url.split('/')[4]) });
projectID = url.split('/')[4] });
});
}); });
cy.then(() => cy.visit(Cypress.config('baseUrl')/ui/console + '/projects/' + projectID +'/roles/create')) describe('validations', () => {
cy.get('[formcontrolname^=key]').type("newdemorole") describe('owned projects', () => {
cy.get('[formcontrolname^=displayName]').type("newdemodisplayname") describe('no ownership', () => {
cy.get('[formcontrolname^=group]').type("newdemogroupname") it('a user without project global ownership can ...');
cy.get('button').filter(':contains("Save")').should('be.visible').click() it('a user without project global ownership can not ...');
//let the Role get processed });
cy.wait(5000) describe('project owner viewer global', () => {
}) it('a project owner viewer global additionally can ...');
it('a project owner viewer global still can not ...');
it('should add a grant', () => { });
cy.visit(Cypress.config('baseUrl')/ui/console + '/org').then(() => { describe('project owner global', () => {
cy.url().should('contain', '/org'); it('a project owner global additionally can ...');
}) it('a project owner global still can not ...');
cy.visit(Cypress.config('baseUrl')/ui/console + '/projects').then(() => { });
cy.url().should('contain', '/projects');
cy.get('.card').should('contain.text', "newProjectToTest")
})
cy.get('.card').filter(':contains("newProjectToTest")').click()
cy.get('.app-container').filter(':contains("newAppToTest")').should('be.visible').click()
let projectID
cy.url().then(url => {
cy.log(url.split('/')[4])
projectID = url.split('/')[4]
}); });
cy.then(() => cy.visit(Cypress.config('baseUrl')/ui/console + '/grant-create/project/' + projectID )) describe('granted projects', () => {
cy.get('input').type("demo") describe('no ownership', () => {
cy.get('[role^=listbox]').filter(`:contains("${Cypress.env("fullUserName")}")`).should('be.visible').click() it('a user without project grant ownership can ...');
cy.wait(5000) it('a user without project grant ownership can not ...');
//cy.get('.button').contains('Continue').click() });
cy.get('button').filter(':contains("Continue")').click() describe('project grant owner viewer', () => {
cy.wait(5000) it('a project grant owner viewer additionally can ...');
cy.get('tr').filter(':contains("demo")').find('label').click() it('a project grant owner viewer still can not ...');
cy.get('button').filter(':contains("Save")').should('be.visible').click() });
//let the grant get processed describe('project grant owner', () => {
cy.wait(5000) 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

@ -2,15 +2,16 @@ import { apiAuth } from '../../support/api/apiauth';
import { ensureProjectDoesntExist, ensureProjectExists } from '../../support/api/projects'; import { ensureProjectDoesntExist, ensureProjectExists } from '../../support/api/projects';
describe('projects', () => { describe('projects', () => {
beforeEach(() => {
apiAuth().as('api');
});
const testProjectNameCreate = 'e2eprojectcreate'; const testProjectNameCreate = 'e2eprojectcreate';
const testProjectNameDeleteList = 'e2eprojectdeletelist'; const testProjectNameDelete = 'e2eprojectdelete';
const testProjectNameDeleteGrid = 'e2eprojectdeletegrid';
describe('add project', () => { describe('add project', () => {
beforeEach(`ensure it doesn't exist already`, () => { beforeEach(`ensure it doesn't exist already`, function () {
apiAuth().then((api) => { ensureProjectDoesntExist(this.api, testProjectNameCreate);
ensureProjectDoesntExist(api, testProjectNameCreate);
});
cy.visit(`/projects`); cy.visit(`/projects`);
}); });
@ -19,52 +20,43 @@ describe('projects', () => {
cy.get('input').type(testProjectNameCreate); cy.get('input').type(testProjectNameCreate);
cy.get('[data-e2e="continue-button"]').click(); cy.get('[data-e2e="continue-button"]').click();
cy.get('.data-e2e-success'); cy.get('.data-e2e-success');
cy.wait(200); cy.shouldNotExist({ selector: '.data-e2e-failure' });
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist');
}); });
it('should configure a project to assert roles on authentication');
});
describe('edit project', () => {
beforeEach('ensure it exists', function () {
ensureProjectExists(this.api, testProjectNameDelete);
cy.visit(`/projects`);
}); });
describe('remove project', () => { describe('remove project', () => {
describe('list view', () => { it('removes the project from list view', () => {
beforeEach('ensure it exists', () => { const rowSelector = `tr:contains(${testProjectNameDelete})`;
apiAuth().then((api) => {
ensureProjectExists(api, testProjectNameDeleteList);
});
cy.visit(`/projects`);
});
it('removes the project', () => {
cy.get('[data-e2e="toggle-grid"]').click(); cy.get('[data-e2e="toggle-grid"]').click();
cy.get('[data-e2e="timestamp"]'); cy.get('[data-e2e="timestamp"]');
cy.contains('tr', testProjectNameDeleteList, { timeout: 1000 }) cy.get(rowSelector).find('[data-e2e="delete-project-button"]').click({ force: true });
.find('[data-e2e="delete-project-button"]') cy.get('[data-e2e="confirm-dialog-input"]').focus().type(testProjectNameDelete);
.click({ force: true });
cy.get('[data-e2e="confirm-dialog-input"]').focus().type(testProjectNameDeleteList);
cy.get('[data-e2e="confirm-dialog-button"]').click(); cy.get('[data-e2e="confirm-dialog-button"]').click();
cy.get('.data-e2e-success'); cy.get('.data-e2e-success');
cy.wait(200); cy.shouldNotExist({ selector: rowSelector, timeout: 2000 });
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist'); cy.shouldNotExist({ selector: '.data-e2e-failure' });
});
it('removes the project from grid view', () => {
const cardSelector = `[data-e2e="grid-card"]:contains(${testProjectNameDelete})`;
cy.get(cardSelector).find('[data-e2e="delete-project-button"]').click({ force: true });
cy.get('[data-e2e="confirm-dialog-input"]').focus().type(testProjectNameDelete);
cy.get('[data-e2e="confirm-dialog-button"]').click();
cy.get('.data-e2e-success');
cy.shouldNotExist({ selector: cardSelector, timeout: 2000 });
cy.shouldNotExist({ selector: '.data-e2e-failure' });
}); });
}); });
describe('grid view', () => { it('should add a project manager');
beforeEach('ensure it exists', () => { it('should remove a project manager');
apiAuth().then((api) => {
ensureProjectExists(api, testProjectNameDeleteGrid);
});
cy.visit(`/projects`);
});
it('removes the project', () => {
cy.contains('[data-e2e="grid-card"]', testProjectNameDeleteGrid)
.find('[data-e2e="delete-project-button"]')
.click({ force: true });
cy.get('[data-e2e="confirm-dialog-input"]').focus().type(testProjectNameDeleteGrid);
cy.get('[data-e2e="confirm-dialog-button"]').click();
cy.get('.data-e2e-success');
cy.wait(200);
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist');
});
});
}); });
}); });

View File

@ -8,7 +8,7 @@ describe('oidc settings', () => {
const refreshTokenExpirationPrecondition = 7; const refreshTokenExpirationPrecondition = 7;
const refreshTokenIdleExpirationPrecondition = 2; const refreshTokenIdleExpirationPrecondition = 2;
before(`ensure they are set`, () => { beforeEach(`ensure they are set`, () => {
apiAuth().then((apiCallProperties) => { apiAuth().then((apiCallProperties) => {
ensureOIDCSettingsSet( ensureOIDCSettingsSet(
apiCallProperties, apiCallProperties,

View File

@ -1,4 +1,4 @@
import { apiAuth, apiCallProperties } from '../../support/api/apiauth'; import { apiAuth, API } from '../../support/api/apiauth';
import { Policy, resetPolicy } from '../../support/api/policies'; import { Policy, resetPolicy } from '../../support/api/policies';
import { login, User } from '../../support/login/users'; import { login, User } from '../../support/login/users';
@ -7,7 +7,7 @@ describe('private labeling', () => {
[User.OrgOwner].forEach((user) => { [User.OrgOwner].forEach((user) => {
describe(`as user "${user}"`, () => { describe(`as user "${user}"`, () => {
let api: apiCallProperties; let api: API;
beforeEach(() => { beforeEach(() => {
login(user); login(user);

View File

@ -1,17 +1,23 @@
import { login, User } from 'support/login/users'; import { login, User } from 'support/login/users';
import { API } from './types';
export interface apiCallProperties { const authHeaderKey = 'Authorization',
authHeader: string; orgIdHeaderKey = 'x-zitadel-orgid';
mgntBaseURL: string;
adminBaseURL: string;
}
export function apiAuth(): Cypress.Chainable<apiCallProperties> { export function apiAuth(): Cypress.Chainable<API> {
return login(User.IAMAdminUser, 'Password1!', false, true).then((token) => { return login(User.IAMAdminUser, 'Password1!', false, true).then((token) => {
return <apiCallProperties>{ return <API>{
authHeader: `Bearer ${token}`, token: token,
mgntBaseURL: `${Cypress.env('BACKEND_URL')}/management/v1/`, mgmtBaseURL: `${Cypress.env('BACKEND_URL')}/management/v1`,
adminBaseURL: `${Cypress.env('BACKEND_URL')}/admin/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( export function ensureItemExists(
api: apiCallProperties, api: API,
searchPath: string, searchPath: string,
find: (entity: any) => boolean, findInList: (entity: Entity) => boolean,
createPath: string, createPath: string,
body: any, body: Entity,
orgId?: number,
newItemIdField: string = 'id',
searchItemIdField?: string,
): Cypress.Chainable<number> { ): Cypress.Chainable<number> {
return searchSomething(api, searchPath, find) return ensureSomething(
.then((sRes) => { api,
if (sRes.entity) { () => searchSomething(api, searchPath, 'POST', mapFromList(findInList, searchItemIdField), orgId),
return cy.wrap({ () => createPath,
id: sRes.entity.id, 'POST',
initialSequence: 0, body,
}); (entity) => !!entity,
} (body) => body[newItemIdField],
return cy orgId,
.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);
});
} }
export function ensureSomethingIsSet( export function ensureItemDoesntExist(
api: apiCallProperties, api: API,
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,
searchPath: string, searchPath: string,
find: (entity: any) => boolean, findInList: (entity: Entity) => boolean,
deletePath: (entity: any) => string, deletePath: (entity: Entity) => string,
orgId?: number,
): Cypress.Chainable<null> { ): Cypress.Chainable<null> {
return searchSomething(api, searchPath, find) return ensureSomething(
.then((sRes) => { api,
if (!sRes.entity) { () => searchSomething(api, searchPath, 'POST', mapFromList(findInList), orgId),
return cy.wrap(0); deletePath,
} 'DELETE',
return cy null,
.request({ (entity) => !entity,
method: 'DELETE', ).then(() => null);
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;
});
} }
type SearchResult = { export function ensureSetting(
entity: any; api: API,
sequence: number; path: string,
}; mapResult: (entity: any) => SearchResult,
createPath: string,
function searchSomething( body: any,
api: apiCallProperties, orgId?: number,
searchPath: string, ): Cypress.Chainable<number> {
find: (entity: any) => boolean, return ensureSomething(
): Cypress.Chainable<SearchResult> { api,
return cy () => searchSomething(api, path, 'GET', mapResult, orgId),
.request({ () => createPath,
method: 'POST', 'PUT',
url: `${api.mgntBaseURL}${searchPath}`, body,
headers: { (entity) => !!entity,
Authorization: api.authHeader, (body) => body?.settings?.id,
}, );
})
.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);
});
} }
function awaitDesired( function awaitDesired(
trials: number, trials: number,
expectEntity: (entity: any) => boolean, expectEntity: (entity: Entity) => boolean,
initialSequence: number, search: () => Cypress.Chainable<SearchResult>,
api: apiCallProperties, initialSequence?: number,
searchPath: string,
find: (entity: any) => boolean,
) { ) {
searchSomething(api, searchPath, find).then((resp) => { search().then((resp) => {
const foundExpectedEntity = expectEntity(resp.entity); const foundExpectedEntity = expectEntity(resp.entity);
const foundExpectedSequence = resp.sequence > initialSequence; const foundExpectedSequence = !initialSequence || resp.sequence >= initialSequence;
if (!foundExpectedEntity || !foundExpectedSequence) { if (!foundExpectedEntity || !foundExpectedSequence) {
expect(trials, `trying ${trials} more times`).to.be.greaterThan(0); expect(trials, `trying ${trials} more times`).to.be.greaterThan(0);
cy.wait(1000); cy.wait(1000);
awaitDesired(trials - 1, expectEntity, initialSequence, api, searchPath, find); awaitDesired(trials - 1, expectEntity, search, initialSequence);
} }
}); });
} }
function awaitDesiredById( interface EnsuredResult {
trials: number, id: number;
expectEntity: (entity: any) => boolean, sequence: number;
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);
} }
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 { ensureSetting } from './ensure';
import { ensureSomethingIsSet } from './ensure'; import { API } from './types';
export function ensureOIDCSettingsSet( export function ensureOIDCSettingsSet(
api: apiCallProperties, api: API,
accessTokenLifetime, accessTokenLifetime: number,
idTokenLifetime, idTokenLifetime: number,
refreshTokenExpiration, refreshTokenExpiration: number,
refreshTokenIdleExpiration: number, refreshTokenIdleExpiration: number,
): Cypress.Chainable<number> { ): Cypress.Chainable<number> {
return ensureSomethingIsSet( return ensureSetting(
api, api,
`${api.adminBaseURL}settings/oidc`, `${api.adminBaseURL}/settings/oidc`,
(settings: any) => { (body: any) => {
let entity = null; const result = {
if ( sequence: body.settings?.details?.sequence,
settings.settings?.accessTokenLifetime === hoursToDuration(accessTokenLifetime) && id: body.settings.id,
settings.settings?.idTokenLifetime === hoursToDuration(idTokenLifetime) && entity: null,
settings.settings?.refreshTokenExpiration === daysToDuration(refreshTokenExpiration) &&
settings.settings?.refreshTokenIdleExpiration === daysToDuration(refreshTokenIdleExpiration)
) {
entity = settings.settings;
}
return {
entity: entity,
sequence: settings.settings?.details?.sequence,
}; };
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), accessTokenLifetime: hoursToDuration(accessTokenLifetime),
idTokenLifetime: hoursToDuration(idTokenLifetime), 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 { export enum Policy {
Label = 'label', Label = 'label',
} }
export function resetPolicy(api: apiCallProperties, policy: Policy) { export function resetPolicy(api: API, policy: Policy) {
cy.request({ cy.request({
method: 'DELETE', method: 'DELETE',
url: `${api.mgntBaseURL}/policies/${policy}`, url: `${api.mgmtBaseURL}/policies/${policy}`,
headers: { headers: requestHeaders(api),
Authorization: api.authHeader,
},
}).then((res) => { }).then((res) => {
expect(res.status).to.equal(200); expect(res.status).to.equal(200);
return null; return null;

View File

@ -1,18 +1,24 @@
import { apiCallProperties } from './apiauth'; import { ensureItemDoesntExist, ensureItemExists } from './ensure';
import { ensureSomethingDoesntExist, ensureSomethingExists } from './ensure'; import { API } from './types';
export function ensureProjectExists(api: apiCallProperties, projectName: string): Cypress.Chainable<number> { export function ensureProjectExists(api: API, projectName: string, orgId?: number): Cypress.Chainable<number> {
return ensureSomethingExists(api, `projects/_search`, (project: any) => project.name === projectName, 'projects', { return ensureItemExists(
name: projectName, 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> { export function ensureProjectDoesntExist(api: API, projectName: string, orgId?: number): Cypress.Chainable<null> {
return ensureSomethingDoesntExist( return ensureItemDoesntExist(
api, api,
`projects/_search`, `${api.mgmtBaseURL}/projects/_search`,
(project: any) => project.name === projectName, (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 const Grants = new ResourceType('apps', 'name')
export function ensureProjectResourceDoesntExist( export function ensureProjectResourceDoesntExist(
api: apiCallProperties, api: API,
projectId: number, projectId: number,
resourceType: ResourceType, resourceType: ResourceType,
resourceName: string, resourceName: string,
orgId?: number,
): Cypress.Chainable<null> { ): Cypress.Chainable<null> {
return ensureSomethingDoesntExist( return ensureItemDoesntExist(
api, api,
`projects/${projectId}/${resourceType.resourcePath}/_search`, `${api.mgmtBaseURL}/projects/${projectId}/${resourceType.resourcePath}/_search`,
(resource: any) => { (resource: any) => resource[resourceType.compareProperty] === resourceName,
return resource[resourceType.compareProperty] === resourceName; (resource) =>
}, `${api.mgmtBaseURL}/projects/${projectId}/${resourceType.resourcePath}/${resource[resourceType.identifierProperty]}`,
(resource) => { orgId,
return `projects/${projectId}/${resourceType.resourcePath}/${resource[resourceType.identifierProperty]}`;
},
); );
} }
export function ensureApplicationExists( export function ensureApplicationExists(api: API, projectId: number, appName: string): Cypress.Chainable<number> {
api: apiCallProperties, return ensureItemExists(
projectId: number,
appName: string,
): Cypress.Chainable<number> {
return ensureSomethingExists(
api, api,
`projects/${projectId}/${Apps.resourcePath}/_search`, `${api.mgmtBaseURL}/projects/${projectId}/${Apps.resourcePath}/_search`,
(resource: any) => resource.name === appName, (resource: any) => resource.name === appName,
`projects/${projectId}/${Apps.resourcePath}/oidc`, `${api.mgmtBaseURL}/projects/${projectId}/${Apps.resourcePath}/oidc`,
{ {
name: appName, name: appName,
redirectUris: ['https://e2eredirecturl.org'], redirectUris: ['https://e2eredirecturl.org'],
@ -59,11 +60,6 @@ export function ensureApplicationExists(
grantTypes: ['OIDC_GRANT_TYPE_AUTHORIZATION_CODE'], grantTypes: ['OIDC_GRANT_TYPE_AUTHORIZATION_CODE'],
authMethodType: 'OIDC_AUTH_METHOD_TYPE_NONE', authMethodType: 'OIDC_AUTH_METHOD_TYPE_NONE',
postLogoutRedirectUris: ['https://e2elogoutredirecturl.org'], 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,8 +1,13 @@
import { apiCallProperties } from './apiauth'; import { ensureItemDoesntExist, ensureItemExists } from './ensure';
import { ensureSomethingDoesntExist, ensureSomethingExists } from './ensure'; import { API } from './types';
export function ensureHumanUserExists(api: apiCallProperties, username: string): Cypress.Chainable<number> { export function ensureHumanUserExists(api: API, username: string): Cypress.Chainable<number> {
return ensureSomethingExists(api, 'users/_search', (user: any) => user.userName === username, 'users/human', { return ensureItemExists(
api,
`${api.mgmtBaseURL}/users/_search`,
(user: any) => user.userName === username,
`${api.mgmtBaseURL}/users/human`,
{
user_name: username, user_name: username,
profile: { profile: {
first_name: 'e2efirstName', first_name: 'e2efirstName',
@ -14,22 +19,33 @@ export function ensureHumanUserExists(api: apiCallProperties, username: string):
phone: { phone: {
phone: '+41 123456789', phone: '+41 123456789',
}, },
}); },
undefined,
'userId',
);
} }
export function ensureMachineUserExists(api: apiCallProperties, username: string): Cypress.Chainable<number> { export function ensureMachineUserExists(api: API, username: string): Cypress.Chainable<number> {
return ensureSomethingExists(api, 'users/_search', (user: any) => user.userName === username, 'users/machine', { return ensureItemExists(
api,
`${api.mgmtBaseURL}/users/_search`,
(user: any) => user.userName === username,
`${api.mgmtBaseURL}/users/machine`,
{
user_name: username, user_name: username,
name: 'e2emachinename', name: 'e2emachinename',
description: 'e2emachinedescription', description: 'e2emachinedescription',
}); },
} undefined,
'userId',
export function ensureUserDoesntExist(api: apiCallProperties, username: string): Cypress.Chainable<null> { );
return ensureSomethingDoesntExist( }
api,
'users/_search', export function ensureUserDoesntExist(api: API, username: string): Cypress.Chainable<null> {
(user: any) => user.userName === username, return ensureItemDoesntExist(
(user) => `users/${user.id}`, api,
`${api.mgmtBaseURL}/users/_search`,
(user: any) => user.userName === username,
(user) => `${api.mgmtBaseURL}/users/${user.id}`,
); );
} }

View File

@ -1,26 +1,80 @@
/* 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;
}
declare global {
namespace Cypress { namespace Cypress {
interface Chainable { interface Chainable {
*/
/** /**
* Custom command that authenticates a user. * Custom command that asserts on clipboard text.
* *
* @example cy.consolelogin('hodor', 'hodor1234') * @example cy.clipboardMatches('hodor', 'hodor1234')
*/ */
/* consolelogin(username: string, password: string): void clipboardMatches(pattern: RegExp | string): Cypress.Chainable<null>;
/**
* Custom command that waits until the selector finds zero elements.
*/
shouldNotExist(options?: ShouldNotExistOptions): Cypress.Chainable<null>;
}
} }
} }
Cypress.Commands.add('consolelogin', { prevSubject: false }, (username: string, password: string) => { Cypress.Commands.add('clipboardMatches', { prevSubject: false }, (pattern: RegExp | string) => {
/* doesn't work reliably
window.sessionStorage.removeItem("zitadel:access_token") return cy.window()
cy.visit(Cypress.config('baseUrl')/ui/console).then(() => { .then(win => {
// fill the fields and push button win.focus()
cy.get('#loginName').type(username, { log: false }) return cy.waitUntil(() => win.navigator.clipboard.readText()
cy.get('#submit-button').click() .then(clipboadText => {
cy.get('#password').type(password, { log: false }) win.focus()
cy.get('#submit-button').click() const matches = typeof pattern === "string"
cy.location('pathname', {timeout: 5 * 1000}).should('eq', '/'); ? 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 },
);
});

View File

@ -43,7 +43,7 @@ services:
network_mode: host network_mode: host
e2e: e2e:
image: cypress/included:10.3.0 image: cypress/included:10.9.0
depends_on: depends_on:
zitadel: zitadel:
condition: 'service_started' condition: 'service_started'

57
e2e/package-lock.json generated
View File

@ -8,16 +8,17 @@
"name": "zitadel-e2e", "name": "zitadel-e2e",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"cypress-wait-until": "^1.7.2",
"debug": "^4.3.4", "debug": "^4.3.4",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"mochawesome": "^7.1.3", "mochawesome": "^7.1.3",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"typescript": "^4.8.3", "typescript": "^4.8.4",
"wait-on": "^6.0.1" "wait-on": "^6.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^18.7.13", "@types/node": "^18.8.3",
"cypress": "^10.3.0" "cypress": "^10.9.0"
} }
}, },
"node_modules/@colors/colors": { "node_modules/@colors/colors": {
@ -110,9 +111,9 @@
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "18.7.13", "version": "18.8.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.13.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.8.3.tgz",
"integrity": "sha512-46yIhxSe5xEaJZXWdIBP7GU4HDTG8/eo0qd9atdiL+lFpA03y8KS+lkTN834TWJj5767GbWv4n/P6efyTFt1Dw==", "integrity": "sha512-0os9vz6BpGwxGe9LOhgP/ncvYN5Tx1fNcd2TM3rD/aCGBkysb+ZWpXEocG24h6ZzOi13+VB8HndAQFezsSOw1w==",
"dev": true "dev": true
}, },
"node_modules/@types/sinonjs__fake-timers": { "node_modules/@types/sinonjs__fake-timers": {
@ -667,9 +668,9 @@
} }
}, },
"node_modules/cypress": { "node_modules/cypress": {
"version": "10.3.0", "version": "10.9.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.3.0.tgz", "resolved": "https://registry.npmjs.org/cypress/-/cypress-10.9.0.tgz",
"integrity": "sha512-txkQWKzvBVnWdCuKs5Xc08gjpO89W2Dom2wpZgT9zWZT5jXxqPIxqP/NC1YArtkpmp3fN5HW8aDjYBizHLUFvg==", "integrity": "sha512-MjIWrRpc+bQM9U4kSSdATZWZ2hUqHGFEQTF7dfeZRa4MnalMtc88FIE49USWP2ZVtfy5WPBcgfBX+YorFqGElA==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
@ -692,7 +693,7 @@
"dayjs": "^1.10.4", "dayjs": "^1.10.4",
"debug": "^4.3.2", "debug": "^4.3.2",
"enquirer": "^2.3.6", "enquirer": "^2.3.6",
"eventemitter2": "^6.4.3", "eventemitter2": "6.4.7",
"execa": "4.1.0", "execa": "4.1.0",
"executable": "^4.1.1", "executable": "^4.1.1",
"extract-zip": "2.0.1", "extract-zip": "2.0.1",
@ -723,6 +724,11 @@
"node": ">=12.0.0" "node": ">=12.0.0"
} }
}, },
"node_modules/cypress-wait-until": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/cypress-wait-until/-/cypress-wait-until-1.7.2.tgz",
"integrity": "sha512-uZ+M8/MqRcpf+FII/UZrU7g1qYZ4aVlHcgyVopnladyoBrpoaMJ4PKZDrdOJ05H5RHbr7s9Tid635X3E+ZLU/Q=="
},
"node_modules/cypress/node_modules/@types/node": { "node_modules/cypress/node_modules/@types/node": {
"version": "14.18.26", "version": "14.18.26",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.26.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.26.tgz",
@ -2559,9 +2565,9 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "4.8.3", "version": "4.8.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz",
"integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@ -2843,9 +2849,9 @@
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
}, },
"@types/node": { "@types/node": {
"version": "18.7.13", "version": "18.8.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.13.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.8.3.tgz",
"integrity": "sha512-46yIhxSe5xEaJZXWdIBP7GU4HDTG8/eo0qd9atdiL+lFpA03y8KS+lkTN834TWJj5767GbWv4n/P6efyTFt1Dw==", "integrity": "sha512-0os9vz6BpGwxGe9LOhgP/ncvYN5Tx1fNcd2TM3rD/aCGBkysb+ZWpXEocG24h6ZzOi13+VB8HndAQFezsSOw1w==",
"dev": true "dev": true
}, },
"@types/sinonjs__fake-timers": { "@types/sinonjs__fake-timers": {
@ -3251,9 +3257,9 @@
} }
}, },
"cypress": { "cypress": {
"version": "10.3.0", "version": "10.9.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.3.0.tgz", "resolved": "https://registry.npmjs.org/cypress/-/cypress-10.9.0.tgz",
"integrity": "sha512-txkQWKzvBVnWdCuKs5Xc08gjpO89W2Dom2wpZgT9zWZT5jXxqPIxqP/NC1YArtkpmp3fN5HW8aDjYBizHLUFvg==", "integrity": "sha512-MjIWrRpc+bQM9U4kSSdATZWZ2hUqHGFEQTF7dfeZRa4MnalMtc88FIE49USWP2ZVtfy5WPBcgfBX+YorFqGElA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@cypress/request": "^2.88.10", "@cypress/request": "^2.88.10",
@ -3275,7 +3281,7 @@
"dayjs": "^1.10.4", "dayjs": "^1.10.4",
"debug": "^4.3.2", "debug": "^4.3.2",
"enquirer": "^2.3.6", "enquirer": "^2.3.6",
"eventemitter2": "^6.4.3", "eventemitter2": "6.4.7",
"execa": "4.1.0", "execa": "4.1.0",
"executable": "^4.1.1", "executable": "^4.1.1",
"extract-zip": "2.0.1", "extract-zip": "2.0.1",
@ -3308,6 +3314,11 @@
} }
} }
}, },
"cypress-wait-until": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/cypress-wait-until/-/cypress-wait-until-1.7.2.tgz",
"integrity": "sha512-uZ+M8/MqRcpf+FII/UZrU7g1qYZ4aVlHcgyVopnladyoBrpoaMJ4PKZDrdOJ05H5RHbr7s9Tid635X3E+ZLU/Q=="
},
"dashdash": { "dashdash": {
"version": "1.14.1", "version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@ -4663,9 +4674,9 @@
"dev": true "dev": true
}, },
"typescript": { "typescript": {
"version": "4.8.3", "version": "4.8.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz",
"integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==" "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ=="
}, },
"universalify": { "universalify": {
"version": "2.0.0", "version": "2.0.0",

View File

@ -4,22 +4,23 @@
"scripts": { "scripts": {
"open": "npx cypress open", "open": "npx cypress open",
"e2e": "npx cypress run", "e2e": "npx cypress run",
"open:dev": "CYPRESS_BASE_URL=http://localhost:4200 CYPRESS_BACKEND_URL=http://localhost:8080 npm run open", "open:dev": "CYPRESS_BASE_URL=http://localhost:4200 CYPRESS_BACKEND_URL=http://localhost:8080 npm run open --",
"e2e:dev": "CYPRESS_BASE_URL=http://localhost:4200 CYPRESS_BACKEND_URL=http://localhost:8080 npm run e2e", "e2e:dev": "CYPRESS_BASE_URL=http://localhost:4200 CYPRESS_BACKEND_URL=http://localhost:8080 npm run e2e --",
"lint": "prettier --check cypress", "lint": "prettier --check cypress",
"lint:fix": "prettier --write cypress" "lint:fix": "prettier --write cypress"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"cypress-wait-until": "^1.7.2",
"debug": "^4.3.4", "debug": "^4.3.4",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"mochawesome": "^7.1.3", "mochawesome": "^7.1.3",
"wait-on": "^6.0.1", "prettier": "^2.7.1",
"typescript": "^4.8.3", "typescript": "^4.8.4",
"prettier": "^2.7.1" "wait-on": "^6.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^18.7.13", "@types/node": "^18.8.3",
"cypress": "^10.3.0" "cypress": "^10.9.0"
} }
} }