fix(console, e2e): optimize console and activate tests (#4207)

* activate some tests

* unskip remove project tests

* focus input elements before typing

* fix: prune permissions observable

* cleanup

* remove timeout

* remove ngIf

* test with chrome

* with ngIf

* single observable

* juhu

* maybe better

* fix isAllowed response

* cleanup

Co-authored-by: Max Peintner <max@caos.ch>
This commit is contained in:
Elio Bischof 2022-09-02 15:43:44 +02:00 committed by GitHub
parent adb5394ae3
commit f0250a3fdb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 178 additions and 151 deletions

View File

@ -42,7 +42,7 @@ jobs:
- name: Build Docker Image
run: docker build -t zitadel:pr --file build/Dockerfile .artifacts/zitadel
- name: Run E2E Tests
run: docker compose run e2e
run: docker compose run e2e --browser chrome
working-directory: e2e
env:
ZITADEL_IMAGE: zitadel:pr

View File

@ -265,7 +265,7 @@ export class AppComponent implements OnDestroy {
public changedOrg(org: Org.AsObject): void {
this.themeService.loadPrivateLabelling();
this.authService.zitadelPermissionsChanged.pipe(take(1)).subscribe(() => {
this.authService.zitadelPermissions$.pipe(take(1)).subscribe(() => {
this.router.navigate(['/org'], { fragment: org.id });
});
}

View File

@ -12,8 +12,6 @@ export class RoleGuard implements CanActivate {
constructor(private authService: GrpcAuthService) {}
public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
return this.authService.fetchedZitadelPermissions
.pipe(filter((permissionsFetched) => !!permissionsFetched))
.pipe(switchMap((_) => this.authService.isAllowed(route.data['roles'], route.data['requiresAll'])));
return this.authService.isAllowed(route.data['roles'], route.data['requiresAll']);
}
}

View File

@ -97,7 +97,7 @@ export class OrgTableComponent {
public selectOrg(item: Org.AsObject, event?: any): void {
this.authService.setActiveOrg(item);
this.themeService.loadPrivateLabelling();
this.authService.zitadelPermissionsChanged.pipe(take(1)).subscribe(() => {
this.authService.zitadelPermissions$.pipe(take(1)).subscribe(() => {
this.router.navigate(['/org'], { fragment: item.id });
});
}
@ -146,7 +146,7 @@ export class OrgTableComponent {
public setAndNavigateToOrg(org: Org.AsObject): void {
this.authService.setActiveOrg(org);
this.themeService.loadPrivateLabelling();
this.authService.zitadelPermissionsChanged.pipe(take(1)).subscribe(() => {
this.authService.zitadelPermissions$.pipe(take(1)).subscribe(() => {
this.router.navigate(['/org'], { fragment: org.id });
});
}

View File

@ -52,12 +52,12 @@
(filterOpen)="filterOpen = $event"
></cnsl-filter-user>
<a
*ngIf="!selection.hasValue()"
[routerLink]="['/users', type === Type.TYPE_HUMAN ? 'create' : 'create-machine']"
color="primary"
mat-raised-button
[disabled]="!canWrite"
class="cnsl-action-button"
*ngIf="!selection.hasValue()"
data-e2e="create-user-button"
>
<mat-icon class="icon">add</mat-icon>

View File

@ -2,91 +2,102 @@ import { Injectable } from '@angular/core';
import { SortDirection } from '@angular/material/sort';
import { OAuthService } from 'angular-oauth2-oidc';
import { BehaviorSubject, from, merge, Observable, of, Subject } from 'rxjs';
import { catchError, filter, finalize, map, mergeMap, switchMap, take, timeout } from 'rxjs/operators';
import {
catchError,
distinctUntilChanged,
filter,
finalize,
map,
mergeMap,
switchMap,
take,
timeout,
withLatestFrom,
} from 'rxjs/operators';
import {
AddMyAuthFactorOTPRequest,
AddMyAuthFactorOTPResponse,
AddMyAuthFactorU2FRequest,
AddMyAuthFactorU2FResponse,
AddMyPasswordlessLinkRequest,
AddMyPasswordlessLinkResponse,
AddMyPasswordlessRequest,
AddMyPasswordlessResponse,
GetMyEmailRequest,
GetMyEmailResponse,
GetMyLabelPolicyRequest,
GetMyLabelPolicyResponse,
GetMyPasswordComplexityPolicyRequest,
GetMyPasswordComplexityPolicyResponse,
GetMyPhoneRequest,
GetMyPhoneResponse,
GetMyPrivacyPolicyRequest,
GetMyPrivacyPolicyResponse,
GetMyProfileRequest,
GetMyProfileResponse,
GetMyUserRequest,
GetMyUserResponse,
GetSupportedLanguagesRequest,
GetSupportedLanguagesResponse,
ListMyAuthFactorsRequest,
ListMyAuthFactorsResponse,
ListMyLinkedIDPsRequest,
ListMyLinkedIDPsResponse,
ListMyMembershipsRequest,
ListMyMembershipsResponse,
ListMyMetadataRequest,
ListMyMetadataResponse,
ListMyPasswordlessRequest,
ListMyPasswordlessResponse,
ListMyProjectOrgsRequest,
ListMyProjectOrgsResponse,
ListMyUserChangesRequest,
ListMyUserChangesResponse,
ListMyUserGrantsRequest,
ListMyUserGrantsResponse,
ListMyUserSessionsRequest,
ListMyUserSessionsResponse,
ListMyZitadelPermissionsRequest,
ListMyZitadelPermissionsResponse,
RemoveMyAuthFactorOTPRequest,
RemoveMyAuthFactorOTPResponse,
RemoveMyAuthFactorU2FRequest,
RemoveMyAuthFactorU2FResponse,
RemoveMyAvatarRequest,
RemoveMyAvatarResponse,
RemoveMyLinkedIDPRequest,
RemoveMyLinkedIDPResponse,
RemoveMyPasswordlessRequest,
RemoveMyPasswordlessResponse,
RemoveMyPhoneRequest,
RemoveMyPhoneResponse,
RemoveMyUserRequest,
RemoveMyUserResponse,
ResendMyEmailVerificationRequest,
ResendMyEmailVerificationResponse,
ResendMyPhoneVerificationRequest,
ResendMyPhoneVerificationResponse,
SendMyPasswordlessLinkRequest,
SendMyPasswordlessLinkResponse,
SetMyEmailRequest,
SetMyEmailResponse,
SetMyPhoneRequest,
SetMyPhoneResponse,
UpdateMyPasswordRequest,
UpdateMyPasswordResponse,
UpdateMyProfileRequest,
UpdateMyProfileResponse,
UpdateMyUserNameRequest,
UpdateMyUserNameResponse,
VerifyMyAuthFactorOTPRequest,
VerifyMyAuthFactorOTPResponse,
VerifyMyAuthFactorU2FRequest,
VerifyMyAuthFactorU2FResponse,
VerifyMyPasswordlessRequest,
VerifyMyPasswordlessResponse,
VerifyMyPhoneRequest,
VerifyMyPhoneResponse,
AddMyAuthFactorOTPRequest,
AddMyAuthFactorOTPResponse,
AddMyAuthFactorU2FRequest,
AddMyAuthFactorU2FResponse,
AddMyPasswordlessLinkRequest,
AddMyPasswordlessLinkResponse,
AddMyPasswordlessRequest,
AddMyPasswordlessResponse,
GetMyEmailRequest,
GetMyEmailResponse,
GetMyLabelPolicyRequest,
GetMyLabelPolicyResponse,
GetMyPasswordComplexityPolicyRequest,
GetMyPasswordComplexityPolicyResponse,
GetMyPhoneRequest,
GetMyPhoneResponse,
GetMyPrivacyPolicyRequest,
GetMyPrivacyPolicyResponse,
GetMyProfileRequest,
GetMyProfileResponse,
GetMyUserRequest,
GetMyUserResponse,
GetSupportedLanguagesRequest,
GetSupportedLanguagesResponse,
ListMyAuthFactorsRequest,
ListMyAuthFactorsResponse,
ListMyLinkedIDPsRequest,
ListMyLinkedIDPsResponse,
ListMyMembershipsRequest,
ListMyMembershipsResponse,
ListMyMetadataRequest,
ListMyMetadataResponse,
ListMyPasswordlessRequest,
ListMyPasswordlessResponse,
ListMyProjectOrgsRequest,
ListMyProjectOrgsResponse,
ListMyUserChangesRequest,
ListMyUserChangesResponse,
ListMyUserGrantsRequest,
ListMyUserGrantsResponse,
ListMyUserSessionsRequest,
ListMyUserSessionsResponse,
ListMyZitadelPermissionsRequest,
ListMyZitadelPermissionsResponse,
RemoveMyAuthFactorOTPRequest,
RemoveMyAuthFactorOTPResponse,
RemoveMyAuthFactorU2FRequest,
RemoveMyAuthFactorU2FResponse,
RemoveMyAvatarRequest,
RemoveMyAvatarResponse,
RemoveMyLinkedIDPRequest,
RemoveMyLinkedIDPResponse,
RemoveMyPasswordlessRequest,
RemoveMyPasswordlessResponse,
RemoveMyPhoneRequest,
RemoveMyPhoneResponse,
RemoveMyUserRequest,
RemoveMyUserResponse,
ResendMyEmailVerificationRequest,
ResendMyEmailVerificationResponse,
ResendMyPhoneVerificationRequest,
ResendMyPhoneVerificationResponse,
SendMyPasswordlessLinkRequest,
SendMyPasswordlessLinkResponse,
SetMyEmailRequest,
SetMyEmailResponse,
SetMyPhoneRequest,
SetMyPhoneResponse,
UpdateMyPasswordRequest,
UpdateMyPasswordResponse,
UpdateMyProfileRequest,
UpdateMyProfileResponse,
UpdateMyUserNameRequest,
UpdateMyUserNameResponse,
VerifyMyAuthFactorOTPRequest,
VerifyMyAuthFactorOTPResponse,
VerifyMyAuthFactorU2FRequest,
VerifyMyAuthFactorU2FResponse,
VerifyMyPasswordlessRequest,
VerifyMyPasswordlessResponse,
VerifyMyPhoneRequest,
VerifyMyPhoneResponse,
} from '../proto/generated/zitadel/auth_pb';
import { ChangeQuery } from '../proto/generated/zitadel/change_pb';
import { MetadataQuery } from '../proto/generated/zitadel/metadata_pb';
@ -103,9 +114,26 @@ export class GrpcAuthService {
private _activeOrgChanged: Subject<Org.AsObject> = new Subject();
public user!: Observable<User.AsObject | undefined>;
public userSubject: BehaviorSubject<User.AsObject | undefined> = new BehaviorSubject<User.AsObject | undefined>(undefined);
private zitadelPermissions: BehaviorSubject<string[]> = new BehaviorSubject(['user.resourceowner']);
public readonly fetchedZitadelPermissions: BehaviorSubject<boolean> = new BehaviorSubject(false as boolean);
private triggerPermissionsRefresh: Subject<void> = new Subject();
public zitadelPermissions$: Observable<string[]> = this.triggerPermissionsRefresh.pipe(
switchMap(() =>
from(this.listMyZitadelPermissions()).pipe(
map((rolesResp) => rolesResp.resultList),
filter((roles) => !!roles.length),
catchError((_) => {
return of([]);
}),
distinctUntilChanged((a, b) => {
return JSON.stringify(a.sort()) === JSON.stringify(b.sort());
}),
finalize(() => {
this.fetchedZitadelPermissions.next(true);
}),
),
),
);
private zitadelPermissions: BehaviorSubject<string[]> = new BehaviorSubject<string[]>([]);
public readonly fetchedZitadelPermissions: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
private cachedOrgs: Org.AsObject[] = [];
@ -114,6 +142,8 @@ export class GrpcAuthService {
private oauthService: OAuthService,
private storage: StorageService,
) {
this.zitadelPermissions$.subscribe(this.zitadelPermissions);
this.user = merge(
of(this.oauthService.getAccessToken()).pipe(filter((token) => (token ? true : false))),
this.oauthService.events.pipe(
@ -223,19 +253,7 @@ export class GrpcAuthService {
}
private loadPermissions(): void {
from(this.listMyZitadelPermissions())
.pipe(
map((rolesResp) => rolesResp.resultList),
catchError((_) => {
return of([]);
}),
finalize(() => {
this.fetchedZitadelPermissions.next(true);
}),
)
.subscribe((roles) => {
this.zitadelPermissions.next(roles);
});
this.triggerPermissionsRefresh.next();
}
/**
@ -244,7 +262,17 @@ export class GrpcAuthService {
*/
public isAllowed(roles: string[] | RegExp[], requiresAll: boolean = false): Observable<boolean> {
if (roles && roles.length > 0) {
return this.zitadelPermissions.pipe(switchMap((zroles) => of(this.hasRoles(zroles, roles, requiresAll))));
return this.fetchedZitadelPermissions.pipe(
withLatestFrom(this.zitadelPermissions),
filter(([hL, p]) => {
return hL === true && !!p.length;
}),
map(([_, zroles]) => {
const what = this.hasRoles(zroles, roles, requiresAll);
return what;
}),
distinctUntilChanged(),
);
} else {
return of(false);
}
@ -356,10 +384,6 @@ export class GrpcAuthService {
return this.grpcService.auth.updateMyProfile(req, null).then((resp) => resp.toObject());
}
public get zitadelPermissionsChanged(): Observable<string[]> {
return this.zitadelPermissions;
}
public listMyUserSessions(): Promise<ListMyUserSessionsResponse.AsObject> {
const req = new ListMyUserSessionsRequest();
return this.grpcService.auth.listMyUserSessions(req, null).then((resp) => resp.toObject());

View File

@ -5,7 +5,7 @@ import {
} from "../../support/api/users";
import { loginname } from "../../support/login/users";
describe.skip("humans", () => {
describe("humans", () => {
const humansPath = `/users?type=human`;
const testHumanUserNameAdd = "e2ehumanusernameadd";
const testHumanUserNameRemove = "e2ehumanusernameremove";
@ -22,18 +22,20 @@ describe.skip("humans", () => {
});
it("should add a user", () => {
cy.get('[data-e2e="action-key-add"]')
.parents('[data-e2e="create-user-button"]')
cy.get('[data-e2e="create-user-button"]')
.click();
cy.url().should("contain", "users/create");
cy.get('[formcontrolname="email"]').type(loginname("e2ehuman"));
cy.get('[formcontrolname="email"]')
.type(loginname("e2ehuman", Cypress.env("ORGANIZATION")));
//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('[formcontrolname="userName"]')
.type(testHumanUserNameAdd);
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);
@ -55,10 +57,10 @@ describe.skip("humans", () => {
// doesn't work, need to force click.
// .trigger('mouseover')
.find('[data-e2e="enabled-delete-button"]')
.click({ force: true });
.click({force: true});
cy.get('[data-e2e="confirm-dialog-input"]')
.click()
.type(loginname(testHumanUserNameRemove, Cypress.env("org")));
.focus()
.type(loginname(testHumanUserNameRemove, Cypress.env("ORGANIZATION")));
cy.get('[data-e2e="confirm-dialog-button"]').click();
cy.get(".data-e2e-success");
cy.wait(200);

View File

@ -5,7 +5,7 @@ import {
} from "../../support/api/users";
import { loginname } from "../../support/login/users";
describe.skip("machines", () => {
describe("machines", () => {
const machinesPath = `/users?type=machine`;
const testMachineUserNameAdd = "e2emachineusernameadd";
const testMachineUserNameRemove = "e2emachineusernameremove";
@ -22,16 +22,16 @@ describe.skip("machines", () => {
});
it("should add a machine", () => {
cy.get('[data-e2e="action-key-add"]')
.parents('[data-e2e="create-user-button"]')
cy.get('[data-e2e="create-user-button"]')
.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('[formcontrolname="userName"]')
.type(testMachineUserNameAdd);
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);
@ -49,14 +49,14 @@ describe.skip("machines", () => {
});
it("should delete a machine", () => {
cy.contains("tr", testMachineUserNameRemove, { timeout: 1000 })
cy.contains("tr", testMachineUserNameRemove)
// doesn't work, need to force click.
// .trigger('mouseover')
.find('[data-e2e="enabled-delete-button"]')
.click({ force: true });
.click({force: true});
cy.get('[data-e2e="confirm-dialog-input"]')
.click()
.type(loginname(testMachineUserNameRemove, Cypress.env("org")));
.focus()
.type(loginname(testMachineUserNameRemove, Cypress.env("ORGANIZATION")));
cy.get('[data-e2e="confirm-dialog-button"]').click();
cy.get(".data-e2e-success");
cy.wait(200);

View File

@ -1,7 +1,7 @@
import { apiAuth } from "../../support/api/apiauth";
import { ensureProjectExists, ensureProjectResourceDoesntExist, Roles } from "../../support/api/projects";
describe.skip('permissions', () => {
describe('permissions', () => {
const testProjectName = 'e2eprojectpermission'
const testAppName = 'e2eapppermission'
@ -28,12 +28,16 @@ describe.skip('permissions', () => {
})
})
it('should add a role', () => {
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('[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')

View File

@ -42,9 +42,9 @@ describe("projects", () => {
cy.contains("tr", testProjectNameDeleteList, { timeout: 1000 })
.find('[data-e2e="delete-project-button"]')
.click({ force: true });
cy.get('[data-e2e="confirm-dialog-input"]').focus().type(
testProjectNameDeleteList
);
cy.get('[data-e2e="confirm-dialog-input"]')
.focus()
.type(testProjectNameDeleteList);
cy.get('[data-e2e="confirm-dialog-button"]').click();
cy.get(".data-e2e-success");
cy.wait(200);
@ -63,11 +63,10 @@ describe("projects", () => {
it("removes the project", () => {
cy.contains('[data-e2e="grid-card"]', testProjectNameDeleteGrid)
.find('[data-e2e="delete-project-button"]')
.trigger("mouseover")
.click();
cy.get('[data-e2e="confirm-dialog-input"]').focus().type(
testProjectNameDeleteGrid
);
.click({force: true});
cy.get('[data-e2e="confirm-dialog-input"]')
.focus()
.type(testProjectNameDeleteGrid);
cy.get('[data-e2e="confirm-dialog-button"]').click();
cy.get(".data-e2e-success");
cy.wait(200);