chore: rehaul DevX (#10571)

# Which Problems Are Solved

Replaces Turbo by Nx and lays the foundation for the next CI
improvements. It enables using Nx Cloud to speed the up the pipelines
that affect any node package.
It streamlines the dev experience for frontend and backend developers by
providing the following commands:

| Task | Command | Notes |
|------|---------|--------|
| **Production** | `nx run PROJECT:prod` | Production server |
| **Develop** | `nx run PROJECT:dev` | Hot reloading development server
|
| **Test** | `nx run PROJECT:test` | Run all tests |
| **Lint** | `nx run PROJECT:lint` | Check code style |
| **Lint Fix** | `nx run PROJECT:lint-fix` | Auto-fix style issues |

The following values can be used for PROJECT:

- @zitadel/zitadel (root commands)
- @zitadel/api,
- @zitadel/login,
- @zitadel/console,
- @zitadel/docs,
- @zitadel/client
- @zitadel/proto

The project names and folders are streamlined:

| Old Folder | New Folder |
| --- | --- |
| ./e2e | ./tests/functional-ui |
| ./load-test | ./benchmark |
| ./build/zitadel | ./apps/api |
| ./console | ./apps/console (postponed so the PR is reviewable) |  

Also, all references to the TypeScript repo are removed so we can
archive it.

# How the Problems Are Solved

- Ran `npx nx@latest init`
- Replaced all turbo.json by project.json and fixed the target configs
- Removed Turbo dependency
- All JavaScript related code affected by a PRs changes is
quality-checked using the `nx affected` command
- We move PR checks that are runnable using Nx into the `check`
workflow. For workflows where we don't use Nx, yet, we restore
previously built dependency artifacts from Nx.
- We only use a single and easy to understand dev container
- The CONTRIBUTING.md is streamlined
- The setup with a generated client pat is orchestrated with Nx
- Everything related to the TypeScript repo is updated or removed. A
**Deploy with Vercel** button is added to the docs and the
CONTRIBUTING.md.

# Additional Changes

- NPM package names have a consistent pattern.
- Docker bake is removed. The login container is built and released like
the core container.
- The integration tests build the login container before running, so
they don't rely on the login container action anymore. This fixes
consistently failing checks on PRs from forks.
- The docs build in GitHub actions is removed, as we already build on
Vercel.

# Additional Context

- Internal discussion:
https://zitadel.slack.com/archives/C087ADF8LRX/p1756277884928169
- Workflow dispatch test:
https://github.com/zitadel/zitadel/actions/runs/17760122959

---------

Co-authored-by: Florian Forster <[email protected]>
Co-authored-by: Tim Möhlmann <[email protected]>
Co-authored-by: Copilot <[email protected]>
This commit is contained in:
Elio Bischof
2025-10-08 10:27:02 +02:00
committed by GitHub
co-authored by Florian Forster Tim Möhlmann Copilot
parent e47acaeef2
commit f69a6ed4f3
236 changed files with 5708 additions and 6549 deletions
+4
View File
@@ -0,0 +1,4 @@
results
videos
screenshots
downloads
@@ -0,0 +1,81 @@
import { Apps, ensureProjectExists, ensureProjectResourceDoesntExist } from '../../support/api/projects';
import { Context } from 'support/commands';
const testProjectName = 'e2eprojectapplication';
const testPKCEAppName = 'e2eapppkcetest';
const testDEVICECODEAppName = 'e2eappdevicecodetest';
describe('applications', () => {
beforeEach(() => {
cy.context()
.as('ctx')
.then((ctx) => {
ensureProjectExists(ctx.api, testProjectName).as('projectId');
});
});
describe('add app', () => {
beforeEach(`ensure it doesn't exist already`, () => {
cy.get<Context>('@ctx').then((ctx) => {
cy.get<string>('@projectId').then((projectId) => {
ensureProjectResourceDoesntExist(ctx.api, projectId, Apps, testPKCEAppName);
cy.visit(`/projects/${projectId}`);
});
});
});
it('add web pkce app', () => {
cy.get('[data-e2e="app-card-add"]').should('be.visible').click();
cy.get('[formcontrolname="name"]').focus().should('be.enabled').type(testPKCEAppName);
cy.get('[for="WEB"]').click();
cy.get('[data-e2e="continue-button-nameandtype"]').click();
cy.get('[for="PKCE"]').should('be.visible').click();
cy.get('[data-e2e="continue-button-authmethod"]').click();
cy.get('[data-e2e="redirect-uris"] input')
.focus()
.should('be.enabled')
.type('http://localhost:3000/api/auth/callback/zitadel');
cy.get('[data-e2e="postlogout-uris"] input').focus().should('be.enabled').type('http://localhost:3000');
cy.get('[data-e2e="continue-button-redirecturis"]').click();
cy.get('[data-e2e="create-button"]').click();
cy.get('[id*=overlay]').should('exist');
cy.shouldConfirmSuccess();
const expectClientId = new RegExp(`^.*[0-9]+.*$`);
cy.get('[data-e2e="client-id-copy"]').click();
cy.contains('[data-e2e="client-id"]', expectClientId);
cy.clipboardMatches(expectClientId);
});
});
describe('add native device code app', () => {
beforeEach(`ensure it doesn't exist already`, () => {
cy.get<Context>('@ctx').then((ctx) => {
cy.get<string>('@projectId').then((projectId) => {
ensureProjectResourceDoesntExist(ctx.api, projectId, Apps, testDEVICECODEAppName);
cy.visit(`/projects/${projectId}`);
});
});
});
it('add device code app', () => {
cy.get('[data-e2e="app-card-add"]').should('be.visible').click();
cy.get('[formcontrolname="name"]').focus().should('be.enabled').type(testDEVICECODEAppName);
cy.get('[for="N"]').click();
cy.get('[data-e2e="continue-button-nameandtype"]').click();
cy.get('[for="DEVICECODE"]').should('be.visible').click();
cy.get('[data-e2e="continue-button-authmethod"]').click();
cy.get('[data-e2e="create-button"]').click();
cy.get('[id*=overlay]').should('exist');
cy.shouldConfirmSuccess();
const expectClientId = new RegExp(`^.*[0-9]+.*$`);
cy.get('[data-e2e="client-id-copy"]').click();
cy.contains('[data-e2e="client-id"]', expectClientId);
cy.clipboardMatches(expectClientId);
});
});
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');
});
});
@@ -0,0 +1,20 @@
describe('events', () => {
beforeEach(() => {
cy.context().as('ctx');
});
it('events can be filtered', () => {
const eventTypeEnglish = 'Instance added';
cy.visit('/instance?id=events');
cy.get('[data-e2e="event-type-cell"]').should('have.length', 20);
cy.get('[data-e2e="open-filter-button"]').click();
cy.get('[data-e2e="event-type-filter-checkbox"]').click();
cy.get('mat-select[name="eventTypesList"]').click();
cy.contains('mat-option', eventTypeEnglish).click();
cy.get('body').type('{esc}');
cy.contains('mat-select', 'Descending').click();
cy.contains('mat-option', 'Descending').click();
cy.get('[data-e2e="filter-finish-button"]').click();
cy.get('[data-e2e="event-type-cell"]').should('have.length', 1);
});
});
@@ -0,0 +1,68 @@
import { ensureHumanUserExists, ensureUserDoesntExist } from '../../support/api/users';
import { loginname } from '../../support/login/users';
import { ensureDomainPolicy } from '../../support/api/policies';
import { Context } from 'support/commands';
describe('humans', () => {
const humansPath = `/users?type=human`;
beforeEach(() => {
cy.context().as('ctx');
});
[
{ mustBeDomain: false, addName: 'e2ehumanusernameaddGlobal', removeName: 'e2ehumanusernameremoveGlobal' },
{ mustBeDomain: false, addName: '[email protected]', removeName: '[email protected]' },
// TODO:Changing the policy return 409 User already exists (SQL-M0dsf)
// { mustBeDomain: true, addName: 'e2ehumanusernameadd', removeName: 'e2ehumanusernameremove' },
].forEach((user) => {
describe(`add "${user.addName}" with domain setting "${user.mustBeDomain}"`, () => {
beforeEach(`ensure it doesn't exist already`, () => {
cy.get<Context>('@ctx').then((ctx) => {
ensureUserDoesntExist(ctx.api, user.addName);
ensureDomainPolicy(ctx.api, user.mustBeDomain, true, false);
cy.visit(humansPath);
});
});
it('should add a user', () => {
cy.get('[data-e2e="create-user-button"]').should('be.visible').click();
cy.url().should('contain', 'users/create');
cy.get('[formcontrolname="email"]').should('be.enabled').type('[email protected]');
//force needed due to the prefilled username prefix
cy.get('[formcontrolname="username"]').should('be.enabled').type(user.addName);
cy.get('[formcontrolname="givenName"]').should('be.enabled').type('e2ehumanfirstname');
cy.get('[formcontrolname="familyName"]').should('be.enabled').type('e2ehumanlastname');
cy.get('[data-e2e="create-button"]').click({ force: true });
cy.shouldConfirmSuccess();
let loginName = user.addName;
if (user.mustBeDomain) {
loginName = loginname(user.addName, Cypress.env('ORGANIZATION'));
}
cy.contains('[data-e2e="copy-loginname"]', loginName).click();
cy.clipboardMatches(loginName);
});
});
describe(`remove "${user.removeName}" with domain setting "${user.mustBeDomain}"`, () => {
beforeEach('ensure it exists', () => {
cy.get<Context>('@ctx').then((ctx) => {
ensureHumanUserExists(ctx.api, user.removeName);
});
cy.visit(humansPath);
});
it('should delete a human user', () => {
const rowSelector = `tr:contains(${user.removeName})`;
cy.get(rowSelector).find('[data-e2e="enabled-delete-button"]').click({ force: true });
cy.get('[data-e2e="confirm-dialog-input"]').focus().should('be.enabled').type(user.removeName);
cy.get('[data-e2e="confirm-dialog-button"]').click();
cy.shouldConfirmSuccess();
cy.shouldNotExist({
selector: rowSelector,
timeout: { ms: 2000, errMessage: 'timed out before human disappeared from the table' },
});
});
});
});
});
@@ -0,0 +1,23 @@
import { getInstance } from 'support/api/instances';
import { ensureQuotaIsRemoved, removeQuota, Unit } from 'support/api/quota';
import { Context } from 'support/commands';
describe('api internationalization', () => {
beforeEach(() => {
cy.context()
.as('ctx')
.then((ctx) => {
ensureQuotaIsRemoved(ctx, Unit.ExecutionSeconds);
});
});
it('instance not found error should be translated', () => {
cy.get<Context>('@ctx').then((ctx) => {
removeQuota(ctx, Unit.ExecutionSeconds, false).then((res) => {
expect(res.body.message).to.contain('Quota not found for this unit');
});
getInstance(ctx.system, "this ID clearly doesn't exist", false).then((res) => {
expect(res.body.message).to.contain('Instance not found');
});
});
});
});
@@ -0,0 +1,221 @@
import { v4 as uuidv4 } from 'uuid';
import { Context } from '../../../support/commands';
import { ensureOrgExists } from '../../../support/api/orgs';
import { activateSMTPProvider, ensureSMTPProviderExists } from '../../../support/api/smtp';
import { ensureSMSProviderDoesntExist, ensureSMSProviderExists } from '../../../support/api/sms';
const notificationPath = `/instance?id=notifications`;
const smtpPath = `/instance?id=smtpprovider`;
const smsPath = `/instance?id=smsprovider`;
beforeEach(() => {
cy.context().as('ctx');
});
type SMTPProvider = {
description: string;
rowSelector: string;
};
describe('instance notifications', () => {
describe('notification settings', () => {
it(`should show notification settings`, () => {
cy.visit(notificationPath);
cy.contains('Notification');
});
});
describe('smtp settings', () => {
beforeEach(() => {
const description = `mailgun-${uuidv4()}`;
cy.wrap<SMTPProvider>({ description, rowSelector: `tr:contains('${description}')` }).as('provider');
});
it(`should add Mailgun SMTP provider settings`, () => {
cy.get<SMTPProvider>('@provider').then((provider) => {
cy.visit(smtpPath);
cy.get(`a:contains('Mailgun')`).click();
cy.get('[formcontrolname="description"]').should('be.enabled').clear().type(provider.description);
cy.get('[formcontrolname="hostAndPort"]').should('have.value', 'smtp.mailgun.org:587');
cy.get('[formcontrolname="user"]').should('be.enabled').clear().type('[email protected]');
cy.get('[formcontrolname="password"]').should('be.enabled').clear().type('password');
cy.get('[data-e2e="continue-to-2nd-form"]').should('be.enabled').click();
cy.get('[formcontrolname="senderAddress"]').should('be.enabled').clear().type('[email protected]');
cy.get('[formcontrolname="senderName"]').should('be.enabled').clear().type('Test1');
cy.get('[formcontrolname="replyToAddress"]').should('be.enabled').clear().type('[email protected]');
cy.get('[data-e2e="continue-button"]').should('be.enabled').click();
cy.get('[data-e2e="create-button"]').should('be.enabled').click();
cy.shouldConfirmSuccess();
cy.get('[data-e2e="close-button"]').should('be.enabled').click();
cy.get(provider.rowSelector).contains('smtp.mailgun.org:587');
cy.get(provider.rowSelector).contains('[email protected]');
});
});
it(`should add Mailgun SMTP provider settings and activate it using wizard`, () => {
cy.get<SMTPProvider>('@provider').then((provider) => {
cy.visit(smtpPath);
cy.get(`a:contains('Mailgun')`).click();
cy.get('[formcontrolname="description"]').should('be.enabled').clear().type(provider.description);
cy.get('[formcontrolname="hostAndPort"]').should('have.value', 'smtp.mailgun.org:587');
cy.get('[formcontrolname="user"]').should('be.enabled').clear().type('[email protected]');
cy.get('[formcontrolname="password"]').should('be.enabled').clear().type('password');
cy.get('[data-e2e="continue-to-2nd-form"]').should('be.enabled').click();
cy.get('[formcontrolname="senderAddress"]').should('be.enabled').clear().type('[email protected]');
cy.get('[formcontrolname="senderName"]').should('be.enabled').clear().type('Test1');
cy.get('[formcontrolname="replyToAddress"]').should('be.enabled').clear().type('[email protected]');
cy.get('[data-e2e="continue-button"]').should('be.enabled').click();
cy.get('[data-e2e="create-button"]').click();
cy.shouldConfirmSuccess();
cy.get('[data-e2e="activate-button"]').click();
cy.shouldConfirmSuccess();
cy.get('[data-e2e="close-button"]').click();
cy.get(provider.rowSelector).find('[data-e2e="active-provider"]');
cy.get(provider.rowSelector).contains('smtp.mailgun.org:587');
cy.get(provider.rowSelector).contains('[email protected]');
});
});
describe('with inactive existing', () => {
beforeEach(() => {
cy.get<Context>('@ctx').then((ctx) => {
cy.get<SMTPProvider>('@provider').then(({ description }) => {
ensureSMTPProviderExists(ctx.api, description);
});
});
cy.visit(smtpPath);
});
it(`should change Mailgun SMTP provider settings`, () => {
cy.get<SMTPProvider>('@provider').then(({ rowSelector }) => {
cy.get(rowSelector).click();
cy.get('[data-e2e="continue-to-2nd-form"]').click();
cy.get('[formcontrolname="senderAddress"]').should('be.enabled').clear().type('[email protected]');
cy.get('[formcontrolname="senderName"]').clear().type('Change1');
cy.get('[data-e2e="continue-button"]').click();
cy.get('[data-e2e="create-button"]').click();
cy.shouldConfirmSuccess();
cy.get('[data-e2e="close-button"]').click();
cy.get(rowSelector).contains('[email protected]');
});
});
it(`should activate Mailgun SMTP provider settings`, () => {
cy.get<SMTPProvider>('@provider').then(({ rowSelector }) => {
cy.get(rowSelector).find('[data-e2e="activate-provider-button"]').click({ force: true });
cy.get('[data-e2e="confirm-dialog-button"]').click();
cy.shouldConfirmSuccess();
cy.get(rowSelector).find('[data-e2e="active-provider"]');
});
});
it(`should delete Mailgun SMTP provider`, () => {
cy.get<SMTPProvider>('@provider').then(({ rowSelector }) => {
cy.get(rowSelector).find('[data-e2e="delete-provider-button"]').click({ force: true });
cy.get('[data-e2e="confirm-dialog-input"]').focus().should('be.enabled').type('A Sender');
cy.get('[data-e2e="confirm-dialog-button"]').click();
cy.shouldConfirmSuccess();
cy.get(rowSelector).should('not.exist');
});
});
});
describe('with active existing', () => {
beforeEach(() => {
cy.get<Context>('@ctx').then((ctx) => {
cy.get<SMTPProvider>('@provider').then(({ description }) => {
ensureSMTPProviderExists(ctx.api, description).then((providerId) => {
activateSMTPProvider(ctx.api, providerId);
});
});
});
cy.pause();
cy.visit(smtpPath);
});
it(`should deactivate an existing Mailgun SMTP provider using wizard`, () => {
cy.get<SMTPProvider>('@provider').then(({ rowSelector }) => {
cy.get(rowSelector).click();
cy.get('[data-e2e="continue-to-2nd-form"]').click();
cy.get('[data-e2e="continue-button"]').click();
cy.get('[data-e2e="create-button"]').click();
cy.shouldConfirmSuccess();
cy.get('[data-e2e="deactivate-button"]').click();
cy.shouldConfirmSuccess();
cy.get('[data-e2e="close-button"]').click();
cy.get(rowSelector).find('[data-e2e="active-provider"]').should('not.exist');
});
});
});
});
describe('sms settings', () => {
beforeEach(() => {
cy.wrap<string>(`twilio-${uuidv4()}`).as('uniqueSid');
});
describe('without existing', () => {
beforeEach(() => {
cy.get<Context>('@ctx').then((ctx) => {
ensureSMSProviderDoesntExist(ctx.api);
});
});
it(`should add SMS provider`, () => {
cy.visit(smsPath);
cy.get('[data-e2e="new-twilio-button"]').click();
cy.get('[formcontrolname="sid"]').should('be.enabled').clear().type('test');
cy.get('[formcontrolname="token"]').should('be.enabled').clear().type('token');
cy.get('[formcontrolname="senderNumber"]').should('be.enabled').clear().type('2312123132');
cy.get('[data-e2e="save-sms-settings-button"]').click();
cy.shouldConfirmSuccess();
cy.get('h4').contains('Twilio');
cy.get('.state').contains('Inactive');
});
});
describe('with inactive existing', () => {
beforeEach(() => {
cy.get<Context>('@ctx').then((ctx) => {
ensureSMSProviderExists(ctx.api);
cy.visit(smsPath);
});
});
it(`should activate SMS provider`, () => {
cy.get('h4').contains('Twilio');
cy.get('.state').contains('Inactive');
cy.get('[data-e2e="activate-sms-provider-button"]').click();
cy.shouldConfirmSuccess();
cy.get('.state').contains('Active');
});
it(`should edit SMS provider`, () => {
cy.get('h4').contains('Twilio');
cy.get('[data-e2e="new-twilio-button"]').click();
cy.get('[formcontrolname="sid"]').should('be.enabled').clear().type('test2');
cy.get('[formcontrolname="senderNumber"]').should('be.enabled').clear().type('6666666666');
cy.get('[data-e2e="save-sms-settings-button"]').click();
cy.shouldConfirmSuccess();
cy.get('[data-e2e="new-twilio-button"]').click();
cy.get('[formcontrolname="sid"]').should('have.value', 'test2');
cy.get('[formcontrolname="senderNumber"]').should('have.value', '6666666666');
});
it(`should edit SMS provider token`, () => {
cy.get('h4').contains('Twilio');
cy.get('[data-e2e="new-twilio-button"]').click();
cy.get('[data-e2e="edit-sms-token-button"]').click();
cy.get('[data-e2e="notification-setting-password"]').should('be.enabled').clear().type('newsupertoken');
cy.get('[data-e2e="save-notification-setting-password-button"]').click();
cy.shouldConfirmSuccess();
});
it(`should remove SMS provider`, () => {
cy.get('h4').contains('Twilio');
cy.get('[data-e2e="remove-sms-provider-button"]').click();
cy.get('[data-e2e="confirm-dialog-button"]').click();
cy.shouldConfirmSuccess();
});
});
});
});
@@ -0,0 +1,224 @@
const secretGeneratorSettingsPath = `/instance?id=secrets`;
beforeEach(() => {
cy.context().as('ctx');
});
describe('instance secret generators', () => {
describe('secret generator settings', () => {
it(`should show secret generator cards`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.contains('Initialization Mail');
cy.contains('Email verification');
cy.contains('Phone verification');
cy.contains('Password Reset');
cy.contains('Passwordless Initialization');
cy.contains('App Secret');
cy.contains('One Time Password (OTP) - SMS');
cy.contains('One Time Password (OTP) - Email');
});
it(`Initialization Mail should contain default settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.get('input[id="length1"]').should('have.value', '6');
cy.get('input[id="expiry1"]').should('have.value', '4320');
cy.get('mat-slide-toggle#includeLowerLetters1 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeUpperLetters1 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeDigits1 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeSymbols1 button').should('have.attr', 'aria-checked', 'false');
});
it(`Email verification should contain default settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.get('input[id="length2"]').should('have.value', '6');
cy.get('input[id="expiry2"]').should('have.value', '60');
cy.get('mat-slide-toggle#includeLowerLetters2 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeUpperLetters2 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeDigits2 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeSymbols2 button').should('have.attr', 'aria-checked', 'false');
});
it(`Phone verification should contain default settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.get('input[id="length3"]').should('have.value', '6');
cy.get('input[id="expiry3"]').should('have.value', '60');
cy.get('mat-slide-toggle#includeLowerLetters3 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeUpperLetters3 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeDigits3 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeSymbols3 button').should('have.attr', 'aria-checked', 'false');
});
it(`Password Reset should contain default settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.get('input[id="length4"]').should('have.value', '6');
cy.get('input[id="expiry4"]').should('have.value', '60');
cy.get('mat-slide-toggle#includeLowerLetters4 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeUpperLetters4 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeDigits4 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeSymbols4 button').should('have.attr', 'aria-checked', 'false');
});
it(`Passwordless Initialization should contain default settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.get('input[id="length5"]').should('have.value', '12');
cy.get('input[id="expiry5"]').should('have.value', '60');
cy.get('mat-slide-toggle#includeLowerLetters5 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeUpperLetters5 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeDigits5 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeSymbols5 button').should('have.attr', 'aria-checked', 'false');
});
it(`App Secret should contain default settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.get('input[id="length6"]').should('have.value', '64');
cy.get('input[id="expiry6"]').should('have.value', '0');
cy.get('mat-slide-toggle#includeLowerLetters6 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeUpperLetters6 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeDigits6 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeSymbols6 button').should('have.attr', 'aria-checked', 'false');
});
it(`One Time Password (OTP) - SMS should contain default settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.get('input[id="length7"]').should('have.value', '8');
cy.get('input[id="expiry7"]').should('have.value', '5');
cy.get('mat-slide-toggle#includeLowerLetters7 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeUpperLetters7 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeDigits7 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeSymbols7 button').should('have.attr', 'aria-checked', 'false');
});
it(`One Time Password (OTP) - Email should contain default settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.get('input[id="length8"]').should('have.value', '8');
cy.get('input[id="expiry8"]').should('have.value', '5');
cy.get('mat-slide-toggle#includeLowerLetters8 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeUpperLetters8 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeDigits8 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeSymbols8 button').should('have.attr', 'aria-checked', 'false');
});
it(`Initialization Mail should update settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.wait(1000);
cy.get('input[id="length1"]').should('be.enabled').clear().type('64');
cy.get('mat-slide-toggle#includeLowerLetters1 button').click();
cy.get('button[id="saveSecretGenerator1"]').click();
cy.wait(1000);
cy.get('input[id="length1"]').should('have.value', '64');
cy.get('input[id="expiry1"]').should('have.value', '4320');
cy.get('mat-slide-toggle#includeLowerLetters1 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeUpperLetters1 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeDigits1 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeSymbols1 button').should('have.attr', 'aria-checked', 'false');
});
it(`Email verification should update settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.wait(1000);
cy.get('input[id="length2"]').should('be.enabled').clear().type('64');
cy.get('mat-slide-toggle#includeUpperLetters2 button').click();
cy.get('button[id="saveSecretGenerator2"]').click();
cy.wait(1000);
cy.get('input[id="length2"]').should('have.value', '64');
cy.get('input[id="expiry2"]').should('have.value', '60');
cy.get('mat-slide-toggle#includeLowerLetters2 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeUpperLetters2 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeDigits2 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeSymbols2 button').should('have.attr', 'aria-checked', 'false');
});
it(`Phone verification should update settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.wait(1000);
cy.get('input[id="expiry3"]').should('be.enabled').clear().type('10');
cy.get('mat-slide-toggle#includeSymbols3 button').click();
cy.get('button[id="saveSecretGenerator3"]').click();
cy.wait(1000);
cy.get('input[id="length3"]').should('have.value', '6');
cy.get('input[id="expiry3"]').should('have.value', '10');
cy.get('mat-slide-toggle#includeLowerLetters3 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeUpperLetters3 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeDigits3 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeSymbols3 button').should('have.attr', 'aria-checked', 'true');
});
it(`Password Reset should update settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.wait(1000);
cy.get('input[id="expiry4"]').should('be.enabled').clear().type('5');
cy.get('mat-slide-toggle#includeDigits4 button').click();
cy.get('button[id="saveSecretGenerator4"]').click();
cy.wait(1000);
cy.get('input[id="length4"]').should('have.value', '6');
cy.get('input[id="expiry4"]').should('have.value', '5');
cy.get('mat-slide-toggle#includeLowerLetters4 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeUpperLetters4 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeDigits4 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeSymbols4 button').should('have.attr', 'aria-checked', 'false');
});
it(`Passwordless Initialization should update settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.wait(1000);
cy.get('input[id="length5"]').should('be.enabled').clear().type('64');
cy.get('mat-slide-toggle#includeDigits5 button').click();
cy.get('button[id="saveSecretGenerator5"]').click();
cy.wait(1000);
cy.get('input[id="length5"]').should('have.value', '64');
cy.get('input[id="expiry5"]').should('have.value', '60');
cy.get('mat-slide-toggle#includeLowerLetters5 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeUpperLetters5 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeDigits5 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeSymbols5 button').should('have.attr', 'aria-checked', 'false');
});
it(`App Secret should update settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.wait(1000);
cy.get('input[id="length6"]').should('be.enabled').clear().type('32');
cy.get('input[id="expiry6"]').should('be.enabled').clear().type('120');
cy.get('mat-slide-toggle#includeUpperLetters6 button').click();
cy.get('button[id="saveSecretGenerator6"]').click();
cy.wait(1000);
cy.get('input[id="length6"]').should('have.value', '32');
cy.get('input[id="expiry6"]').should('have.value', '120');
cy.get('mat-slide-toggle#includeLowerLetters6 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeUpperLetters6 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeDigits6 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeSymbols6 button').should('have.attr', 'aria-checked', 'false');
});
it(`One Time Password (OTP) - SMS should update settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.wait(1000);
cy.get('input[id="expiry7"]').should('be.enabled').clear().type('120');
cy.get('mat-slide-toggle#includeLowerLetters7 button').click();
cy.get('button[id="saveSecretGenerator7"]').click();
cy.wait(1000);
cy.get('input[id="length7"]').should('have.value', '8');
cy.get('input[id="expiry7"]').should('have.value', '120');
cy.get('mat-slide-toggle#includeLowerLetters7 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeUpperLetters7 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeDigits7 button').should('have.attr', 'aria-checked', 'true');
cy.get('mat-slide-toggle#includeSymbols7 button').should('have.attr', 'aria-checked', 'false');
});
it(`One Time Password (OTP) should update settings`, () => {
cy.visit(secretGeneratorSettingsPath);
cy.wait(1000);
cy.get('input[id="length8"]').should('be.enabled').clear().type('12');
cy.get('input[id="expiry8"]').should('be.enabled').clear().type('90');
cy.get('mat-slide-toggle#includeDigits8 button').click();
cy.get('mat-slide-toggle#includeSymbols8 button').click();
cy.get('button[id="saveSecretGenerator8"]').click();
cy.wait(1000);
cy.get('input[id="length8"]').should('have.value', '12');
cy.get('input[id="expiry8"]').should('have.value', '90');
cy.get('mat-slide-toggle#includeLowerLetters8 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeUpperLetters8 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeDigits8 button').should('have.attr', 'aria-checked', 'false');
cy.get('mat-slide-toggle#includeSymbols8 button').should('have.attr', 'aria-checked', 'true');
});
});
});
@@ -0,0 +1,73 @@
import { ensureMachineUserExists, ensureUserDoesntExist } from '../../support/api/users';
import { loginname } from '../../support/login/users';
import { ensureDomainPolicy } from '../../support/api/policies';
import { Context } from 'support/commands';
describe('machines', () => {
const machinesPath = `/users?type=machine`;
beforeEach(() => {
cy.context().as('ctx');
});
[
{ mustBeDomain: false, addName: 'e2emachineusernameaddGlobal', removeName: 'e2emachineusernameremoveGlobal' },
{ mustBeDomain: false, addName: '[email protected]', removeName: '[email protected]' },
// TODO:Changing the policy return 409 User already exists (SQL-M0dsf)
// { mustBeDomain: true, addName: 'e2emachineusernameadd', removeName: 'e2emachineusernameremove' },
].forEach((machine) => {
describe(`add "${machine.addName}" with domain setting "${machine.mustBeDomain}"`, () => {
beforeEach(`ensure it doesn't exist already`, () => {
cy.get<Context>('@ctx').then((ctx) => {
ensureUserDoesntExist(ctx.api, machine.addName);
ensureDomainPolicy(ctx.api, machine.mustBeDomain, false, false);
cy.visit(machinesPath);
});
});
it('should add a machine', () => {
cy.get('[data-e2e="create-user-button"]').should('be.visible').click();
cy.url().should('contain', 'users/create-machine');
//force needed due to the prefilled username prefix
cy.get('[formcontrolname="userName"]').should('be.enabled').type(machine.addName);
cy.get('[formcontrolname="name"]').should('be.enabled').type('e2emachinename');
cy.get('[formcontrolname="description"]').should('be.enabled').type('e2emachinedescription');
cy.get('[data-e2e="create-button"]').click();
cy.shouldConfirmSuccess();
let loginName = machine.addName;
if (machine.mustBeDomain) {
loginName = loginname(machine.addName, Cypress.env('ORGANIZATION'));
}
cy.contains('[data-e2e="copy-loginname"]', loginName).click();
cy.clipboardMatches(loginName);
});
});
describe(`remove "${machine.removeName}" with domain setting "${machine.mustBeDomain}"`, () => {
beforeEach('ensure it exists', () => {
cy.get<Context>('@ctx').then((ctx) => {
ensureMachineUserExists(ctx.api, machine.removeName);
cy.visit(machinesPath);
});
});
let loginName = machine.removeName;
if (machine.mustBeDomain) {
loginName = loginname(machine.removeName, Cypress.env('ORGANIZATION'));
}
it('should delete a machine', () => {
const rowSelector = `tr:contains('${machine.removeName}')`;
cy.get(rowSelector).should('be.visible').find('[data-e2e="enabled-delete-button"]').click({ force: true });
cy.get('[data-e2e="confirm-dialog-input"]').focus().should('be.enabled').type(loginName);
cy.get('[data-e2e="confirm-dialog-button"]').click();
cy.shouldConfirmSuccess();
cy.shouldNotExist({
selector: rowSelector,
timeout: { ms: 2000, errMessage: 'timed out before machine disappeared from the table' },
});
});
it('should create a personal access token');
});
});
});
@@ -0,0 +1,100 @@
import { ensureOrgExists, ensureOrgIsDefault, isDefaultOrg } from 'support/api/orgs';
import { v4 as uuidv4 } from 'uuid';
import { Context } from 'support/commands';
const orgPath = `/org`;
const orgsPath = `/orgs`;
const orgsPathCreate = `/orgs/create`;
const orgNameOnCreation = 'e2eorgrename';
const testOrgNameChange = uuidv4();
const newOrg = uuidv4();
beforeEach(() => {
cy.context().as('ctx');
});
describe('organizations', () => {
describe('add and delete org', () => {
it('should create an org', () => {
cy.visit(orgsPathCreate);
cy.get('[data-e2e="org-name-input"]').focus().clear().should('be.enabled').type(newOrg);
cy.get('[data-e2e="create-org-button"]').click();
cy.contains('tr', newOrg);
});
it('should delete an org', () => {
cy.visit(orgsPath);
cy.wait(2000);
cy.contains('tr', newOrg).click();
cy.wait(1000);
cy.get('[data-e2e="actions"]').click();
cy.get('[data-e2e="delete"]', { timeout: 1000 }).should('be.visible').click();
cy.get('[data-e2e="confirm-dialog-input"]').focus().clear().should('be.enabled').type(newOrg);
cy.get('[data-e2e="confirm-dialog-button"]').click();
cy.shouldConfirmSuccess();
cy.contains('tr', newOrg).should('not.exist');
});
});
describe('rename', () => {
beforeEach(() => {
cy.get<Context>('@ctx').then((ctx) => {
ensureOrgExists(ctx, orgNameOnCreation).then((newOrgId) => {
cy.visit(`${orgPath}?org=${newOrgId}`).as('orgsite');
});
});
});
it('should rename the organization', () => {
cy.get('[data-e2e="actions"]').click();
cy.get('[data-e2e="rename"]', { timeout: 1000 }).should('be.visible').click();
cy.get('[data-e2e="name"]').focus().clear().should('be.enabled').type(testOrgNameChange);
cy.get('[data-e2e="dialog-submit"]').click();
cy.shouldConfirmSuccess();
cy.visit(orgPath);
cy.get('[data-e2e="top-view-title"').should('contain', testOrgNameChange);
});
const orgOverviewPath = `/orgs`;
const initialDefaultOrg = 'e2eorgolddefault';
const orgNameForNewDefault = 'e2eorgnewdefault';
describe('set default org', () => {
beforeEach(() => {
cy.get<Context>('@ctx').then((ctx) => {
ensureOrgExists(ctx, orgNameForNewDefault)
.as('newDefaultOrgId')
.then(() => {
ensureOrgExists(ctx, initialDefaultOrg)
.as('defaultOrg')
.then((id) => {
ensureOrgIsDefault(ctx, id)
.as('orgWasDefault')
.then(() => {
cy.visit(`${orgOverviewPath}`).as('orgsite');
});
});
});
});
});
it('should rename the organization', () => {
cy.get<Context>('@ctx').then((ctx) => {
const rowSelector = `tr:contains(${orgNameForNewDefault})`;
cy.get(rowSelector).find('[data-e2e="table-actions-button"]').click({ force: true });
cy.get('[data-e2e="set-default-button"]', { timeout: 1000 }).should('be.visible').click();
cy.shouldConfirmSuccess();
cy.get<string>('@newDefaultOrgId').then((newDefaultOrgId) => {
isDefaultOrg(ctx, newDefaultOrgId);
});
});
});
});
it('should add an organization with the personal account as org owner');
describe('changing the current organization', () => {
it('should update displayed organization details');
});
});
});
@@ -0,0 +1,282 @@
import { ensureProjectGrantExists } from 'support/api/grants';
import {
ensureHumanIsOrgMember,
ensureHumanIsNotOrgMember,
ensureHumanIsNotProjectMember,
ensureHumanIsProjectMember,
} from 'support/api/members';
import { ensureOrgExists } from 'support/api/orgs';
import { ensureDomainPolicy } from 'support/api/policies';
import { ensureHumanUserExists, ensureUserDoesntExist } from 'support/api/users';
import { Context } from 'support/commands';
import { ensureProjectExists, ensureProjectResourceDoesntExist, Roles } from '../../support/api/projects';
describe('permissions', () => {
beforeEach(() => {
cy.context()
.as('ctx')
.then((ctx) => {
ensureDomainPolicy(ctx.api, false, true, false);
});
});
describe('management', () => {
const testManagerUsername = 'e2ehumanmanager';
function testAuthorizations(
roles: string[],
beforeCreate: (ctx: Context) => void,
beforeMutate: (ctx: Context) => void,
navigate: () => void,
) {
beforeEach(() => {
cy.get<Context>('@ctx').then((ctx) => {
ensureUserDoesntExist(ctx.api, testManagerUsername);
ensureHumanUserExists(ctx.api, testManagerUsername);
});
});
describe('create authorization', () => {
beforeEach(() => {
cy.get<Context>('@ctx').then((ctx) => {
beforeCreate(ctx);
navigate();
});
});
it('should add a manager', () => {
cy.get('[data-e2e="add-member-button"]').click();
cy.get('[data-e2e="add-member-input"]').should('be.enabled').type(testManagerUsername);
cy.get('[data-e2e="user-option"]').first().click();
cy.contains('[data-e2e="role-checkbox"]', roles[0]).click();
cy.get('[data-e2e="confirm-add-member-button"]').click();
cy.shouldConfirmSuccess();
cy.contains('[data-e2e="member-avatar"]', 'ee');
});
});
describe('mutate authorization', () => {
const rowSelector = `tr:contains(${testManagerUsername})`;
beforeEach(() => {
cy.get<Context>('@ctx').then((ctx) => {
beforeMutate(ctx);
navigate();
cy.contains('[data-e2e="member-avatar"]', 'ee').click();
cy.get(rowSelector).as('managerRow');
});
});
it('should remove a manager', () => {
cy.get('@managerRow').find('[data-e2e="remove-member-button"]').click({ force: true });
cy.get('[data-e2e="confirm-dialog-button"]').click();
cy.shouldConfirmSuccess();
cy.shouldNotExist({
selector: rowSelector,
timeout: { ms: 2000, errMessage: 'timed out before manager disappeared from the table' },
});
});
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.shouldConfirmSuccess();
cy.get('@managerRow')
.find('[data-e2e="remove-role-button"]')
.should('have.length', roles.length - 1);
});
});
}
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 (ctx: Context) {
ensureHumanIsNotOrgMember(ctx.api, testManagerUsername);
},
function (ctx: Context) {
ensureHumanIsNotOrgMember(ctx.api, testManagerUsername);
ensureHumanIsOrgMember(
ctx.api,
testManagerUsername,
roles.map((role) => role.internal),
);
},
() => {
cy.visit('/orgs');
cy.contains('tr', Cypress.env('ORGANIZATION')).click();
},
);
});
describe('projects', () => {
describe('owned projects', () => {
beforeEach(() => {
cy.get<Context>('@ctx').then((ctx) => {
ensureProjectExists(ctx.api, 'e2eprojectpermission').as('projectId');
});
});
const visitOwnedProject = () => {
cy.get<number>('@projectId').then((projectId) => {
cy.visit(`/projects/${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 (ctx) {
cy.get<string>('@projectId').then((projectId) => {
ensureHumanIsNotProjectMember(ctx.api, projectId, testManagerUsername);
});
},
function (ctx) {
cy.get<string>('@projectId').then((projectId) => {
ensureHumanIsNotProjectMember(ctx.api, projectId, testManagerUsername);
ensureHumanIsProjectMember(
ctx.api,
projectId,
testManagerUsername,
roles.map((role) => role.internal),
);
});
},
visitOwnedProject,
);
});
describe('roles', () => {
const testRoleName = 'e2eroleundertestname';
beforeEach(() => {
cy.get<Context>('@ctx').then((ctx) => {
cy.get<string>('@projectId').then((projectId) => {
ensureProjectResourceDoesntExist(ctx.api, projectId, Roles, testRoleName);
visitOwnedProject();
});
});
});
it('should add a role', () => {
cy.get('[data-e2e="sidenav-element-roles"]').click();
cy.get('[data-e2e="add-new-role"]').click();
cy.get('[formcontrolname="key"]').should('be.enabled').type(testRoleName);
cy.get('[formcontrolname="displayName"]').should('be.enabled').type('e2eroleundertestdisplay');
cy.get('[formcontrolname="group"]').should('be.enabled').type('e2eroleundertestgroup');
cy.get('[data-e2e="save-button"]').click();
cy.shouldConfirmSuccess();
cy.contains('tr', testRoleName);
});
it('should remove a role');
});
});
describe('granted projects', () => {
beforeEach(() => {
cy.get<Context>('@ctx').then((ctx) => {
ensureOrgExists(ctx, 'e2eforeignorg').then((foreignOrgId) => {
ensureProjectExists(ctx.api, 'e2eprojectgrants', foreignOrgId)
.as('foreignProjectId')
.then((foreignProjectId) => {
ensureProjectGrantExists(ctx, foreignOrgId, foreignProjectId).as('grantId');
});
});
});
});
function visitGrantedProject() {
cy.get<string>('@foreignProjectId').then((foreignProjectId) => {
cy.get<string>('@grantId').then((grantId) => {
cy.visit(`/granted-projects/${foreignProjectId}/grant/${grantId}`);
});
});
}
describe('authorizations', () => {
const roles = [
{ internal: 'PROJECT_GRANT_OWNER', display: 'Project Grant Owner' },
{ internal: 'PROJECT_GRANT_OWNER_VIEWER', display: 'Project Grant Owner Viewer' },
];
testAuthorizations(
roles.map((role) => role.display),
function (ctx: Context) {
cy.get<string>('@foreignProjectId').then((foreignProjectId) => {
cy.get<string>('@grantId').then((grantId) => {
ensureHumanIsNotProjectMember(ctx.api, foreignProjectId, testManagerUsername, grantId);
});
});
},
function (ctx: Context) {
cy.get<string>('@foreignProjectId').then((foreignProjectId) => {
cy.get<string>('@grantId').then((grantId) => {
ensureHumanIsNotProjectMember(ctx.api, foreignProjectId, testManagerUsername, grantId);
ensureHumanIsProjectMember(
ctx.api,
foreignProjectId,
testManagerUsername,
roles.map((role) => role.internal),
grantId,
);
});
});
},
visitGrantedProject,
);
});
});
});
});
});
describe('validations', () => {
describe('owned projects', () => {
describe('no ownership', () => {
it('a user without project global ownership can ...');
it('a user without project global ownership can not ...');
});
describe('project owner viewer global', () => {
it('a project owner viewer global additionally can ...');
it('a project owner viewer global still can not ...');
});
describe('project owner global', () => {
it('a project owner global additionally can ...');
it('a project owner global still can not ...');
});
});
describe('granted projects', () => {
describe('no ownership', () => {
it('a user without project grant ownership can ...');
it('a user without project grant ownership can not ...');
});
describe('project grant owner viewer', () => {
it('a project grant owner viewer additionally can ...');
it('a project grant owner viewer still can not ...');
});
describe('project grant owner', () => {
it('a project grant owner additionally can ...');
it('a project grant owner still can not ...');
});
});
describe('organization', () => {
describe('org owner', () => {
it('a project owner global can ...');
it('a project owner global can not ...');
});
});
});
@@ -0,0 +1,125 @@
import { Context } from 'support/commands';
import { ensureProjectDoesntExist, ensureProjectExists, ensureRoleExists } from '../../support/api/projects';
import { ensureOrgExists } from 'support/api/orgs';
import { ensureProjectGrantDoesntExist, ensureProjectGrantExists } from '../../support/api/grants';
describe('projects', () => {
beforeEach(() => {
cy.context().as('ctx');
});
const foreignOrg = 'e2eorgnewdefault';
const testProjectNameCreate = 'e2eprojectcreate';
const testProjectNameDelete = 'e2eprojectdelete';
const testProjectRole = 'e2eprojectrole';
describe('add project', () => {
beforeEach(`ensure it doesn't exist already`, () => {
cy.get<Context>('@ctx').then((ctx) => {
ensureProjectDoesntExist(ctx.api, testProjectNameCreate);
cy.visit(`/projects`);
});
});
it('should add a project', () => {
cy.get('.add-project-button').click({ force: true });
cy.get('input').should('be.enabled').type(testProjectNameCreate);
cy.get('[data-e2e="continue-button"]').click();
cy.shouldConfirmSuccess();
});
it('should configure a project to assert roles on authentication');
});
describe('create project grant', () => {
beforeEach('ensure it exists', () => {
cy.get<Context>('@ctx').then((ctx) => {
ensureProjectExists(ctx.api, testProjectNameCreate).as('projectId');
});
});
it('should add a role', () => {
const testRoleName = 'e2eroleundertestname';
cy.get<number>('@projectId').then((projectId) => {
cy.visit(`/projects/${projectId}`);
});
cy.get('[data-e2e="sidenav-element-roles"]').click();
cy.get('[data-e2e="add-new-role"]').click();
cy.get('[data-e2e="role-key-input"]').should('be.enabled').type(testRoleName);
cy.get('[formcontrolname="displayName"]').should('be.enabled').type('e2eroleundertestdisplay');
cy.get('[formcontrolname="group"]').should('be.enabled').type('e2eroleundertestgroup');
cy.get('[data-e2e="save-button"]').click();
cy.shouldConfirmSuccess();
cy.contains('tr', testRoleName);
});
describe('with existing role, without project grant', () => {
beforeEach(() => {
cy.get<Context>('@ctx').then((ctx) => {
cy.get<number>('@projectId').then((projectId) => {
ensureOrgExists(ctx, foreignOrg).then((foreignOrgID) => {
ensureRoleExists(ctx.api, projectId, testProjectRole);
ensureProjectGrantDoesntExist(ctx, projectId, foreignOrgID);
cy.visit(`/projects/${projectId}`);
});
});
});
});
it('should add a project grant', () => {
const rowSelector = `tr:contains(${testProjectRole})`;
cy.get('[data-e2e="sidenav-element-projectgrants"]').click();
cy.get('[data-e2e="create-project-grant-button"]').click();
cy.get('[data-e2e="add-org-input"]').should('be.enabled').type(foreignOrg);
cy.get('mat-option').contains(foreignOrg).click();
cy.get('button').should('be.enabled');
cy.get('[data-e2e="project-grant-continue"]').first().click();
cy.get(rowSelector).find('input').click({ force: true });
cy.get('[data-e2e="save-project-grant-button"]').click();
cy.contains('tr', foreignOrg);
cy.contains('tr', testProjectRole);
});
});
});
describe('edit project', () => {
beforeEach('ensure it exists', () => {
cy.get<Context>('@ctx').then((ctx) => {
ensureProjectExists(ctx.api, testProjectNameDelete).as('projectId');
cy.visit(`/projects`);
});
});
describe('remove project', () => {
it('removes the project from list view', () => {
const rowSelector = `tr:contains(${testProjectNameDelete})`;
cy.get('[data-e2e="toggle-grid"]').click();
cy.get('[data-e2e="timestamp"]');
cy.get(rowSelector).find('[data-e2e="delete-project-button"]').click({ force: true });
cy.get('[data-e2e="confirm-dialog-input"]').focus().should('be.enabled').type(testProjectNameDelete);
cy.get('[data-e2e="confirm-dialog-button"]').click();
cy.shouldConfirmSuccess();
cy.shouldNotExist({
selector: rowSelector,
timeout: { ms: 2000, errMessage: 'timed out before project disappeared from the table' },
});
});
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().should('be.enabled').type(testProjectNameDelete);
cy.get('[data-e2e="confirm-dialog-button"]').click();
cy.shouldConfirmSuccess();
cy.shouldNotExist({
selector: cardSelector,
timeout: { ms: 2000, errMessage: 'timed out before project disappeared from the grid' },
});
});
});
it('should add a project manager');
it('should remove a project manager');
});
});
@@ -0,0 +1,96 @@
import { ensureExternalLinksSettingsSet } from 'support/api/external-links-settings';
import { apiAuth } from '../../support/api/apiauth';
describe('external link settings', () => {
const tosLink = '';
const privacyPolicyLink = '';
const helpLink = '';
const supportEmail = '';
const customLink = '';
const customLinkText = '';
const docsLink = 'https://zitadel.com/docs';
beforeEach(`reset`, () => {
apiAuth().then((apiCallProperties) => {
ensureExternalLinksSettingsSet(apiCallProperties, tosLink, privacyPolicyLink, docsLink);
});
});
describe('instance', () => {
beforeEach(`visit`, () => {
cy.visit(`/instance?id=privacypolicy`);
});
it(`should have default settings`, () => {
cy.get('[formcontrolname="tosLink"]').should('value', tosLink);
cy.get('[formcontrolname="privacyLink"]').should('value', privacyPolicyLink);
cy.get('[formcontrolname="helpLink"]').should('value', helpLink);
cy.get('[formcontrolname="supportEmail"]').should('value', supportEmail);
cy.get('[formcontrolname="customLink"]').should('value', customLink);
cy.get('[formcontrolname="customLinkText"]').should('value', customLinkText);
cy.get('[formcontrolname="docsLink"]').should('value', docsLink);
});
it(`should update external links`, () => {
cy.get('[formcontrolname="tosLink"]').clear().type('tosLink2');
cy.get('[formcontrolname="privacyLink"]').clear().type('privacyLink2');
cy.get('[formcontrolname="helpLink"]').clear().type('helpLink');
cy.get('[formcontrolname="supportEmail"]').clear().type('[email protected]');
cy.get('[formcontrolname="customLink"]').clear().type('customLink');
cy.get('[formcontrolname="customLinkText"]').clear().type('customLinkText');
cy.get('[formcontrolname="docsLink"]').clear().type('docsLink');
cy.get('[data-e2e="save-button"]').click();
cy.shouldConfirmSuccess();
});
it(`should return to default values`, () => {
cy.get('[formcontrolname="tosLink"]').should('value', tosLink);
cy.get('[formcontrolname="privacyLink"]').should('value', privacyPolicyLink);
cy.get('[formcontrolname="helpLink"]').should('value', helpLink);
cy.get('[formcontrolname="supportEmail"]').should('value', supportEmail);
cy.get('[formcontrolname="customLink"]').should('value', customLink);
cy.get('[formcontrolname="customLinkText"]').should('value', customLinkText);
cy.get('[formcontrolname="docsLink"]').should('value', docsLink);
});
});
describe('org', () => {
beforeEach(`visit`, () => {
cy.visit(`/org-settings?id=privacypolicy`);
});
it(`should have default settings`, () => {
cy.get('[formcontrolname="tosLink"]').should('value', tosLink);
cy.get('[formcontrolname="privacyLink"]').should('value', privacyPolicyLink);
cy.get('[formcontrolname="helpLink"]').should('value', helpLink);
cy.get('[formcontrolname="supportEmail"]').should('value', supportEmail);
cy.get('[formcontrolname="customLink"]').should('value', customLink);
cy.get('[formcontrolname="customLinkText"]').should('value', customLinkText);
cy.get('[formcontrolname="docsLink"]').should('value', docsLink);
});
it(`should update external links`, () => {
cy.get('[formcontrolname="tosLink"]').clear().type('tosLink2');
cy.get('[formcontrolname="privacyLink"]').clear().type('privacyLink2');
cy.get('[formcontrolname="helpLink"]').clear().type('helpLink');
cy.get('[formcontrolname="supportEmail"]').clear().type('[email protected]');
cy.get('[formcontrolname="customLink"]').clear().type('customLink');
cy.get('[formcontrolname="customLinkText"]').clear().type('customLinkText');
cy.get('[formcontrolname="docsLink"]').clear().type('docsLink');
cy.get('[data-e2e="save-button"]').click();
cy.shouldConfirmSuccess();
});
it(`should return to default values`, () => {
cy.get('[data-e2e="reset-button"]').click();
cy.get('[data-e2e="confirm-dialog-button"]').click();
cy.get('[formcontrolname="tosLink"]').should('value', tosLink);
cy.get('[formcontrolname="privacyLink"]').should('value', privacyPolicyLink);
cy.get('[formcontrolname="helpLink"]').should('value', helpLink);
cy.get('[formcontrolname="supportEmail"]').should('value', supportEmail);
cy.get('[formcontrolname="customLink"]').should('value', customLink);
cy.get('[formcontrolname="customLinkText"]').should('value', customLinkText);
cy.get('[formcontrolname="docsLink"]').should('value', docsLink);
});
});
});
@@ -0,0 +1,170 @@
import { apiAuth } from '../../support/api/apiauth';
import { resetInstanceFeatures } from '../../support/api/features';
import { login, User } from '../../support/login/users';
describe('features settings', () => {
const featuresPath = '/instance?id=features';
beforeEach(() => {
cy.context().as('ctx');
cy.visit(featuresPath);
});
describe('UI and Display Tests', () => {
it('should display features page with correct elements', () => {
// Check page title contains relevant text (flexible for translation issues)
cy.get('h2').should('be.visible').and('contain.text', 'Feature');
cy.get('.events-desc').should('be.visible');
// Check info link
cy.get('a[href*="feature-service"]').should('be.visible').and('have.attr', 'target', '_blank');
// Check reset button is always present
cy.get('[data-e2e="reset-features-button"]').should('be.visible');
});
it('should display feature toggles', () => {
cy.get('cnsl-card').should('be.visible');
cy.get('.features').should('be.visible');
// Check that feature toggles are present
cy.get('cnsl-feature-toggle').should('have.length.greaterThan', 0);
cy.get('cnsl-login-v2-feature-toggle').should('be.visible');
});
it('should maintain feature states after page reload', () => {
// Wait for features to load
cy.get('cnsl-feature-toggle').should('be.visible');
// Get the count of feature toggles before reload
cy.get('cnsl-feature-toggle').then(($toggles) => {
const initialCount = $toggles.length;
// Reload page
cy.reload();
// Wait for features to load again
cy.get('cnsl-feature-toggle').should('be.visible');
// Verify that the same number of toggles are still present
cy.get('cnsl-feature-toggle').should('have.length', initialCount);
// Verify that toggles are still functional
cy.get('cnsl-feature-toggle')
.first()
.within(() => {
cy.get('mat-button-toggle').should('be.visible');
});
});
});
it('should handle API errors gracefully', () => {
// Intercept API calls and force them to fail
cy.intercept('POST', '**/features*', {
statusCode: 500,
body: { message: 'Internal server error' },
}).as('featureError');
// Try to toggle a feature
cy.get('cnsl-feature-toggle')
.first()
.within(() => {
cy.get('mat-button-toggle').first().click();
});
// Wait for the error response (optional since it might not always trigger)
cy.get('body').then(() => {
// Just verify that the page is still functional
cy.get('cnsl-feature-toggle').should('be.visible');
});
});
describe('permissions', () => {
it('should show appropriate elements for admin users', () => {
// Admin should see reset button
cy.get('[data-e2e="reset-features-button"]').should('be.visible');
// Admin should see all feature toggles
cy.get('cnsl-feature-toggle').should('have.length.greaterThan', 0);
});
});
});
describe('Feature Modification Tests', () => {
afterEach(() => {
// Reset features after each feature modification test to ensure clean state
apiAuth().then((api) => {
resetInstanceFeatures(api);
});
});
it('should be able to toggle a feature', () => {
// Wait for features to load
cy.get('cnsl-feature-toggle').should('be.visible');
// Get the first feature toggle and check its initial state
cy.get('cnsl-feature-toggle')
.first()
.within(() => {
// Ensure we always trigger a state change by clicking an unchecked button
cy.get('mat-button-toggle').then(($allButtons) => {
const uncheckedButtons = $allButtons.not('.mat-button-toggle-checked');
if (uncheckedButtons.length > 0) {
// Click an unchecked button to enable it
const targetButton = uncheckedButtons.first();
cy.wrap(targetButton).click();
// Verify the toggle reflected the new state
cy.wrap(targetButton).should('have.class', 'mat-button-toggle-checked');
} else {
// All buttons are checked, click the first one to uncheck it
const targetButton = $allButtons.first();
cy.wrap(targetButton).click();
// Verify the toggle reflected the new state (should be unchecked now)
cy.wrap(targetButton).should('not.have.class', 'mat-button-toggle-checked');
}
});
});
// Check for success toast since we made a real change
cy.shouldConfirmSuccess();
});
it('should reset features when reset button is clicked', () => {
// Change a feature first to have something to reset
cy.get('cnsl-feature-toggle')
.first()
.within(() => {
// Check current state and click the opposite to ensure we trigger a change
cy.get('mat-button-toggle').then(($buttons) => {
const checkedButton = $buttons.filter('.mat-button-toggle-checked');
const uncheckedButtons = $buttons.not('.mat-button-toggle-checked');
if (uncheckedButtons.length > 0) {
// Click an unchecked button to enable it
cy.wrap(uncheckedButtons.first()).click();
// Verify the change was applied
cy.wrap(uncheckedButtons.first()).should('have.class', 'mat-button-toggle-checked');
} else {
// All buttons are checked, click the first one to uncheck it
cy.wrap(checkedButton.first()).click();
// Verify the change was applied
cy.wrap(checkedButton.first()).should('not.have.class', 'mat-button-toggle-checked');
}
});
});
// Check for success toast since we made a real change
cy.shouldConfirmSuccess();
// Click the reset button
cy.get('[data-e2e="reset-features-button"]').click();
// Check for success toast from reset operation
cy.shouldConfirmSuccess();
// Verify features are still loaded and functional after UI reset
cy.get('cnsl-feature-toggle').should('be.visible');
});
});
});
@@ -0,0 +1,38 @@
import { apiAuth } from '../../support/api/apiauth';
import { ensureHumanUserExists } from '../../support/api/users';
import { login, User } from '../../support/login/users';
describe('login policy', () => {
const orgPath = `/org`;
[User.OrgOwner].forEach((user) => {
describe(`as user "${user}"`, () => {
beforeEach(() => {
login(user);
cy.visit(orgPath);
// TODO: Why force?
cy.contains('[data-e2e="policy-card"]', 'Login Policy').contains('button', 'Modify').click({ force: true }); // TODO: select data-e2e
apiAuth().then((api) => {
ensureHumanUserExists(api, User.LoginPolicyUser);
});
});
// TODO: verify email
it.skip(`username and password disallowed`, () => {
login(User.LoginPolicyUser, '123abcABC?&*');
});
it(`registering is allowed`);
it(`registering is disallowed`);
it(`login by an external IDP is allowed`);
it(`login by an external IDP is disallowed`);
it(`MFA is forced`);
it(`MFA is not forced`);
it(`the password reset option is hidden`);
it(`the password reset option is shown`);
it(`passwordless login is allowed`);
it(`passwordless login is disallowed`);
describe('identity providers', () => {});
});
});
});
@@ -0,0 +1,48 @@
import { apiAuth } from '../../support/api/apiauth';
import { ensureOIDCSettingsSet } from '../../support/api/oidc-settings';
describe('oidc settings', () => {
const oidcSettingsPath = `/instance?id=oidc`;
const accessTokenPrecondition = 1;
const idTokenPrecondition = 2;
const refreshTokenExpirationPrecondition = 7;
const refreshTokenIdleExpirationPrecondition = 2;
beforeEach(`ensure they are set`, () => {
apiAuth().then((apiCallProperties) => {
ensureOIDCSettingsSet(
apiCallProperties,
accessTokenPrecondition,
idTokenPrecondition,
refreshTokenExpirationPrecondition,
refreshTokenIdleExpirationPrecondition,
);
cy.visit(oidcSettingsPath);
});
});
it(`should update oidc settings`, () => {
cy.get('[formcontrolname="accessTokenLifetime"]')
.should('value', accessTokenPrecondition)
.clear()
.should('be.enabled')
.type('2');
cy.get('[formcontrolname="idTokenLifetime"]')
.should('value', idTokenPrecondition)
.clear()
.should('be.enabled')
.type('24');
cy.get('[formcontrolname="refreshTokenExpiration"]')
.should('value', refreshTokenExpirationPrecondition)
.clear()
.should('be.enabled')
.type('30');
cy.get('[formcontrolname="refreshTokenIdleExpiration"]')
.should('value', refreshTokenIdleExpirationPrecondition)
.clear()
.should('be.enabled')
.type('7');
cy.get('[data-e2e="save-button"]').click();
cy.shouldConfirmSuccess();
});
});
@@ -0,0 +1,29 @@
import { login, User } from '../../support/login/users';
describe('password complexity', () => {
const orgPath = `/org`;
const testProjectName = 'e2eproject';
[User.OrgOwner].forEach((user) => {
describe(`as user "${user}"`, () => {
beforeEach(() => {
login(user);
cy.visit(orgPath);
// TODO: Why force?
cy.contains('[data-e2e="policy-card"]', 'Password Complexity').contains('button', 'Modify').click({ force: true }); // TODO: select data-e2e
});
// TODO: fix saving password complexity policy bug
it(`should restrict passwords that don't have the minimal length`);
it(`should require passwords to contain a number if option is switched on`);
it(`should not require passwords to contain a number if option is switched off`);
it(`should require passwords to contain a symbol if option is switched on`);
it(`should not require passwords to contain a symbol if option is switched off`);
it(`should require passwords to contain a lowercase letter if option is switched on`);
it(`should not require passwords to contain a lowercase letter if option is switched off`);
it(`should require passwords to contain an uppercase letter if option is switched on`);
it(`should not require passwords to contain an uppercase letter if option is switched off`);
});
});
});
@@ -0,0 +1,71 @@
import { Context } from 'mocha';
import { apiAuth } from '../../support/api/apiauth';
import { Policy, resetPolicy } from '../../support/api/policies';
import { login, User } from '../../support/login/users';
describe('private labeling', () => {
const orgPath = `/org`;
[User.OrgOwner].forEach((user) => {
describe(`as user "${user}"`, () => {
beforeEach(() => {
login(user);
cy.visit(orgPath);
// TODO: Why force?
cy.contains('[data-e2e="policy-card"]', 'Private Labeling').contains('button', 'Modify').click({ force: true }); // TODO: select data-e2e
});
customize('white', user);
customize('dark', user);
});
});
});
function customize(theme: string, user: User) {
describe(`${theme} theme`, () => {
beforeEach(() => {
apiAuth().then((api) => {
resetPolicy(api, Policy.Label);
});
});
describe.skip('logo', () => {
beforeEach('expand logo category', () => {
cy.contains('[data-e2e="policy-category"]', 'Logo').click(); // TODO: select data-e2e
cy.fixture('logo.png').as('logo');
});
it('should update a logo', () => {
cy.get<Context>('@ctx').then((ctx) => {
cy.get('[data-e2e="image-part-logo"]')
.find('input')
.then(function (el) {
const blob = Cypress.Blob.base64StringToBlob(ctx.logo, 'image/png');
const file = new File([blob], 'images/logo.png', { type: 'image/png' });
const list = new DataTransfer();
list.items.add(file);
const myFileList = list.files;
el[0].files = myFileList;
el[0].dispatchEvent(new Event('change', { bubbles: true }));
});
});
});
it('should delete a logo');
});
it('should update an icon');
it('should delete an icon');
it('should update the background color');
it('should update the primary color');
it('should update the warning color');
it('should update the font color');
it('should update the font style');
it('should hide the loginname suffix');
it('should show the loginname suffix');
it('should hide the watermark');
it('should show the watermark');
it('should show the current configuration');
it('should reset the policy');
});
}
@@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "[email protected]",
"body": "Fixtures are a great way to mock data for responses to routes"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

