ci(e2e): Run Tests in Pipelines (#3903)

* cy10 changes

* test: setup local e2e env

* test(e2e): migrate e2e setup

* add more config

* make e2e setup work

* align variables

* fix config

* skip mfa

* set user register to false

* read ids from database if not provided

* don't read ids withing env file

* fix escaping in id queries

* fix project root

* export projectRoot path

* export projectRoot

* add e2e-setup.sh

* specify GOOS and GOARCH for dockerfile compatible binary

* add org default redirect uri

* correctly initialize org policy

* await ids

* fix awaiting ids

* fix cypress configuration

* fix some tests

* initial compose setup

* fix working directory

* fix references

* make tests less flaky

* run go tests

* compose works until e2e-setup incl

* pass created e2e sa key

* make cypress run

* derive e2e orgs domain from baseurl

* use host from baseurl for setup ctx

* move defaults.yaml back to cmd pkg

* just create org owner

* Don't render element if no roles are passed

* use map instead of switchMap

* fix e2e tests

* added testdata for e3e

* zipped dump

* removed dumpDir

* cypress workflow with compose

* quote name

* cleanup vars

* eliminate need for e2e setup

* compose has no builds anymore

* use compose run and zitadel nw

* test e2e on pr (#4114)

* test e2e on pr

* install goreleaser

* install npm dev dependencies

* run cypress wf

* dynamic release version

* skip flaky user tests

* skip flaky permissions test

* cache docker layers in pipeline

* Update .github/workflows/cypress.yml

Co-authored-by: Florian Forster <florian@caos.ch>

* align goreleaser version

* get rid of install.sh

* remove cypress-terminal-report

* Revert "remove cypress-terminal-report"

This reverts commit 254b5a1f87.

* just one npm e2e:build command

* cache npm dependencies

* install node modules using docker

* dedicated e2e context

* fix syntax

* don't copy node modules from goreleaser

* add npm-copy target

* add tsconfig.json

* remove docker caching

* deleted unneeded shellscript

* naming and cleanup

Co-authored-by: Florian Forster <florian@caos.ch>
Co-authored-by: Christian Jakob <christian@caos.ch>

* cleanup

Co-authored-by: Elio Bischof <eliobischof@gmail.com>
Co-authored-by: Christian Jakob <christian@caos.ch>
Co-authored-by: Florian Forster <florian@caos.ch>
This commit is contained in:
Max Peintner
2022-08-05 20:00:46 +02:00
committed by GitHub
parent ce85397050
commit fc99ec87c5
55 changed files with 5539 additions and 3621 deletions

View File

@@ -1,14 +0,0 @@
{
"supportFile": "./cypress/support/index.ts",
"reporter": "mochawesome",
"reporterOptions": {
"reportDir": "cypress/results",
"overwrite": false,
"html": true,
"json": true
},
"chromeWebSecurity": false,
"experimentalSessionSupport": true,
"trashAssetsBeforeRuns": false
}

View File

@@ -1,21 +0,0 @@
#!/usr/bin/env bash
ACTION=$1
ENVFILE=$2
shift
shift
projectRoot=".."
set -a; source $ENVFILE; set +a
NPX=""
if ! command -v cypress &> /dev/null; then
NPX="npx"
fi
$NPX cypress $ACTION \
--port ${E2E_CYPRESS_PORT} \
--env org="${E2E_ORG}",org_owner_password="${E2E_ORG_OWNER_PW}",org_owner_viewer_password="${E2E_ORG_OWNER_VIEWER_PW}",org_project_creator_password="${E2E_ORG_PROJECT_CREATOR_PW}",login_policy_user_password="${E2E_LOGIN_POLICY_USER_PW}",password_complexity_user_password="${E2E_PASSWORD_COMPLEXITY_USER_PW}",consoleUrl=${E2E_CONSOLE_URL},apiUrl="${E2E_API_URL}",accountsUrl="${E2E_ACCOUNTS_URL}",issuerUrl="${E2E_ISSUER_URL}",serviceAccountKey="${E2E_SERVICEACCOUNT_KEY}",serviceAccountKeyPath="${E2E_SERVICEACCOUNT_KEY_PATH}",otherZitadelIdpInstance="${E2E_OTHER_ZITADEL_IDP_INSTANCE}",zitadelProjectResourceId="${E2E_ZITADEL_PROJECT_RESOURCE_ID}" \
"$@"

View File

@@ -1,4 +0,0 @@
results
videos
screenshots
downloads

View File

@@ -1,5 +0,0 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -1,50 +0,0 @@
import { login, User } from "../../support/login/users";
import { Apps, ensureProjectExists, ensureProjectResourceDoesntExist } from "../../support/api/projects";
import { apiAuth } from "../../support/api/apiauth";
describe('applications', () => {
const testProjectName = 'e2eprojectapplication'
const testAppName = 'e2eappundertest'
;[User.OrgOwner].forEach(user => {
describe(`as user "${user}"`, () => {
beforeEach(`ensure it doesn't exist already`, () => {
login(user)
apiAuth().then(api => {
ensureProjectExists(api, testProjectName).then(projectID => {
ensureProjectResourceDoesntExist(api, projectID, Apps, testAppName).then(() => {
cy.visit(`${Cypress.env('consoleUrl')}/projects/${projectID}`)
})
})
})
})
it('add app', () => {
cy.get('mat-spinner')
cy.get('mat-spinner').should('not.exist')
cy.get('[data-e2e="app-card-add"]').should('be.visible').click()
// select webapp
cy.get('[formcontrolname="name"]').type(testAppName)
cy.get('[for="WEB"]').click()
cy.get('[data-e2e="continue-button-nameandtype"]').click()
//select authentication
cy.get('[for="PKCE"]').click()
cy.get('[data-e2e="continue-button-authmethod"]').click()
//enter URL
cy.get('cnsl-redirect-uris').eq(0).type("https://testurl.org")
cy.get('cnsl-redirect-uris').eq(1).type("https://testlogouturl.org")
cy.get('[data-e2e="continue-button-redirecturis"]').click()
cy.get('[data-e2e="create-button"]').click().then(() => {
cy.get('[id*=overlay]').should('exist')
})
cy.get('.data-e2e-success')
cy.wait(200)
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist')
//TODO: check client ID/Secret
})
})
})
})

View File

@@ -1,83 +0,0 @@
import { apiAuth } from '../../support/api/apiauth';
import { ensureHumanUserExists, ensureUserDoesntExist } from '../../support/api/users';
import { login, User, username } from '../../support/login/users';
describe('humans', () => {
const humansPath = `${Cypress.env('consoleUrl')}/users?type=human`;
const testHumanUserNameAdd = 'e2ehumanusernameadd';
const testHumanUserNameRemove = 'e2ehumanusernameremove';
[User.OrgOwner].forEach((user) => {
describe(`as user "${user}"`, () => {
beforeEach(() => {
login(user);
cy.visit(humansPath);
cy.get('[data-cy=timestamp]');
});
describe('add', () => {
before(`ensure it doesn't exist already`, () => {
apiAuth().then((apiCallProperties) => {
ensureUserDoesntExist(apiCallProperties, testHumanUserNameAdd);
});
});
it('should add a user', () => {
cy.get('a[href="/users/create"]').click();
cy.url().should('contain', 'users/create');
cy.get('[formcontrolname="email"]').type(username('e2ehuman'));
//force needed due to the prefilled username prefix
cy.get('[formcontrolname="userName"]').type(testHumanUserNameAdd, { force: true });
cy.get('[formcontrolname="firstName"]').type('e2ehumanfirstname');
cy.get('[formcontrolname="lastName"]').type('e2ehumanlastname');
cy.get('[formcontrolname="phone"]').type('+41 123456789');
cy.get('[data-e2e="create-button"]').click();
cy.get('.data-e2e-success');
cy.wait(200);
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist');
});
});
describe('remove', () => {
before('ensure it exists', () => {
apiAuth().then((api) => {
ensureHumanUserExists(api, testHumanUserNameRemove);
});
});
it('should delete a human user', () => {
cy.contains('tr', testHumanUserNameRemove, { timeout: 1000 })
.find('button')
//force due to angular hidden buttons
.click({ force: true });
cy.get('[e2e-data="confirm-dialog-input"]').type(username(testHumanUserNameRemove, Cypress.env('org')));
cy.get('[e2e-data="confirm-dialog-button"]').click();
cy.get('.data-e2e-success');
cy.wait(200);
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist');
});
});
});
});
});
/*
describe("users", ()=> {
before(()=> {
cy.consolelogin(Cypress.env('username'), Cypress.env('password'), Cypress.env('consoleUrl'))
})
it('should show personal information', () => {
cy.log(`USER: show personal information`);
//click on user information
cy.get('a[href*="users/me"').eq(0).click()
cy.url().should('contain', '/users/me')
})
it('should show users', () => {
cy.visit(Cypress.env('consoleUrl') + '/users/list/humans')
cy.url().should('contain', 'users/list/humans')
})
})
*/

View File

@@ -1,60 +0,0 @@
import { apiAuth } from '../../support/api/apiauth';
import { ensureMachineUserExists, ensureUserDoesntExist } from '../../support/api/users';
import { login, User, username } from '../../support/login/users';
describe('machines', () => {
const machinesPath = `${Cypress.env('consoleUrl')}/users?type=machine`;
const testMachineUserNameAdd = 'e2emachineusernameadd';
const testMachineUserNameRemove = 'e2emachineusernameremove';
[User.OrgOwner].forEach((user) => {
describe(`as user "${user}"`, () => {
beforeEach(() => {
login(user);
cy.visit(machinesPath);
cy.get('[data-cy=timestamp]');
});
describe('add', () => {
before(`ensure it doesn't exist already`, () => {
apiAuth().then((apiCallProperties) => {
ensureUserDoesntExist(apiCallProperties, testMachineUserNameAdd);
});
});
it('should add a machine', () => {
cy.get('a[href="/users/create-machine"]').click();
cy.url().should('contain', 'users/create-machine');
//force needed due to the prefilled username prefix
cy.get('[formcontrolname="userName"]').type(testMachineUserNameAdd, { force: true });
cy.get('[formcontrolname="name"]').type('e2emachinename');
cy.get('[formcontrolname="description"]').type('e2emachinedescription');
cy.get('[data-e2e="create-button"]').click();
cy.get('.data-e2e-success');
cy.wait(200);
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist');
});
});
describe('remove', () => {
before('ensure it exists', () => {
apiAuth().then((api) => {
ensureMachineUserExists(api, testMachineUserNameRemove);
});
});
it('should delete a machine', () => {
cy.contains('tr', testMachineUserNameRemove, { timeout: 1000 })
.find('button')
//force due to angular hidden buttons
.click({ force: true });
cy.get('[e2e-data="confirm-dialog-input"]').type(username(testMachineUserNameRemove, Cypress.env('org')));
cy.get('[e2e-data="confirm-dialog-button"]').click();
cy.get('.data-e2e-success');
cy.wait(200);
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist');
});
});
});
});
});

View File

@@ -1,120 +0,0 @@
import { apiAuth, apiCallProperties } from "../../support/api/apiauth";
import { ensureProjectExists, ensureProjectResourceDoesntExist, Roles } from "../../support/api/projects";
import { login, User } from "../../support/login/users";
describe('permissions', () => {
const testProjectName = 'e2eprojectpermission'
const testAppName = 'e2eapppermission'
const testRoleName = 'e2eroleundertestname'
const testRoleDisplay = 'e2eroleundertestdisplay'
const testRoleGroup = 'e2eroleundertestgroup'
const testGrantName = 'e2egrantundertest'
;[User.OrgOwner].forEach(user => {
describe(`as user "${user}"`, () => {
var api: apiCallProperties
var projectId: number
beforeEach(() => {
login(user)
apiAuth().then(apiCalls => {
api = apiCalls
ensureProjectExists(apiCalls, testProjectName).then(projId => {
projectId = projId
cy.visit(`${Cypress.env('consoleUrl')}/projects/${projId}`)
})
})
})
describe('add role', () => {
beforeEach(()=> {
ensureProjectResourceDoesntExist(api, projectId, Roles, testRoleName)
})
it('should add a role', () => {
cy.get('[data-e2e="add-new-role"]').click()
cy.get('[formcontrolname="key"]').type(testRoleName)
cy.get('[formcontrolname="displayName"]').type(testRoleDisplay)
cy.get('[formcontrolname="group"]').type(testRoleGroup)
cy.get('[data-e2e="save-button"]').click()
cy.get('.data-e2e-success')
cy.wait(200)
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist')
})
})
})
})
})
/*
describe('permissions', () => {
before(()=> {
// cy.consolelogin(Cypress.env('username'), Cypress.env('password'), Cypress.env('consoleUrl'))
})
it('should show projects ', () => {
cy.visit(Cypress.env('consoleUrl') + '/projects')
cy.url().should('contain', '/projects')
})
it('should add a role', () => {
cy.visit(Cypress.env('consoleUrl') + '/org').then(() => {
cy.url().should('contain', '/org');
})
cy.visit(Cypress.env('consoleUrl') + '/projects').then(() => {
cy.url().should('contain', '/projects');
cy.get('.card').should('contain.text', "newProjectToTest")
})
cy.get('.card').filter(':contains("newProjectToTest")').click()
cy.get('.app-container').filter(':contains("newAppToTest")').should('be.visible').click()
let projectID
cy.url().then(url => {
cy.log(url.split('/')[4])
projectID = url.split('/')[4]
});
cy.then(() => cy.visit(Cypress.env('consoleUrl') + '/projects/' + projectID +'/roles/create'))
cy.get('[formcontrolname^=key]').type("newdemorole")
cy.get('[formcontrolname^=displayName]').type("newdemodisplayname")
cy.get('[formcontrolname^=group]').type("newdemogroupname")
cy.get('button').filter(':contains("Save")').should('be.visible').click()
//let the Role get processed
cy.wait(5000)
})
it('should add a grant', () => {
cy.visit(Cypress.env('consoleUrl') + '/org').then(() => {
cy.url().should('contain', '/org');
})
cy.visit(Cypress.env('consoleUrl') + '/projects').then(() => {
cy.url().should('contain', '/projects');
cy.get('.card').should('contain.text', "newProjectToTest")
})
cy.get('.card').filter(':contains("newProjectToTest")').click()
cy.get('.app-container').filter(':contains("newAppToTest")').should('be.visible').click()
let projectID
cy.url().then(url => {
cy.log(url.split('/')[4])
projectID = url.split('/')[4]
});
cy.then(() => cy.visit(Cypress.env('consoleUrl') + '/grant-create/project/' + projectID ))
cy.get('input').type("demo")
cy.get('[role^=listbox]').filter(`:contains("${Cypress.env("fullUserName")}")`).should('be.visible').click()
cy.wait(5000)
//cy.get('.button').contains('Continue').click()
cy.get('button').filter(':contains("Continue")').click()
cy.wait(5000)
cy.get('tr').filter(':contains("demo")').find('label').click()
cy.get('button').filter(':contains("Save")').should('be.visible').click()
//let the grant get processed
cy.wait(5000)
})
})
*/

View File

@@ -1,78 +0,0 @@
import { apiAuth } from '../../support/api/apiauth';
import { ensureProjectDoesntExist, ensureProjectExists } from '../../support/api/projects';
import { login, User } from '../../support/login/users';
describe('projects', () => {
const testProjectNameCreate = 'e2eprojectcreate';
const testProjectNameDeleteList = 'e2eprojectdeletelist';
const testProjectNameDeleteGrid = 'e2eprojectdeletegrid';
[User.OrgOwner].forEach((user) => {
describe(`as user "${user}"`, () => {
beforeEach(() => {
login(user);
});
describe('add project', () => {
beforeEach(`ensure it doesn't exist already`, () => {
apiAuth().then((api) => {
ensureProjectDoesntExist(api, testProjectNameCreate);
});
cy.visit(`${Cypress.env('consoleUrl')}/projects`);
});
it('should add a project', () => {
cy.get('.add-project-button').click({ force: true });
cy.get('input').type(testProjectNameCreate);
cy.get('[data-e2e="continue-button"]').click();
cy.get('.data-e2e-success');
cy.wait(200);
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist');
});
});
describe('remove project', () => {
describe('list view', () => {
beforeEach('ensure it exists', () => {
apiAuth().then((api) => {
ensureProjectExists(api, testProjectNameDeleteList);
});
cy.visit(`${Cypress.env('consoleUrl')}/projects`);
});
it('removes the project', () => {
cy.get('[data-e2e=toggle-grid]').click();
cy.get('[data-cy=timestamp]');
cy.contains('tr', testProjectNameDeleteList, { timeout: 1000 })
.find('[data-e2e=delete-project-button]')
.click({ force: true });
cy.get('[e2e-data="confirm-dialog-button"]').click();
cy.get('.data-e2e-success');
cy.wait(200);
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist');
});
});
describe('grid view', () => {
beforeEach('ensure it exists', () => {
apiAuth().then((api) => {
ensureProjectExists(api, testProjectNameDeleteGrid);
});
cy.visit(`${Cypress.env('consoleUrl')}/projects`);
});
it('removes the project', () => {
cy.contains('[data-e2e=grid-card]', testProjectNameDeleteGrid)
.find('[data-e2e=delete-project-button]')
.trigger('mouseover')
.click();
cy.get('[e2e-data="confirm-dialog-button"]').click();
cy.get('.data-e2e-success');
cy.wait(200);
cy.get('.data-e2e-failure', { timeout: 0 }).should('not.exist');
});
});
});
});
});
});

View File

@@ -1,45 +0,0 @@
import { apiAuth } from "../../support/api/apiauth";
import { ensureHumanUserExists } from "../../support/api/users";
import { login, User } from "../../support/login/users";
describe("login policy", ()=> {
const orgPath = `${Cypress.env('consoleUrl')}/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', () => {
})
})
})
})

View File

@@ -1,34 +0,0 @@
import { login, User } from "../../support/login/users";
describe("password complexity", ()=> {
const orgPath = `${Cypress.env('consoleUrl')}/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`)
})
})
})

View File

@@ -1,85 +0,0 @@
import { apiAuth, apiCallProperties } from "../../support/api/apiauth";
import { Policy, resetPolicy } from "../../support/api/policies";
import { login, User } from "../../support/login/users";
describe("private labeling", ()=> {
const orgPath = `${Cypress.env('consoleUrl')}/org`
;[User.OrgOwner].forEach(user => {
describe(`as user "${user}"`, () => {
let api: apiCallProperties
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('[data-e2e=image-part-logo]').find('input').then(function (el) {
const blob = Cypress.Blob.base64StringToBlob(this.logo, 'image/png')
const file = new File([blob], 'images/logo.png', { type: 'image/png' })
const list = new DataTransfer()
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.skip('should update the background color', () => {
cy.contains('[data-e2e=color]', 'Background Color').find('button').click() // TODO: select data-e2e
cy.get('color-editable-input').find('input').clear().type('#ae44dc')
cy.get('[data-e2e=save-colors-button]').click()
cy.get('[data-e2e=header-user-avatar]').click()
cy.contains('Logout All Users').click() // TODO: select data-e2e
login(User.LoginPolicyUser, true, null, () => {
cy.pause()
})
})
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')
})
}

View File

@@ -1,15 +0,0 @@
import { readFileSync } from "fs";
module.exports = (on, config) => {
require('cypress-terminal-report/src/installLogsPrinter')(on);
config.defaultCommandTimeout = 10_000
config.env.parsedServiceAccountKey = config.env.serviceAccountKey
if (config.env.serviceAccountKeyPath) {
config.env.parsedServiceAccountKey = JSON.parse(readFileSync(config.env.serviceAccountKeyPath, 'utf-8'))
}
return config
}

View File

@@ -1,49 +0,0 @@
import { sign } from 'jsonwebtoken'
export interface apiCallProperties {
authHeader: string
mgntBaseURL: string
}
export function apiAuth(): Cypress.Chainable<apiCallProperties> {
const apiUrl = Cypress.env('apiUrl')
const issuerUrl = Cypress.env('issuerUrl')
const zitadelProjectResourceID = (<string>Cypress.env('zitadelProjectResourceId')).replace('bignumber-', '')
const key = Cypress.env("parsedServiceAccountKey")
const now = new Date().getTime()
const iat = Math.floor(now / 1000)
const exp = Math.floor(new Date(now + 1000 * 60 * 55).getTime() / 1000) // 55 minutes
const bearerToken = sign({
iss: key.userId,
sub: key.userId,
aud: `${issuerUrl}`,
iat: iat,
exp: exp
}, key.key, {
header: {
alg: "RS256",
kid: key.keyId
}
})
return cy.request({
method: 'POST',
url: `${apiUrl}/oauth/v2/token`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: {
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
scope: `openid urn:zitadel:iam:org:project:id:${zitadelProjectResourceID}:aud`,
assertion: bearerToken,
}
}).its('body.access_token').then(token => {
return <apiCallProperties>{
authHeader: `Bearer ${token}`,
mgntBaseURL: `${apiUrl}/management/v1/`,
}
})
}

View File

@@ -1,86 +0,0 @@
import { apiCallProperties } from "./apiauth"
export function ensureSomethingExists(api: apiCallProperties, searchPath: string, find: (entity: any) => boolean, createPath: string, body: any): Cypress.Chainable<number> {
return searchSomething(api, searchPath, find).then(sRes => {
if (sRes.entity) {
return cy.wrap({
id: sRes.entity.id,
initialSequence: 0
})
}
return cy.request({
method: 'POST',
url: `${api.mgntBaseURL}${createPath}`,
headers: {
Authorization: api.authHeader
},
body: body,
failOnStatusCode: false,
followRedirect: false,
}).then(cRes => {
expect(cRes.status).to.equal(200)
return {
id: cRes.body.id,
initialSequence: sRes.sequence
}
})
}).then((data) => {
awaitDesired(30, (entity) => !!entity, data.initialSequence, api, searchPath, find)
return cy.wrap<number>(data.id)
})
}
export function ensureSomethingDoesntExist(api: apiCallProperties, searchPath: string, find: (entity: any) => boolean, deletePath: (entity: any) => string): Cypress.Chainable<null> {
return searchSomething(api, searchPath, find).then(sRes => {
if (!sRes.entity) {
return cy.wrap(0)
}
return cy.request({
method: 'DELETE',
url: `${api.mgntBaseURL}${deletePath(sRes.entity)}`,
headers: {
Authorization: api.authHeader
},
failOnStatusCode: false
}).then((dRes) => {
expect(dRes.status).to.equal(200)
return sRes.sequence
})
}).then((initialSequence) => {
awaitDesired(30, (entity) => !entity , initialSequence, api, searchPath, find)
return null
})
}
type SearchResult = {
entity: any
sequence: number
}
function searchSomething(api: apiCallProperties, searchPath: string, find: (entity: any) => boolean): Cypress.Chainable<SearchResult> {
return cy.request({
method: 'POST',
url: `${api.mgntBaseURL}${searchPath}`,
headers: {
Authorization: api.authHeader
},
}).then(res => {
return {
entity: res.body.result?.find(find) || null,
sequence: res.body.details.processedSequence
}
})
}
function awaitDesired(trials: number, expectEntity: (entity: any) => boolean, initialSequence: number, api: apiCallProperties, searchPath: string, find: (entity: any) => boolean) {
searchSomething(api, searchPath, find).then(resp => {
if (!expectEntity(resp.entity) || resp.sequence <= initialSequence) {
expect(trials, `trying ${trials} more times`).to.be.greaterThan(0);
cy.wait(1000)
awaitDesired(trials - 1, expectEntity, initialSequence, api, searchPath, find)
}
})
}

View File

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

View File

@@ -1,80 +0,0 @@
import { apiCallProperties } from "./apiauth"
import { ensureSomethingDoesntExist, ensureSomethingExists } from "./ensure"
export function ensureProjectExists(api: apiCallProperties, projectName: string): Cypress.Chainable<number> {
return ensureSomethingExists(
api,
`projects/_search`,
(project: any) => project.name === projectName,
'projects',
{ name: projectName },
)
}
export function ensureProjectDoesntExist(api: apiCallProperties, projectName: string): Cypress.Chainable<null> {
return ensureSomethingDoesntExist(
api,
`projects/_search`,
(project: any) => project.name === projectName,
(project) => `projects/${project.id}`,
)
}
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: apiCallProperties, projectId: number, resourceType: ResourceType, resourceName: string): Cypress.Chainable<null> {
return ensureSomethingDoesntExist(
api,
`projects/${projectId}/${resourceType.resourcePath}/_search`,
(resource: any) => {
return resource[resourceType.compareProperty] === resourceName
},
(resource) => {
return `projects/${projectId}/${resourceType.resourcePath}/${resource[resourceType.identifierProperty]}`
}
)
}
export function ensureApplicationExists(api: apiCallProperties, projectId: number, appName: string): Cypress.Chainable<number> {
return ensureSomethingExists(
api,
`projects/${projectId}/${Apps.resourcePath}/_search`,
(resource: any) => resource.name === appName,
`projects/${projectId}/${Apps.resourcePath}/oidc`,
{
name: appName,
redirectUris: [
'https://e2eredirecturl.org'
],
responseTypes: [
"OIDC_RESPONSE_TYPE_CODE"
],
grantTypes: [
"OIDC_GRANT_TYPE_AUTHORIZATION_CODE"
],
authMethodType: "OIDC_AUTH_METHOD_TYPE_NONE",
postLogoutRedirectUris: [
'https://e2elogoutredirecturl.org'
],
/* "clientId": "129383004379407963@e2eprojectpermission",
"clockSkew": "0s",
"allowedOrigins": [
"https://testurl.org"
]*/
},
)
}

View File

@@ -1,49 +0,0 @@
import { apiCallProperties } from "./apiauth"
import { ensureSomethingDoesntExist, ensureSomethingExists } from "./ensure"
export function ensureHumanUserExists(api: apiCallProperties, username: string): Cypress.Chainable<number> {
return ensureSomethingExists(
api,
'users/_search',
(user: any) => user.userName === username,
'users/human',
{
user_name: username,
profile: {
first_name: 'e2efirstName',
last_name: 'e2elastName',
},
email: {
email: 'e2e@email.ch',
},
phone: {
phone: '+41 123456789',
},
})
}
export function ensureMachineUserExists(api: apiCallProperties, username: string): Cypress.Chainable<number> {
return ensureSomethingExists(
api,
'users/_search',
(user: any) => user.userName === username,
'users/machine',
{
user_name: username,
name: 'e2emachinename',
description: 'e2emachinedescription',
},
)
}
export function ensureUserDoesntExist(api: apiCallProperties, username: string): Cypress.Chainable<null> {
return ensureSomethingDoesntExist(
api,
'users/_search',
(user: any) => user.userName === username,
(user) => `users/${user.id}`
)
}

View File

@@ -1,26 +0,0 @@
/*
namespace Cypress {
interface Chainable {
*/
/**
* Custom command that authenticates a user.
*
* @example cy.consolelogin('hodor', 'hodor1234')
*/
/* consolelogin(username: string, password: string): void
}
}
Cypress.Commands.add('consolelogin', { prevSubject: false }, (username: string, password: string) => {
window.sessionStorage.removeItem("zitadel:access_token")
cy.visit(Cypress.env('consoleUrl')).then(() => {
// fill the fields and push button
cy.get('#loginName').type(username, { log: false })
cy.get('#submit-button').click()
cy.get('#password').type(password, { log: false })
cy.get('#submit-button').click()
cy.location('pathname', {timeout: 5 * 1000}).should('eq', '/');
})
})
*/

View File

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

View File

@@ -1,164 +0,0 @@
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, force?: boolean, pw?: string, onUsernameScreen?: () => void, onPasswordScreen?: () => void, onAuthenticated?: () => void): void {
let creds = credentials(user, pw)
const accountsUrl: string = Cypress.env('accountsUrl')
const consoleUrl: string = Cypress.env('consoleUrl')
const otherZitadelIdpInstance: boolean = Cypress.env('otherZitadelIdpInstance')
cy.session(creds.username, () => {
const cookies = new Map<string, string>()
if (otherZitadelIdpInstance) {
cy.intercept({
method: 'GET',
url: `${accountsUrl}/login*`,
times: 1
}, (req) => {
req.headers['cookie'] = requestCookies(cookies)
req.continue((res) => {
updateCookies(res.headers['set-cookie'] as string[], cookies)
})
}).as('login')
cy.intercept({
method: 'POST',
url: `${accountsUrl}/loginname*`,
times: 1
}, (req) => {
req.headers['cookie'] = requestCookies(cookies)
req.continue((res) => {
updateCookies(res.headers['set-cookie'] as string[], cookies)
})
}).as('loginName')
cy.intercept({
method: 'POST',
url: `${accountsUrl}/password*`,
times: 1
}, (req) => {
req.headers['cookie'] = requestCookies(cookies)
req.continue((res) => {
updateCookies(res.headers['set-cookie'] as string[], cookies)
})
}).as('password')
cy.intercept({
method: 'GET',
url: `${accountsUrl}/success*`,
times: 1
}, (req) => {
req.headers['cookie'] = requestCookies(cookies)
req.continue((res) => {
updateCookies(res.headers['set-cookie'] as string[], cookies)
})
}).as('success')
cy.intercept({
method: 'GET',
url: `${accountsUrl}/oauth/v2/authorize/callback*`,
times: 1
}, (req) => {
req.headers['cookie'] = requestCookies(cookies)
req.continue((res) => {
updateCookies(res.headers['set-cookie'] as string[], cookies)
})
}).as('callback')
cy.intercept({
method: 'GET',
url: `${accountsUrl}/oauth/v2/authorize*`,
times: 1,
}, (req) => {
req.continue((res) => {
updateCookies(res.headers['set-cookie'] as string[], cookies)
})
})
}
cy.visit(`${consoleUrl}/loginname`, { retryOnNetworkFailure: true });
otherZitadelIdpInstance && cy.wait('@login')
onUsernameScreen ? onUsernameScreen() : null
cy.get('#loginName').type(creds.username)
cy.get('#submit-button').click()
otherZitadelIdpInstance && cy.wait('@loginName')
onPasswordScreen ? onPasswordScreen() : null
cy.get('#password').type(creds.password)
cy.get('#submit-button').click()
onAuthenticated ? onAuthenticated() : null
otherZitadelIdpInstance && cy.wait('@callback')
cy.location('pathname', {timeout: 5 * 1000}).should('eq', '/');
}, {
validate: () => {
if (force) {
throw new Error("clear session");
}
cy.visit(`${consoleUrl}/users/me`)
}
})
}
export function username(withoutDomain: string, project?: string): string {
return `${withoutDomain}@${project ? `${project}.` : ''}${host(Cypress.env('apiUrl')).replace('api.', '')}`
}
function credentials(user: User, pw?: string) {
const isAdmin = user == User.IAMAdminUser
return {
username: username(isAdmin ? user : `${user}_user_name`, isAdmin ? 'caos-ag' : Cypress.env('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://', '')
}

View File

@@ -1,8 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress"]
},
"include": ["**/*.ts"]
}

View File

@@ -1,14 +0,0 @@
E2E_CYPRESS_PORT=5003
E2E_ORG=e2e-tests
E2E_ORG_OWNER_PW=Password1!
E2E_ORG_OWNER_VIEWER_PW=Password1!
E2E_ORG_PROJECT_CREATOR_PW=Password1!
E2E_PASSWORD_COMPLEXITY_USER_PW=Password1!
E2E_LOGIN_POLICY_USER_PW=Password1!
E2E_SERVICEACCOUNT_KEY_PATH="${projectRoot}/.keys/e2e.json"
E2E_CONSOLE_URL="http://localhost:4200"
E2E_API_URL="http://localhost:50002"
E2E_ACCOUNTS_URL="http://localhost:50003"
E2E_ISSUER_URL="http://localhost:50002/oauth/v2"
E2E_OTHER_ZITADEL_IDP_INSTANCE=false
E2E_ZITADEL_PROJECT_RESOURCE_ID="bignumber-$(echo -n $(docker compose -f ${projectRoot}/build/local/docker-compose-local.yml exec --no-TTY db cockroach sql --insecure --execute "select aggregate_id from eventstore.events where event_type = 'project.added' and event_data = '{\"name\": \"Zitadel\"}';" --format tsv) | cut -d " " -f 2)"

2897
console/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,9 +6,7 @@
"start": "ng serve",
"build": "ng build",
"prodbuild": "ng build --configuration production --base-href=/ui/console/",
"lint": "ng lint && stylelint './src/**/*.scss' --syntax scss",
"e2e": "./cypress.sh run e2e.env",
"e2e:open": "./cypress.sh open e2e.env"
"lint": "ng lint && stylelint './src/**/*.scss' --syntax scss"
},
"private": true,
"dependencies": {
@@ -68,18 +66,14 @@
"@typescript-eslint/eslint-plugin": "5.30.4",
"@typescript-eslint/parser": "5.30.4",
"codelyzer": "^6.0.0",
"cypress": "^10.1.0",
"cypress-terminal-report": "^4.0.1",
"eslint": "^8.18.0",
"jasmine-core": "~4.2.0",
"jasmine-spec-reporter": "~7.0.0",
"jsonwebtoken": "^8.5.1",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "^2.0.0",
"mochawesome": "^7.1.2",
"prettier": "^2.4.1",
"protractor": "~7.0.0",
"stylelint": "^13.10.0",

View File

@@ -6,7 +6,7 @@
<span>ESC</span>
</div>
</div>
<div *ngSwitchCase="ActionKeysType.ADD" class="action-keys-row">
<div *ngSwitchCase="ActionKeysType.ADD" class="action-keys-row" [attr.data-e2e]="'action-key-add'">
<div class="action-key">
<div class="key-overlay"></div>
<span>N</span>

View File

@@ -3,7 +3,7 @@
<p class="length">
<span>{{ length }} </span>{{ 'PAGINATOR.COUNT' | translate }}
</p>
<p class="ts cnsl-secondary-text" *ngIf="timestamp">
<p class="ts cnsl-secondary-text" *ngIf="timestamp" [attr.data-e2e]="'timestamp'">
{{ timestamp | timestampToDate | localizedDate: 'EEEE dd. MMM YYYY, HH:mm' }}
</p>
</div>

View File

@@ -5,8 +5,8 @@
<ng-template cnslHasRole [hasRole]="['project.role.write:' + projectId, 'project.role.write']" actions>
<a *ngIf="actionsVisible" [disabled]="disabled" [routerLink]="[ '/projects', projectId, 'roles', 'create']"
color="primary" class="cnsl-action-button" mat-raised-button [attr.data-e2e]="'add-new-role'">
<mat-icon class="icon">add</mat-icon>
color="primary" class="cnsl-action-button" mat-raised-button>
<mat-icon [attr.data-e2e]="'add-new-role'" class="icon">add</mat-icon>
<span>{{ 'ACTIONS.NEW' | translate }}</span>
<cnsl-action-keys (actionTriggered)="gotoRouterLink([ '/projects', projectId, 'roles', 'create'])">
</cnsl-action-keys>

View File

@@ -3,7 +3,8 @@
<ng-content select="[hoverActions]"></ng-content>
<ng-content select="[actions]"></ng-content>
<button (click)="$event.stopPropagation()" *ngIf="hasActions" class="table-actions-trigger" mat-icon-button
[matMenuTriggerFor]="actions">
[matMenuTriggerFor]="actions"
[attr.data-e2e]="'table-actions-button'">
<mat-icon>more_horiz</mat-icon>
</button>

View File

@@ -84,7 +84,8 @@
<td mat-cell *matCellDef="let project">
<cnsl-table-actions>
<button actions *ngIf="project.id !== zitadelProjectId" color="warn" mat-icon-button
matTooltip="{{'ACTIONS.DELETE' | translate}}" (click)="deleteProject(project.id, project.name)">
matTooltip="{{'ACTIONS.DELETE' | translate}}" (click)="deleteProject(project.id, project.name)"
[attr.data-e2e]="'delete-project-button'">
<i class="las la-trash"></i>
</button>
</cnsl-table-actions>

View File

@@ -13,11 +13,13 @@
label="{{ 'USER.TABLE.TYPES.HUMAN' | translate }}"
(clicked)="setType(Type.TYPE_HUMAN)"
[active]="type === Type.TYPE_HUMAN"
[attr.data-e2e]="'list-humans'"
></cnsl-nav-toggle>
<cnsl-nav-toggle
label="{{ 'USER.TABLE.TYPES.MACHINE' | translate }}"
(clicked)="setType(Type.TYPE_MACHINE)"
[active]="type === Type.TYPE_MACHINE"
[attr.data-e2e]="'list-machines'"
></cnsl-nav-toggle>
</div>
@@ -56,6 +58,7 @@
mat-raised-button
[disabled]="!canWrite"
class="cnsl-action-button"
[attr.data-e2e]="'create-user-button'"
>
<mat-icon class="icon">add</mat-icon>
<span>{{ 'ACTIONS.NEW' | translate }}</span>
@@ -194,6 +197,7 @@
color="warn"
(click)="deleteUser(user)"
[disabled]="!canWrite || !canDelete"
[attr.e2e-data]="!canWrite || !canDelete ? 'disabled-delete-button' : 'enabled-delete-button'"
mat-icon-button
>
<i class="las la-trash"></i>