@appsemble/e2e 0.36.10-test.4 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
- # ![](https://gitlab.com/appsemble/appsemble/-/raw/0.36.10-test.4/config/assets/logo.svg) Appsemble End 2 End Tests
1
+ # ![](https://gitlab.com/appsemble/appsemble/-/raw/0.37.0/config/assets/logo.svg) Appsemble End 2 End Tests
2
2
 
3
3
  > Run end 2 end tests on an Appsemble environment and provide Appsemble fixtures
4
4
 
5
- [![GitLab CI](https://gitlab.com/appsemble/appsemble/badges/0.36.10-test.4/pipeline.svg)](https://gitlab.com/appsemble/appsemble/-/releases/0.36.10-test.4)
5
+ [![GitLab CI](https://gitlab.com/appsemble/appsemble/badges/0.37.0/pipeline.svg)](https://gitlab.com/appsemble/appsemble/-/releases/0.37.0)
6
6
  [![Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://prettier.io)
7
7
 
8
8
  ## Table of Contents
@@ -102,5 +102,5 @@ When writing end-to-end tests, have a look at the
102
102
 
103
103
  ## License
104
104
 
105
- [LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.36.10-test.4/LICENSE.md) ©
105
+ [LGPL-3.0-only](https://gitlab.com/appsemble/appsemble/-/blob/0.37.0/LICENSE.md) ©
106
106
  [Appsemble](https://appsemble.com)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appsemble/e2e",
3
- "version": "0.36.10-test.4",
3
+ "version": "0.37.0",
4
4
  "description": "End-to-end tests used for testing Appsemble apps in an existing environment.",
5
5
  "keywords": [
6
6
  "app",
@@ -41,9 +41,9 @@
41
41
  "@playwright/test": "Pinned exact and kept in sync with the mcr.microsoft.com/playwright:v<version>-noble tag of the `e2e node` job in .gitlab-ci.yml; Renovate bumps both together (see renovate.json)."
42
42
  },
43
43
  "dependencies": {
44
- "@appsemble/lang-sdk": "0.36.10-test.4",
45
- "@appsemble/node-utils": "0.36.10-test.4",
46
- "@appsemble/types": "0.36.10-test.4",
44
+ "@appsemble/lang-sdk": "0.37.0",
45
+ "@appsemble/node-utils": "0.37.0",
46
+ "@appsemble/types": "0.37.0",
47
47
  "@playwright/test": "1.58.2",
48
48
  "strip-indent": "^4.0.0"
49
49
  },
@@ -24,14 +24,16 @@ test.describe('Person', () => {
24
24
  const lastName = `Last name ${date}`;
25
25
  const email = `Email${date}@example.com`;
26
26
  const description = `Description ${date}`;
27
+ const submitButton = page.locator('button[type="submit"]');
28
+ await expect(submitButton).toBeEnabled({ timeout: 15000 });
27
29
  // The form block resets field values while it finishes initializing (the
28
30
  // file field briefly holds the form in a loading state), dropping values
29
31
  // typed too early. Re-fill until every field keeps its value.
30
32
  await expect(async () => {
31
- await page.getByPlaceholder('First name').fill(firstName);
32
33
  await page.getByPlaceholder('Last name').fill(lastName);
33
34
  await page.getByPlaceholder('Email').fill(email);
34
35
  await page.getByPlaceholder('Description').fill(description);
36
+ await page.getByPlaceholder('First name').fill(firstName);
35
37
  await expect(page.getByPlaceholder('First name')).toHaveValue(firstName, {
36
38
  timeout: 1000,
37
39
  });
@@ -45,7 +47,7 @@ test.describe('Person', () => {
45
47
  timeout: 1000,
46
48
  });
47
49
  }).toPass({ timeout: 10000 });
48
- await page.click('button[type="submit"]');
50
+ await submitButton.click();
49
51
  await page.click(`td:has-text("${firstName}")`);
50
52
  await expect(page.getByText(firstName)).toBeVisible();
51
53
  await expect(page.getByText(lastName)).toBeVisible();
@@ -41,10 +41,12 @@ test.describe('Individual app', () => {
41
41
  await codeEditor.getByText('name: test app').press('End');
42
42
  await codeEditor.getByText('name: test app').press('Backspace');
43
43
  await codeEditor.getByText('name: test ap').press('p');
44
- const responsePromise = page.waitForResponse((response) => response.url().includes('/api/apps/') && response.request().method() === 'PATCH');
44
+ const responsePromise = page.waitForResponse((response) => response.url().includes('/api/apps/') &&
45
+ response.request().method() === 'PATCH' &&
46
+ response.status() === 200);
45
47
  await page.getByRole('button', { name: 'Publish' }).click();
46
48
  await responsePromise;
47
- await expect(page.getByText('Successfully updated app definition')).toBeVisible();
49
+ await expect(page.getByRole('button', { name: 'Publish' })).toBeDisabled();
48
50
  await page.getByRole('link', { name: 'Details' }).click();
49
51
  await expect(page.getByText('Clone App')).toBeVisible();
50
52
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,225 @@
1
+ import { expect, authenticatedTest as test } from '../../../index.js';
2
+ const { KEYCLOAK_ADMIN_PASSWORD = 'password', KEYCLOAK_ADMIN_USERNAME = 'admin', KEYCLOAK_BASE_URL, KEYCLOAK_REALM = 'appsemble-e2e', } = process.env;
3
+ const clientId = 'appsemble-oidc';
4
+ const clientSecret = 'appsemble-oidc-secret';
5
+ const userEmail = 'keycloak-user@appsemble.com';
6
+ const userPassword = 'password';
7
+ let app;
8
+ let organizationId;
9
+ let secret;
10
+ async function requestKc(path, { body, expectedStatuses, method, token, }) {
11
+ const response = await fetch(`${KEYCLOAK_BASE_URL}${path}`, {
12
+ body,
13
+ headers: {
14
+ authorization: `Bearer ${token}`,
15
+ 'content-type': 'application/json',
16
+ },
17
+ method,
18
+ });
19
+ if (!expectedStatuses.includes(response.status)) {
20
+ throw new Error(`${method} ${path} returned ${response.status}: ${await response.text()}`);
21
+ }
22
+ return response;
23
+ }
24
+ async function getAdminToken() {
25
+ for (let attempt = 0; attempt < 60; attempt += 1) {
26
+ try {
27
+ const response = await fetch(`${KEYCLOAK_BASE_URL}/realms/master/protocol/openid-connect/token`, {
28
+ body: new URLSearchParams({
29
+ client_id: 'admin-cli',
30
+ grant_type: 'password',
31
+ password: KEYCLOAK_ADMIN_PASSWORD,
32
+ username: KEYCLOAK_ADMIN_USERNAME,
33
+ }),
34
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
35
+ method: 'POST',
36
+ });
37
+ if (response.ok) {
38
+ const { access_token: token } = (await response.json());
39
+ return token;
40
+ }
41
+ }
42
+ catch {
43
+ // The service may still be starting.
44
+ }
45
+ await new Promise((resolve) => {
46
+ setTimeout(resolve, 1000);
47
+ });
48
+ }
49
+ throw new Error('Could not obtain Keycloak admin token');
50
+ }
51
+ async function createGroup(token, name, parentGroupId) {
52
+ const path = parentGroupId
53
+ ? `/admin/realms/${KEYCLOAK_REALM}/groups/${parentGroupId}/children`
54
+ : `/admin/realms/${KEYCLOAK_REALM}/groups`;
55
+ const response = await requestKc(path, {
56
+ body: JSON.stringify({ name }),
57
+ expectedStatuses: [201],
58
+ method: 'POST',
59
+ token,
60
+ });
61
+ return response.headers.get('location').split('/').pop();
62
+ }
63
+ async function createUser(token) {
64
+ const response = await requestKc(`/admin/realms/${KEYCLOAK_REALM}/users`, {
65
+ body: JSON.stringify({
66
+ credentials: [{ temporary: false, type: 'password', value: userPassword }],
67
+ email: userEmail,
68
+ emailVerified: true,
69
+ enabled: true,
70
+ firstName: 'Keycloak',
71
+ lastName: 'User',
72
+ username: userEmail,
73
+ }),
74
+ expectedStatuses: [201],
75
+ method: 'POST',
76
+ token,
77
+ });
78
+ return response.headers.get('location').split('/').pop();
79
+ }
80
+ async function joinGroup(token, userId, groupId) {
81
+ await requestKc(`/admin/realms/${KEYCLOAK_REALM}/users/${userId}/groups/${groupId}`, {
82
+ expectedStatuses: [204],
83
+ method: 'PUT',
84
+ token,
85
+ });
86
+ }
87
+ async function configureRealm() {
88
+ const token = await getAdminToken();
89
+ await requestKc(`/admin/realms/${KEYCLOAK_REALM}`, {
90
+ expectedStatuses: [204, 404],
91
+ method: 'DELETE',
92
+ token,
93
+ });
94
+ await requestKc('/admin/realms', {
95
+ body: JSON.stringify({ enabled: true, realm: KEYCLOAK_REALM }),
96
+ expectedStatuses: [201],
97
+ method: 'POST',
98
+ token,
99
+ });
100
+ const parentGroupId = await createGroup(token, 'Appsemble');
101
+ const managersGroupId = await createGroup(token, 'Managers', parentGroupId);
102
+ const editorsGroupId = await createGroup(token, 'Editors', parentGroupId);
103
+ await requestKc(`/admin/realms/${KEYCLOAK_REALM}/clients`, {
104
+ body: JSON.stringify({
105
+ clientAuthenticatorType: 'client-secret',
106
+ clientId,
107
+ directAccessGrantsEnabled: true,
108
+ enabled: true,
109
+ protocol: 'openid-connect',
110
+ protocolMappers: [
111
+ {
112
+ config: {
113
+ 'access.token.claim': 'true',
114
+ 'claim.name': 'groups',
115
+ 'full.path': 'true',
116
+ 'id.token.claim': 'true',
117
+ 'userinfo.token.claim': 'true',
118
+ },
119
+ name: 'groups',
120
+ protocol: 'openid-connect',
121
+ protocolMapper: 'oidc-group-membership-mapper',
122
+ },
123
+ ],
124
+ publicClient: false,
125
+ redirectUris: ['http://appsemble:9999/callback'],
126
+ secret: clientSecret,
127
+ standardFlowEnabled: true,
128
+ webOrigins: ['+'],
129
+ }),
130
+ expectedStatuses: [201],
131
+ method: 'POST',
132
+ token,
133
+ });
134
+ const userId = await createUser(token);
135
+ await joinGroup(token, userId, managersGroupId);
136
+ await joinGroup(token, userId, editorsGroupId);
137
+ }
138
+ test.describe('App Keycloak OIDC login', () => {
139
+ test.skip(!KEYCLOAK_BASE_URL, 'Keycloak OIDC login requires KEYCLOAK_BASE_URL');
140
+ test.beforeAll(async ({ createApp, createOrganization, randomTestId, request }) => {
141
+ await configureRealm();
142
+ organizationId = (await createOrganization({ id: randomTestId() })).id;
143
+ app = await createApp(organizationId, `
144
+ name: Keycloak Role Mapping App
145
+ defaultPage: Manager Page
146
+ security:
147
+ default:
148
+ role: Viewer
149
+ policy: everyone
150
+ roles:
151
+ Viewer: {}
152
+ Manager: {}
153
+ Editor: {}
154
+ Auditor: {}
155
+ pages:
156
+ - name: Manager Page
157
+ roles:
158
+ - Manager
159
+ blocks:
160
+ - type: html
161
+ version: 0.36.10
162
+ parameters:
163
+ content: <h1>Manager content</h1>
164
+ - name: Editor Page
165
+ roles:
166
+ - Editor
167
+ blocks:
168
+ - type: html
169
+ version: 0.36.10
170
+ parameters:
171
+ content: <h1>Editor content</h1>
172
+ - name: Auditor Page
173
+ roles:
174
+ - Auditor
175
+ blocks:
176
+ - type: html
177
+ version: 0.36.10
178
+ parameters:
179
+ content: <h1>Auditor content</h1>
180
+ `);
181
+ const response = await request.post(`/api/apps/${app.id}/secrets/oauth2`, {
182
+ data: {
183
+ name: 'Keycloak OIDC',
184
+ icon: 'key',
185
+ authorizationUrl: `${KEYCLOAK_BASE_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/auth`,
186
+ tokenUrl: `${KEYCLOAK_BASE_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/token`,
187
+ clientId,
188
+ clientSecret,
189
+ userInfoUrl: `${KEYCLOAK_BASE_URL}/realms/${KEYCLOAK_REALM}/protocol/openid-connect/userinfo`,
190
+ scope: 'email openid profile',
191
+ roleMappings: [
192
+ { group: '/Appsemble/Managers', role: 'Manager' },
193
+ { group: '/Appsemble/Editors', role: 'Editor' },
194
+ ],
195
+ },
196
+ });
197
+ expect(response.status()).toBe(201);
198
+ secret = (await response.json());
199
+ });
200
+ test.afterAll(async ({ deleteApp, deleteOrganization }) => {
201
+ if (app?.id != null) {
202
+ await deleteApp(app.id);
203
+ }
204
+ if (organizationId) {
205
+ await deleteOrganization(organizationId);
206
+ }
207
+ });
208
+ test('should map Keycloak groups to cumulative app roles', async ({ page, visitApp }) => {
209
+ const appUrl = await visitApp(app.id);
210
+ await page.goto(`${appUrl}/Login`);
211
+ await page.getByRole('button', { name: `Login with ${secret.name}` }).click();
212
+ await page.locator('#username').fill(userEmail);
213
+ await page.locator('#password').fill(userPassword);
214
+ await page.locator('#kc-login').click();
215
+ await expect(page.getByRole('heading', { name: 'Manager content' })).toBeVisible();
216
+ await expect(page.getByRole('link', { name: 'Manager Page' })).toBeVisible();
217
+ await expect(page.getByRole('link', { name: 'Editor Page' })).toBeVisible();
218
+ await expect(page.getByRole('link', { name: 'Auditor Page' })).toBeHidden();
219
+ await page.getByRole('link', { name: 'Editor Page' }).click();
220
+ await expect(page.getByRole('heading', { name: 'Editor content' })).toBeVisible();
221
+ await page.goto(`${appUrl}/Auditor%20Page`);
222
+ await expect(page.getByRole('heading', { name: 'Auditor content' })).toBeHidden();
223
+ });
224
+ });
225
+ //# sourceMappingURL=keycloak-oidc.spec.js.map
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,291 @@
1
+ import { expect, authenticatedTest as test } from '../../../index.js';
2
+ const { KEYCLOAK_ADMIN_PASSWORD = 'password', KEYCLOAK_ADMIN_USERNAME = 'admin', KEYCLOAK_BASE_URL, KEYCLOAK_REALM = 'appsemble-e2e', } = process.env;
3
+ const appsembleBaseUrl = 'http://appsemble:9999';
4
+ const samlAttributeNameFormat = 'attribute.nameformat';
5
+ const samlAssertionConsumerUrlPostAttribute = 'saml_assertion_consumer_url_post';
6
+ const samlAuthnStatementAttribute = 'saml.authnstatement';
7
+ const samlNameIdFormatAttribute = 'saml_name_id_format';
8
+ const userEmail = 'keycloak-saml-user@appsemble.com';
9
+ const userPassword = 'password';
10
+ let app;
11
+ let idpCertificate;
12
+ let organizationId;
13
+ let secret;
14
+ test.use({ timezoneId: 'Europe/Amsterdam' });
15
+ async function requestKc(path, { body, expectedStatuses, method, token, }) {
16
+ const response = await fetch(`${KEYCLOAK_BASE_URL}${path}`, {
17
+ body,
18
+ headers: {
19
+ authorization: `Bearer ${token}`,
20
+ 'content-type': 'application/json',
21
+ },
22
+ method,
23
+ });
24
+ if (!expectedStatuses.includes(response.status)) {
25
+ throw new Error(`${method} ${path} returned ${response.status}: ${await response.text()}`);
26
+ }
27
+ return response;
28
+ }
29
+ async function getAdminToken() {
30
+ for (let attempt = 0; attempt < 60; attempt += 1) {
31
+ try {
32
+ const response = await fetch(`${KEYCLOAK_BASE_URL}/realms/master/protocol/openid-connect/token`, {
33
+ body: new URLSearchParams({
34
+ client_id: 'admin-cli',
35
+ grant_type: 'password',
36
+ password: KEYCLOAK_ADMIN_PASSWORD,
37
+ username: KEYCLOAK_ADMIN_USERNAME,
38
+ }),
39
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
40
+ method: 'POST',
41
+ });
42
+ if (response.ok) {
43
+ const { access_token: token } = (await response.json());
44
+ return token;
45
+ }
46
+ }
47
+ catch {
48
+ // The service may still be starting.
49
+ }
50
+ await new Promise((resolve) => {
51
+ setTimeout(resolve, 1000);
52
+ });
53
+ }
54
+ throw new Error('Could not obtain Keycloak admin token');
55
+ }
56
+ async function createGroup(token, name, parentGroupId) {
57
+ const path = parentGroupId
58
+ ? `/admin/realms/${KEYCLOAK_REALM}/groups/${parentGroupId}/children`
59
+ : `/admin/realms/${KEYCLOAK_REALM}/groups`;
60
+ const response = await requestKc(path, {
61
+ body: JSON.stringify({ name }),
62
+ expectedStatuses: [201],
63
+ method: 'POST',
64
+ token,
65
+ });
66
+ return response.headers.get('location').split('/').pop();
67
+ }
68
+ async function createUser(token) {
69
+ const response = await requestKc(`/admin/realms/${KEYCLOAK_REALM}/users`, {
70
+ body: JSON.stringify({
71
+ credentials: [{ temporary: false, type: 'password', value: userPassword }],
72
+ email: userEmail,
73
+ emailVerified: true,
74
+ enabled: true,
75
+ firstName: 'Keycloak',
76
+ lastName: 'SAML User',
77
+ username: userEmail,
78
+ }),
79
+ expectedStatuses: [201],
80
+ method: 'POST',
81
+ token,
82
+ });
83
+ return response.headers.get('location').split('/').pop();
84
+ }
85
+ function wrapCertificate(certificate) {
86
+ const lines = certificate.match(/.{1,64}/g);
87
+ if (!lines) {
88
+ throw new Error('Could not wrap Keycloak signing certificate');
89
+ }
90
+ return `-----BEGIN CERTIFICATE-----\n${lines.join('\n')}\n-----END CERTIFICATE-----`;
91
+ }
92
+ async function getSigningCertificate(token) {
93
+ const response = await requestKc(`/admin/realms/${KEYCLOAK_REALM}/keys`, {
94
+ expectedStatuses: [200],
95
+ method: 'GET',
96
+ token,
97
+ });
98
+ const { keys } = (await response.json());
99
+ const certificate = keys.find((key) => key.use === 'SIG' && key.status === 'ACTIVE' && key.certificate)?.certificate;
100
+ if (!certificate) {
101
+ throw new Error('Could not find Keycloak signing certificate');
102
+ }
103
+ return wrapCertificate(certificate);
104
+ }
105
+ async function joinGroup(token, userId, groupId) {
106
+ await requestKc(`/admin/realms/${KEYCLOAK_REALM}/users/${userId}/groups/${groupId}`, {
107
+ expectedStatuses: [204],
108
+ method: 'PUT',
109
+ token,
110
+ });
111
+ }
112
+ async function configureRealm() {
113
+ const token = await getAdminToken();
114
+ await requestKc(`/admin/realms/${KEYCLOAK_REALM}`, {
115
+ expectedStatuses: [204, 404],
116
+ method: 'DELETE',
117
+ token,
118
+ });
119
+ await requestKc('/admin/realms', {
120
+ body: JSON.stringify({ enabled: true, realm: KEYCLOAK_REALM }),
121
+ expectedStatuses: [201],
122
+ method: 'POST',
123
+ token,
124
+ });
125
+ idpCertificate = await getSigningCertificate(token);
126
+ const parentGroupId = await createGroup(token, 'Appsemble');
127
+ const managersGroupId = await createGroup(token, 'Managers', parentGroupId);
128
+ const editorsGroupId = await createGroup(token, 'Editors', parentGroupId);
129
+ const userId = await createUser(token);
130
+ await joinGroup(token, userId, managersGroupId);
131
+ await joinGroup(token, userId, editorsGroupId);
132
+ }
133
+ async function createSamlClient(token, appId, secretId) {
134
+ const samlBaseUrl = `${appsembleBaseUrl}/api/apps/${appId}/saml/${secretId}`;
135
+ const acsUrl = `${samlBaseUrl}/acs`;
136
+ const metadataUrl = `${samlBaseUrl}/metadata.xml`;
137
+ await requestKc(`/admin/realms/${KEYCLOAK_REALM}/clients`, {
138
+ body: JSON.stringify({
139
+ attributes: {
140
+ 'saml.assertion.signature': 'true',
141
+ [samlAuthnStatementAttribute]: 'true',
142
+ 'saml.client.signature': 'false',
143
+ 'saml.encrypt': 'false',
144
+ 'saml.force.post.binding': 'true',
145
+ 'saml.server.signature': 'true',
146
+ 'saml.signature.algorithm': 'RSA_SHA256',
147
+ [samlAssertionConsumerUrlPostAttribute]: acsUrl,
148
+ [samlNameIdFormatAttribute]: 'email',
149
+ },
150
+ clientId: metadataUrl,
151
+ enabled: true,
152
+ name: 'Appsemble SAML',
153
+ protocol: 'saml',
154
+ protocolMappers: [
155
+ {
156
+ config: {
157
+ 'attribute.name': 'email',
158
+ [samlAttributeNameFormat]: 'Basic',
159
+ 'user.attribute': 'email',
160
+ },
161
+ name: 'email',
162
+ protocol: 'saml',
163
+ protocolMapper: 'saml-user-property-mapper',
164
+ },
165
+ {
166
+ config: {
167
+ 'attribute.name': 'emailVerified',
168
+ [samlAttributeNameFormat]: 'Basic',
169
+ 'attribute.value': 'true',
170
+ },
171
+ name: 'email verified',
172
+ protocol: 'saml',
173
+ protocolMapper: 'saml-hardcode-attribute-mapper',
174
+ },
175
+ {
176
+ config: {
177
+ 'attribute.name': 'name',
178
+ [samlAttributeNameFormat]: 'Basic',
179
+ 'user.attribute': 'firstName',
180
+ },
181
+ name: 'name',
182
+ protocol: 'saml',
183
+ protocolMapper: 'saml-user-property-mapper',
184
+ },
185
+ {
186
+ config: {
187
+ 'attribute.name': 'memberOf',
188
+ [samlAttributeNameFormat]: 'Basic',
189
+ 'full.path': 'true',
190
+ single: 'true',
191
+ },
192
+ name: 'groups',
193
+ protocol: 'saml',
194
+ protocolMapper: 'saml-group-membership-mapper',
195
+ },
196
+ ],
197
+ redirectUris: [acsUrl],
198
+ }),
199
+ expectedStatuses: [201],
200
+ method: 'POST',
201
+ token,
202
+ });
203
+ }
204
+ test.describe('App Keycloak SAML login', () => {
205
+ test.skip(!KEYCLOAK_BASE_URL, 'Keycloak SAML login requires KEYCLOAK_BASE_URL');
206
+ test.beforeAll(async ({ createApp, createOrganization, randomTestId, request }) => {
207
+ await configureRealm();
208
+ organizationId = (await createOrganization({ id: randomTestId() })).id;
209
+ app = await createApp(organizationId, `
210
+ name: Keycloak SAML Roles App
211
+ defaultPage: Manager Page
212
+ security:
213
+ default:
214
+ role: Viewer
215
+ policy: everyone
216
+ roles:
217
+ Viewer: {}
218
+ Manager: {}
219
+ Editor: {}
220
+ Auditor: {}
221
+ pages:
222
+ - name: Manager Page
223
+ roles:
224
+ - Manager
225
+ blocks:
226
+ - type: html
227
+ version: 0.36.7
228
+ parameters:
229
+ content: <h1>Manager content</h1>
230
+ - name: Editor Page
231
+ roles:
232
+ - Editor
233
+ blocks:
234
+ - type: html
235
+ version: 0.36.7
236
+ parameters:
237
+ content: <h1>Editor content</h1>
238
+ - name: Auditor Page
239
+ roles:
240
+ - Auditor
241
+ blocks:
242
+ - type: html
243
+ version: 0.36.7
244
+ parameters:
245
+ content: <h1>Auditor content</h1>
246
+ `);
247
+ const response = await request.post(`/api/apps/${app.id}/secrets/saml`, {
248
+ data: {
249
+ name: 'Keycloak SAML',
250
+ icon: 'key',
251
+ entityId: `${KEYCLOAK_BASE_URL}/realms/${KEYCLOAK_REALM}/protocol/saml/descriptor`,
252
+ ssoUrl: `${KEYCLOAK_BASE_URL}/realms/${KEYCLOAK_REALM}/protocol/saml`,
253
+ idpCertificate,
254
+ emailAttribute: 'email',
255
+ emailVerifiedAttribute: 'emailVerified',
256
+ nameAttribute: 'name',
257
+ groupAttribute: 'memberOf',
258
+ roleMappings: [
259
+ { group: '/Appsemble/Managers', role: 'Manager' },
260
+ { group: '/Appsemble/Editors', role: 'Editor' },
261
+ ],
262
+ },
263
+ });
264
+ expect(response.status()).toBe(201);
265
+ secret = (await response.json());
266
+ await createSamlClient(await getAdminToken(), app.id, secret.id);
267
+ });
268
+ test.afterAll(async ({ deleteApp, deleteOrganization }) => {
269
+ if (app?.id != null) {
270
+ await deleteApp(app.id);
271
+ }
272
+ if (organizationId) {
273
+ await deleteOrganization(organizationId);
274
+ }
275
+ });
276
+ test('should map Keycloak groups to cumulative app roles', async ({ page, visitApp }) => {
277
+ const appUrl = await visitApp(app.id);
278
+ await page.goto(`${appUrl}/Login`);
279
+ await page.getByRole('button', { name: `Login with ${secret.name}` }).click();
280
+ await page.locator('#username').fill(userEmail);
281
+ await page.locator('#password').fill(userPassword);
282
+ await page.locator('#kc-login').click();
283
+ await expect(page.getByRole('heading', { name: 'Manager content' })).toBeVisible();
284
+ await expect(page.getByRole('link', { name: 'Manager Page' })).toBeVisible();
285
+ await expect(page.getByRole('link', { name: 'Editor Page' })).toBeVisible();
286
+ await expect(page.getByRole('link', { name: 'Auditor Page' })).toBeHidden();
287
+ await page.getByRole('link', { name: 'Editor Page' }).click();
288
+ await expect(page.getByRole('heading', { name: 'Editor content' })).toBeVisible();
289
+ });
290
+ });
291
+ //# sourceMappingURL=keycloak-saml.spec.js.map