@@ -0,0 +1,39 @@
import { login, User } from 'support/login/users';
import { API, SystemAPI, Token } from './types';
const authHeaderKey = 'Authorization',
orgIdHeaderKey = 'x-zitadel-orgid',
backendUrl = Cypress.env('BACKEND_URL');
export function apiAuth(): Cypress.Chainable<API> {
return login(User.IAMAdminUser, 'Password1!', false, true).then((token) => {
return <API>{
token: token,
mgmtBaseURL: `${backendUrl}/management/v1`,
adminBaseURL: `${backendUrl}/admin/v1`,
authBaseURL: `${backendUrl}/auth/v1`,
assetsBaseURL: `${backendUrl}/assets/v1`,
oauthBaseURL: `${backendUrl}/oauth/v2`,
oidcBaseURL: `${backendUrl}/oidc/v1`,
samlBaseURL: `${backendUrl}/saml/v2`,
featuresBaseURL: `${backendUrl}/v2/features`,
};
});
}
export function systemAuth(): Cypress.Chainable<SystemAPI> {
return cy.task('systemToken').then((token) => {
return <SystemAPI>{
token: token,
baseURL: `${backendUrl}/system/v1`,
};
});
}
export function requestHeaders(token: Token, orgId?: string): object {
const headers = { [authHeaderKey]: `Bearer ${token.token}` };
if (orgId) {
headers[orgIdHeaderKey] = orgId;
}
return headers;
}
@@ -0,0 +1,124 @@
import { requestHeaders } from './apiauth';
import { findFromList as mapFromList, searchSomething } from './search';
import { API, Entity, SearchResult, Token } from './types';
export function ensureItemExists(
token: Token,
searchPath: string,
findInList: (entity: Entity) => boolean,
createPath: string,
body: Entity,
orgId?: string,
newItemIdField: string = 'id',
searchItemIdField?: string,
) {
return ensureSomething(
token,
() => searchSomething(token, searchPath, 'POST', mapFromList(findInList, searchItemIdField), orgId),
() => createPath,
'POST',
body,
(entity) => !!entity,
(body) => body[newItemIdField],
orgId,
);
}
export function ensureItemDoesntExist(
token: Token,
searchPath: string,
findInList: (entity: Entity) => boolean,
deletePath: (entity: Entity) => string,
orgId?: string,
): Cypress.Chainable<null> {
return ensureSomething(
token,
() => searchSomething(token, searchPath, 'POST', mapFromList(findInList), orgId),
deletePath,
'DELETE',
null,
(entity) => !entity,
).then(() => null);
}
export function ensureSetting(
api: API,
path: string,
mapResult: (entity: any) => SearchResult,
createPath: string,
body: any,
orgId?: string,
): Cypress.Chainable<string> {
return ensureSomething(
api,
() => searchSomething(api, path, 'GET', mapResult, orgId),
() => createPath,
'PUT',
body,
(entity) => !!entity,
);
}
function awaitDesired(
trials: number,
expectEntity: (entity: Entity) => boolean,
search: () => Cypress.Chainable<SearchResult>,
initialSequence?: number,
) {
return search().then((resp) => {
const foundExpectedEntity = expectEntity(resp.entity);
const foundExpectedSequence = !initialSequence || resp.sequence >= initialSequence;
const check = !foundExpectedEntity || !foundExpectedSequence;
if (check) {
expect(trials, `trying ${trials} more times`).to.be.greaterThan(0);
cy.wait(1000);
return awaitDesired(trials - 1, expectEntity, search, initialSequence);
}
});
}
interface EnsuredResult {
id: string;
sequence: number;
}
export function ensureSomething(
token: Token,
search: () => Cypress.Chainable<SearchResult>,
apiPath: (entity: Entity) => string,
ensureMethod: string,
body: Entity,
expectEntity: (entity: Entity) => boolean,
mapId?: (body: any) => string,
orgId?: string,
): Cypress.Chainable<string> {
return search()
.then((sRes) => {
if (expectEntity(sRes.entity)) {
return cy.wrap(<EnsuredResult>{ id: sRes.id, sequence: sRes.sequence });
}
return cy
.request({
method: ensureMethod,
url: apiPath(sRes.entity),
headers: requestHeaders(token, orgId),
body: body,
failOnStatusCode: false,
followRedirect: false,
})
.then((cRes) => {
expect(cRes.status).to.equal(200);
return <EnsuredResult>{
id: mapId ? mapId(cRes.body) : undefined,
sequence: sRes.sequence,
};
});
})
.then((data) => {
return awaitDesired(90, expectEntity, search, data.sequence).then(() => {
return cy.wrap(data.id);
});
});
}
@@ -0,0 +1,31 @@
import { ensureSetting } from './ensure';
import { API } from './types';
export function ensureExternalLinksSettingsSet(api: API, tosLink: string, privacyPolicyLink: string, docsLink: string) {
return ensureSetting(
api,
`${api.adminBaseURL}/policies/privacy`,
(body: any) => {
const result = {
sequence: body.policy?.details?.sequence,
id: body.policy.id,
entity: null,
};
if (
body.policy &&
(body.policy.tosLink || '') === tosLink &&
(body.policy.privacyLink || '') === privacyPolicyLink &&
(body.policy.docsLink || '') === docsLink
) {
return { ...result, entity: body.policy };
}
return result;
},
`${api.adminBaseURL}/policies/privacy`,
{
tosLink,
privacyLink: privacyPolicyLink,
docsLink,
},
);
}
@@ -0,0 +1,59 @@
import { API } from './types';
export function getInstanceFeatures(api: API) {
return cy.request({
method: 'GET',
url: `${api.featuresBaseURL}/instance`,
headers: {
authorization: `Bearer ${api.token}`,
},
body: {},
});
}
export function setInstanceFeature(api: API, feature: string, enabled: boolean, additionalConfig?: Record<string, any>) {
const body: Record<string, any> = {
[feature]: enabled,
...additionalConfig,
};
return cy.request({
method: 'PUT',
url: `${api.featuresBaseURL}/instance`,
headers: {
authorization: `Bearer ${api.token}`,
},
body,
});
}
export function resetInstanceFeatures(api: API) {
return cy.request({
method: 'DELETE',
url: `${api.featuresBaseURL}/instance`,
headers: {
authorization: `Bearer ${api.token}`,
},
});
}
export function ensureFeatureState(api: API, feature: string, enabled: boolean, additionalConfig?: Record<string, any>) {
return getInstanceFeatures(api).then((response) => {
const currentState = response.body?.[feature]?.enabled;
if (currentState !== enabled) {
return setInstanceFeature(api, feature, enabled, additionalConfig);
}
return cy.wrap(response);
});
}
export function ensureLoginV2FeatureState(api: API, required: boolean, baseUri?: string) {
return setInstanceFeature(api, 'loginV2', required, {
loginV2: {
required,
baseUri: baseUri || '',
},
});
}
@@ -0,0 +1,32 @@
import { Context } from 'support/commands';
import { ensureItemDoesntExist, ensureItemExists } from './ensure';
import { getOrgUnderTest } from './orgs';
import { API, Entity } from './types';
export function ensureProjectGrantExists(ctx: Context, foreignOrgId: string, foreignProjectId: string) {
return getOrgUnderTest(ctx).then((orgUnderTest) => {
return ensureItemExists(
ctx.api,
`${ctx.api.mgmtBaseURL}/projectgrants/_search`,
(grant: any) => grant.grantedOrgId == orgUnderTest && grant.projectId == foreignProjectId,
`${ctx.api.mgmtBaseURL}/projects/${foreignProjectId}/grants`,
{ granted_org_id: orgUnderTest },
foreignOrgId,
'grantId',
'grantId',
);
});
}
export function ensureProjectGrantDoesntExist(ctx: Context, projectId: number, foreignOrgId: string) {
return getOrgUnderTest(ctx).then((orgUnderTest) => {
console.log('removing grant to foreignOrgId', foreignOrgId, 'in orgUnderTest', orgUnderTest, 'projectId', projectId);
return ensureItemDoesntExist(
ctx.api,
`${ctx.api.mgmtBaseURL}/projectgrants/_search`,
(grant: any) => grant.grantedOrgId == foreignOrgId && grant.projectId == projectId,
(grant: any) => `${ctx.api.mgmtBaseURL}/projects/${projectId}/grants/${grant.grantId}`,
orgUnderTest.toString(),
);
});
}
@@ -0,0 +1,31 @@
import { SystemAPI } from './types';
export function instanceUnderTest(api: SystemAPI): Cypress.Chainable<string> {
return cy
.request({
method: 'POST',
url: `${api.baseURL}/instances/_search`,
auth: {
bearer: api.token,
},
})
.then((res) => {
const instances = <Array<any>>res.body.result;
expect(instances.length).to.equal(
1,
'instanceUnderTest just supports running against an API with exactly one instance, yet',
);
return instances[0].id;
});
}
export function getInstance(api: SystemAPI, instanceId: string, failOnStatusCode = true) {
return cy.request({
method: 'GET',
url: `${api.baseURL}/instances/${instanceId}`,
auth: {
bearer: api.token,
},
failOnStatusCode: failOnStatusCode,
});
}
@@ -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) {
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[]) {
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?: string,
): Cypress.Chainable<string> {
return ensureItemDoesntExist(
api,
`${api.mgmtBaseURL}/projects/${projectId}/${grantId ? `grants/${grantId}/` : ''}members/_search`,
(member: any) => (<string>member.preferredLoginName).startsWith(username),
(member) => `${api.mgmtBaseURL}/projects/${projectId}/${grantId ? `grants/${grantId}/` : ''}members/${member.userId}`,
);
}
export function ensureHumanIsProjectMember(
api: API,
projectId: string,
username: string,
roles: string[],
grantId?: string,
): Cypress.Chainable<string> {
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,
},
);
});
}
@@ -0,0 +1,47 @@
import { ensureSetting } from './ensure';
import { API } from './types';
export function ensureOIDCSettingsSet(
api: API,
accessTokenLifetime: number,
idTokenLifetime: number,
refreshTokenExpiration: number,
refreshTokenIdleExpiration: number,
) {
return ensureSetting(
api,
`${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`,
{
accessTokenLifetime: hoursToDuration(accessTokenLifetime),
idTokenLifetime: hoursToDuration(idTokenLifetime),
refreshTokenExpiration: daysToDuration(refreshTokenExpiration),
refreshTokenIdleExpiration: daysToDuration(refreshTokenIdleExpiration),
},
);
}
function hoursToDuration(hours: number): string {
return (hours * 3600).toString() + 's';
}
function daysToDuration(days: number): string {
return hoursToDuration(24 * days);
}
@@ -0,0 +1,76 @@
import { ensureSomething } from './ensure';
import { searchSomething } from './search';
import { API } from './types';
import { host } from '../login/users';
import { requestHeaders } from './apiauth';
import { Context } from 'support/commands';
export function ensureOrgExists(ctx: Context, name: string) {
return ensureSomething(
ctx.api,
() =>
searchSomething(
ctx.api,
encodeURI(`${ctx.api.mgmtBaseURL}/global/orgs/_by_domain?domain=${name}.${host(Cypress.config('baseUrl'))}`),
'GET',
(res) => {
return { entity: res.org, id: res.org?.id, sequence: parseInt(<string>res.org?.details?.sequence) };
},
),
() => `${ctx.api.mgmtBaseURL}/orgs`,
'POST',
{ name: name },
(org) => org?.name === name,
(res) => res.id,
);
}
export function isDefaultOrg(ctx: Context, orgId: string): Cypress.Chainable<boolean> {
return cy
.request({
method: 'GET',
url: encodeURI(`${ctx.api.mgmtBaseURL}/iam`),
headers: requestHeaders(ctx.api, orgId),
})
.then((res) => {
const { defaultOrgId } = res.body;
expect(defaultOrgId).to.equal(orgId);
return defaultOrgId === orgId;
});
}
export function ensureOrgIsDefault(ctx: Context, orgId: string): Cypress.Chainable<boolean> {
return cy
.request({
method: 'GET',
url: encodeURI(`${ctx.api.mgmtBaseURL}/iam`),
headers: requestHeaders(ctx.api, orgId),
})
.then((res) => {
return res.body;
})
.then(({ defaultOrgId }) => {
if (defaultOrgId === orgId) {
return true;
} else {
return cy
.request({
method: 'PUT',
url: `${ctx.api.adminBaseURL}/orgs/default/${orgId}`,
headers: requestHeaders(ctx.api, orgId),
failOnStatusCode: true,
followRedirect: false,
})
.then((cRes) => {
expect(cRes.status).to.equal(200);
return !!cRes.body;
});
}
});
}
export function getOrgUnderTest(ctx: Context): Cypress.Chainable<number> {
return searchSomething(ctx.api, `${ctx.api.mgmtBaseURL}/orgs/me`, 'GET', (res) => {
return { entity: res.org, id: res.org.id, sequence: parseInt(<string>res.org.details.sequence) };
}).then((res) => res.entity.id);
}
@@ -0,0 +1,53 @@
import { requestHeaders } from './apiauth';
import { API } from './types';
import { ensureSetting } from './ensure';
export enum Policy {
Label = 'label',
}
export function resetPolicy(api: API, policy: Policy) {
cy.request({
method: 'DELETE',
url: `${api.mgmtBaseURL}/policies/${policy}`,
headers: requestHeaders(api),
}).then((res) => {
expect(res.status).to.equal(200);
return null;
});
}
export function ensureDomainPolicy(
api: API,
userLoginMustBeDomain: boolean,
validateOrgDomains: boolean,
smtpSenderAddressMatchesInstanceDomain: boolean,
) {
return ensureSetting(
api,
`${api.adminBaseURL}/policies/domain`,
(body: any) => {
const result = {
sequence: parseInt(<string>body.policy?.details?.sequence),
id: body.policy?.details?.resourceOwner,
entity: null,
};
if (
body.policy &&
(body.policy.userLoginMustBeDomain ? body.policy.userLoginMustBeDomain : false) == userLoginMustBeDomain &&
(body.policy.validateOrgDomains ? body.policy.validateOrgDomains : false) == validateOrgDomains &&
(body.policy.smtpSenderAddressMatchesInstanceDomain ? body.policy.smtpSenderAddressMatchesInstanceDomain : false) ==
smtpSenderAddressMatchesInstanceDomain
) {
return { ...result, entity: body.policy };
}
return result;
},
`${api.adminBaseURL}/policies/domain`,
{
userLoginMustBeDomain: userLoginMustBeDomain,
validateOrgDomains: validateOrgDomains,
smtpSenderAddressMatchesInstanceDomain: smtpSenderAddressMatchesInstanceDomain,
},
);
}
@@ -0,0 +1,66 @@
import { ensureItemDoesntExist, ensureItemExists } from './ensure';
import { API, Entity } from './types';
export function ensureProjectExists(api: API, projectName: string, orgId?: string) {
return ensureItemExists(
api,
`${api.mgmtBaseURL}/projects/_search`,
(project: any) => project.name === projectName,
`${api.mgmtBaseURL}/projects`,
{ name: projectName },
orgId,
);
}
export function ensureProjectDoesntExist(api: API, projectName: string, orgId?: string) {
return ensureItemDoesntExist(
api,
`${api.mgmtBaseURL}/projects/_search`,
(project: any) => project.name === projectName,
(project) => `${api.mgmtBaseURL}/projects/${project.id}`,
orgId,
);
}
class ResourceType {
constructor(
public resourcePath: string,
public compareProperty: string,
public identifierProperty: string,
) {}
}
export const Apps = new ResourceType('apps', 'name', 'id');
export const Roles = new ResourceType('roles', 'key', 'key');
//export const Grants = new ResourceType('apps', 'name')
export function ensureProjectResourceDoesntExist(
api: API,
projectId: string,
resourceType: ResourceType,
resourceName: string,
orgId?: string,
): Cypress.Chainable<null> {
return ensureItemDoesntExist(
api,
`${api.mgmtBaseURL}/projects/${projectId}/${resourceType.resourcePath}/_search`,
(resource: Entity) => resource[resourceType.compareProperty] === resourceName,
(resource: Entity) =>
`${api.mgmtBaseURL}/projects/${projectId}/${resourceType.resourcePath}/${resource[resourceType.identifierProperty]}`,
orgId,
);
}
export function ensureRoleExists(api: API, projectId: number, roleName: string) {
return ensureItemExists(
api,
`${api.mgmtBaseURL}/projects/${projectId}/${Roles.resourcePath}/_search`,
(resource: any) => resource.key === roleName,
`${api.mgmtBaseURL}/projects/${projectId}/${Roles.resourcePath}`,
{
name: roleName,
roleKey: roleName,
displayName: roleName,
},
);
}
@@ -0,0 +1,84 @@
import { Context } from 'support/commands';
export enum Unit {
Unimplemented,
AuthenticatedRequests,
ExecutionSeconds,
}
interface notification {
percent: number;
repeat?: boolean;
callUrl: string;
}
export function addQuota(
ctx: Context,
unit: Unit = Unit.AuthenticatedRequests,
limit: boolean,
amount: number,
notifications?: Array<notification>,
from: Date = (() => {
const date = new Date();
date.setMonth(0, 1);
date.setMinutes(0, 0, 0);
// default to start of current year
return date;
})(),
intervalSeconds: string = `${315_576_000_000}s`, // proto max duration is 1000 years
failOnStatusCode = true,
): Cypress.Chainable<Cypress.Response<any>> {
return cy.request({
method: 'POST',
url: `${ctx.system.baseURL}/instances/${ctx.instanceId}/quotas`,
auth: {
bearer: ctx.system.token,
},
body: {
unit: unit,
amount: amount,
resetInterval: intervalSeconds,
limit: limit,
from: from,
notifications: notifications,
},
failOnStatusCode: failOnStatusCode,
});
}
export function ensureQuotaIsAdded(
ctx: Context,
unit: Unit,
limit: boolean,
amount?: number,
notifications?: Array<notification>,
from?: Date,
intervalSeconds?: string,
): Cypress.Chainable<null> {
return addQuota(ctx, unit, limit, amount, notifications, from, intervalSeconds, false).then((res) => {
if (!res.isOkStatusCode) {
expect(res.status).to.equal(409);
}
return null;
});
}
export function removeQuota(ctx: Context, unit: Unit, failOnStatusCode = true): Cypress.Chainable<Cypress.Response<any>> {
return cy.request({
method: 'DELETE',
url: `${ctx.system.baseURL}/instances/${ctx.instanceId}/quotas/${unit}`,
auth: {
bearer: ctx.system.token,
},
failOnStatusCode: failOnStatusCode,
});
}
export function ensureQuotaIsRemoved(ctx: Context, unit?: Unit): Cypress.Chainable<null> {
return removeQuota(ctx, unit, false).then((res) => {
if (!res.isOkStatusCode) {
expect(res.status).to.equal(404);
}
return null;
});
}
@@ -0,0 +1,32 @@
import { requestHeaders } from './apiauth';
import { API, Entity, SearchResult, Token } from './types';
export function searchSomething(
token: Token,
searchPath: string,
method: string,
mapResult: (body: any) => SearchResult,
orgId?: string,
): Cypress.Chainable<SearchResult> {
return cy
.request({
method: method,
url: searchPath,
headers: requestHeaders(token, 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],
};
};
}
@@ -0,0 +1,28 @@
import { ensureItemDoesntExist, ensureItemExists } from './ensure';
import { API, Entity } from './types';
import { ensureSMTPProviderExists } from './smtp';
export function ensureSMSProviderExists(api: API) {
// remove and create
ensureSMSProviderDoesntExist(api);
return ensureItemExists(
api,
`${api.adminBaseURL}/sms/_search`,
({ twilio: { sid: foundSid } }: any) => foundSid === 'initial-sid',
`${api.adminBaseURL}/sms/twilio`,
{
sid: 'initial-sid',
senderNumber: 'initial-senderNumber',
token: 'initial-token',
},
);
}
export function ensureSMSProviderDoesntExist(api: API) {
return ensureItemDoesntExist(
api,
`${api.adminBaseURL}/sms/_search`,
(provider: any) => !!provider,
(provider) => `${api.adminBaseURL}/sms/${provider.id}`,
);
}
@@ -0,0 +1,32 @@
import { ensureItemDoesntExist, ensureItemExists } from './ensure';
import { API, Entity } from './types';
export function ensureSMTPProviderExists(api: API, providerDescription: string) {
return ensureItemExists(
api,
`${api.adminBaseURL}/smtp/_search`,
(provider: any) => {
return provider.description === providerDescription;
},
`${api.adminBaseURL}/smtp`,
{
name: providerDescription,
description: providerDescription,
senderAddress: '[email protected]',
senderName: 'A Sender',
host: 'smtp.host.com:587',
user: 'smtpuser',
},
);
}
export function activateSMTPProvider(api: API, providerId: string) {
return cy.request({
method: 'POST',
url: `${api.adminBaseURL}/smtp/${providerId}/_activate`,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${api.token}`,
},
});
}
@@ -0,0 +1,27 @@
export interface Token {
token: string;
}
export interface API extends Token {
mgmtBaseURL: string;
adminBaseURL: string;
authBaseURL: string;
assetsBaseURL: string;
oidcBaseURL: string;
oauthBaseURL: string;
samlBaseURL: string;
featuresBaseURL: string;
}
export interface SystemAPI extends Token {
baseURL: string;
}
export type SearchResult = {
entity: Entity | null;
sequence: number;
id: string;
};
// Entity is an object but not a function
export type Entity = { [k: string]: any } & ({ bind?: never } | { call?: never });
@@ -0,0 +1,71 @@
import { requestHeaders } from './apiauth';
import { ensureItemDoesntExist, ensureItemExists } from './ensure';
import { API } from './types';
export function ensureHumanUserExists(api: API, username: string) {
return ensureItemExists(
api,
`${api.mgmtBaseURL}/users/_search`,
(user: any) => user.userName === username,
`${api.mgmtBaseURL}/users/human`,
{
...defaultHuman,
user_name: username,
},
undefined,
'userId',
);
}
export function ensureMachineUserExists(api: API, username: string) {
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) {
return ensureItemDoesntExist(
api,
`${api.mgmtBaseURL}/users/_search`,
(user: any) => user.userName === username,
(user) => `${api.mgmtBaseURL}/users/${user.id}`,
);
}
export function createHumanUser(api: API, username: string, failOnStatusCode = true) {
return cy.request({
method: 'POST',
url: `${api.mgmtBaseURL}/users/human`,
body: {
...defaultHuman,
user_name: username,
},
auth: {
bearer: api.token,
},
failOnStatusCode: failOnStatusCode,
});
}
const defaultHuman = {
profile: {
first_name: 'e2efirstName',
last_name: 'e2elastName',
},
email: {
email: '[email protected]',
},
phone: {
phone: '+41 123456789',
},
};
@@ -0,0 +1,131 @@
import 'cypress-wait-until';
import { apiAuth, systemAuth } from './api/apiauth';
import { API, SystemAPI } from './api/types';
import { ensureQuotaIsRemoved, Unit } from './api/quota';
import { instanceUnderTest } from './api/instances';
interface ShouldNotExistOptions {
selector: string;
timeout?: {
errMessage: string;
ms: number;
};
}
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>;
/**
* Custom command that waits until the selector finds zero elements.
*/
shouldNotExist(options?: ShouldNotExistOptions): Cypress.Chainable<null>;
/**
* Custom command that ensures a reliable testing context and returns it
*/
context(): Cypress.Chainable<Context>;
/**
* Custom command that asserts success is printed after a change.
*/
shouldConfirmSuccess(): Cypress.Chainable<null>;
}
}
}
export interface Context {
api: API;
system: SystemAPI;
instanceId: number;
}
Cypress.Commands.add('clipboardMatches', { prevSubject: false }, (pattern: RegExp | string) => {
/* doesn't work reliably
return cy.window()
.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) => {
if (!options.timeout) {
const elements = Cypress.$(options.selector);
expect(elements.text()).to.be.empty;
expect(elements.length).to.equal(0);
return null;
}
return cy
.waitUntil(
() => {
const elements = Cypress.$(options.selector);
if (!elements.length) {
return cy.wrap(true);
}
return cy.log(`elements with selector ${options.selector} and text ${elements.text()} exist`).wrap(false);
},
{
timeout: options.timeout.ms,
errorMsg: options.timeout.errMessage,
},
)
.then(() => null);
});
Cypress.Commands.add('shouldConfirmSuccess', { prevSubject: false }, () => {
cy.get('.data-e2e-message');
cy.shouldNotExist({ selector: '.data-e2e-failure' });
cy.get('.data-e2e-success');
});
Cypress.Commands.add('context', { prevSubject: false }, () => {
return systemAuth().then((system) => {
return instanceUnderTest(system).then((instanceId) => {
return ensureQuotaIsRemoved(
{
system: system,
api: null,
instanceId: instanceId,
},
Unit.AuthenticatedRequests,
).then(() => {
return ensureQuotaIsRemoved(
{
system: system,
api: null,
instanceId: instanceId,
},
Unit.ExecutionSeconds,
).then(() => {
return apiAuth().then((api) => {
return {
system: system,
api: api,
instanceId: instanceId,
};
});
});
});
});
});
});
@@ -0,0 +1,18 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands';
import './types';
@@ -0,0 +1,31 @@
export function authenticate(
loginUrl: string,
creds: {
password: any;
username: string;
},
onUsernameScreen: () => void,
onPasswordScreen: () => void,
onAuthenticated: () => void,
) {
onUsernameScreen ? onUsernameScreen() : null;
cy.get('#loginName').type(creds.username);
cy.get('#submit-button').click();
onPasswordScreen ? onPasswordScreen() : null;
cy.get('#password').type(creds.password);
cy.get('#submit-button').click();
cy.wait('@password').then((interception) => {
if (interception.response.body.indexOf(`${loginUrl}/mfa/prompt`) === -1) {
return;
}
cy.contains('button', 'skip').click();
});
cy.wait('@token');
onAuthenticated ? onAuthenticated() : null;
cy.wait(1000);
}
@@ -0,0 +1,147 @@
import { authenticate as authenticateOnBaseUrl } from './authenticate';
export enum User {
OrgOwner = 'org_owner',
OrgOwnerViewer = 'org_owner_viewer',
OrgProjectCreator = 'org_project_creator',
LoginPolicyUser = 'login_policy_user',
PasswordComplexityUser = 'password_complexity_user',
IAMAdminUser = 'zitadel-admin',
}
export function login(
user: User,
pw?: string,
force?: boolean,
skipMFAChangePW?: boolean,
onUsernameScreen?: () => void,
onPasswordScreen?: () => void,
onAuthenticated?: () => void,
): Cypress.Chainable<string> {
let creds = credentials(user, pw);
const loginUrl: string = '/ui/login';
const issuerUrl: string = '/oauth/v2';
return cy
.session(
creds.username,
() => {
const cookies = new Map<string, string>();
cy.intercept(
{
times: 6,
},
(req) => {
req.headers['cookie'] = requestCookies(cookies);
req.continue((res) => {
updateCookies(res.headers['set-cookie'] as string[], cookies);
});
},
);
cy.intercept({
method: 'POST',
url: `${issuerUrl}/token`,
}).as('token');
cy.intercept({
method: 'POST',
url: `${loginUrl}/password*`,
times: 1,
}).as('password');
cy.visit(Cypress.config('baseUrl'), { retryOnNetworkFailure: true });
const backendUrl = Cypress.env('BACKEND_URL');
if (Cypress.config('baseUrl').startsWith(backendUrl)) {
authenticateOnBaseUrl(loginUrl, creds, onUsernameScreen, onPasswordScreen, onAuthenticated);
cy.get('@token')
.its('response.body.access_token')
.then((token) => {
cy.task('safetoken', { key: creds.username, token: token });
});
} else {
cy.origin(
backendUrl,
{ args: { loginUrl, creds, onUsernameScreen, onPasswordScreen, onAuthenticated } },
({ loginUrl, creds, onUsernameScreen, onPasswordScreen, onAuthenticated }) => {
const authenticateOnBackendUrl = Cypress.require('./authenticate');
authenticateOnBackendUrl.authenticate(loginUrl, creds, onUsernameScreen, onPasswordScreen, onAuthenticated);
},
);
cy.get('@token')
.its('response.body.access_token')
.then((token) => {
cy.task('safetoken', { key: creds.username, token: token });
});
}
cy.visit('/');
cy.get('[data-e2e=authenticated-welcome]', {
timeout: 50_000,
});
},
{
validate: () => {
if (force) {
throw new Error('clear session');
}
},
},
)
.then(() => {
return cy.task('loadtoken', { key: creds.username });
});
}
export function loginname(withoutDomain: string, org?: string): string {
return `${withoutDomain}@${org}.${host(Cypress.config('baseUrl'))}`;
}
function credentials(user: User, pw?: string) {
// TODO: ugly
const woDomain = user == User.IAMAdminUser ? User.IAMAdminUser : `${user}_user_name`;
const org = Cypress.env('ORGANIZATION') ? Cypress.env('ORGANIZATION') : 'zitadel';
return {
username: loginname(woDomain, org),
password: pw ? pw : Cypress.env(`${user}_password`),
};
}
function updateCookies(newCookies: string[] | undefined, currentCookies: Map<string, string>) {
if (newCookies === undefined) {
return;
}
newCookies.forEach((cs) => {
cs.split('; ').forEach((cookie) => {
const idx = cookie.indexOf('=');
currentCookies.set(cookie.substring(0, idx), cookie.substring(idx + 1));
});
});
}
function requestCookies(currentCookies: Map<string, string>): string[] {
let list: Array<string> = [];
currentCookies.forEach((val, key) => {
list.push(key + '=' + val);
});
return list;
}
export function host(url: string): string {
return stripPort(stripProtocol(url));
}
function stripPort(s: string): string {
const idx = s.indexOf(':');
return idx === -1 ? s : s.substring(0, idx);
}
function stripProtocol(url: string): string {
return url.replace('http://', '').replace('https://', '');
}
@@ -0,0 +1,13 @@
let webhookEventSchema = {
sentStatus: 0,
payload: {
unit: 0,
id: '',
callURL: '',
periodStart: new Date(),
threshold: 0,
usage: 0,
},
};
export type ZITADELWebhookEvent = typeof webhookEventSchema;
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"baseUrl": "./",
"target": "es5",
"lib": ["es5", "dom", "es2015"],
"types": ["cypress", "node"]
},
"include": ["**/*.ts"]
}