@appsemble/e2e 0.36.10 → 0.37.1

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