mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-09-15 15:39:04 +02:00
feat!: replace custom claims with custom fields
This commit is contained in:
@@ -161,3 +161,41 @@ export const signupTokens = {
|
||||
createdAt: new Date(Date.now() - 1 * 60 * 60 * 1000).toISOString()
|
||||
}
|
||||
};
|
||||
|
||||
export const customFields = {
|
||||
department: {
|
||||
id: '189356b1-57f3-4c14-bd59-3ae1132a36d1',
|
||||
key: 'department',
|
||||
type: 'string',
|
||||
userEditable: true,
|
||||
displayName: 'Department',
|
||||
target: 'user',
|
||||
validationRegex: '^[A-Za-z]+$',
|
||||
validationErrorMessage: 'Department must only contain letters'
|
||||
},
|
||||
elevatedRights: {
|
||||
id: '8d081fd8-6a51-45a1-8051-04c3b043f5bd',
|
||||
key: 'elevatedRights',
|
||||
type: 'boolean',
|
||||
displayName: 'Elevated Rights',
|
||||
target: 'group'
|
||||
},
|
||||
internalId: {
|
||||
id: '3d7c6054-e146-48cb-b2d3-7d7897dcbc51',
|
||||
key: 'internalId',
|
||||
type: 'number',
|
||||
userEditable: false,
|
||||
displayName: 'Internal ID',
|
||||
target: 'both',
|
||||
required: true
|
||||
},
|
||||
nickname: {
|
||||
id: '0b68d19a-bb72-4750-84b4-2f0992f5200c',
|
||||
key: 'department',
|
||||
type: 'string',
|
||||
userEditable: true,
|
||||
required: true,
|
||||
displayName: 'Nickname',
|
||||
target: 'user'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -31,6 +31,10 @@
|
||||
{
|
||||
"key": "instanceId",
|
||||
"value": "test-instance-id"
|
||||
},
|
||||
{
|
||||
"key": "customFields",
|
||||
"value": "[{\"id\":\"189356b1-57f3-4c14-bd59-3ae1132a36d1\",\"key\":\"department\",\"displayName\":\"Department\",\"type\":\"string\",\"target\":\"user\",\"required\":false,\"userEditable\":true,\"defaultValue\":\"\",\"validationRegex\":\"^[A-Za-z]+$\",\"validationErrorMessage\":\"Department must only contain letters\"},{\"id\":\"0b68d19a-bb72-4750-84b4-2f0992f5200c\",\"key\":\"nickname\",\"displayName\":\"Nickname\",\"type\":\"string\",\"target\":\"user\",\"required\":true,\"userEditable\":true,\"defaultValue\":\"to-remove\",\"validationRegex\":\"\",\"validationErrorMessage\":\"\"},{\"id\":\"8d081fd8-6a51-45a1-8051-04c3b043f5bd\",\"key\":\"elevatedRights\",\"displayName\":\"Elevated Rights\",\"type\":\"boolean\",\"target\":\"group\",\"required\":false,\"userEditable\":false,\"defaultValue\":\"\",\"validationRegex\":\"\",\"validationErrorMessage\":\"\"},{\"id\":\"3d7c6054-e146-48cb-b2d3-7d7897dcbc51\",\"key\":\"internalId\",\"displayName\":\"Internal ID\",\"type\":\"number\",\"target\":\"both\",\"required\":true,\"userEditable\":false,\"defaultValue\":\"0\",\"validationRegex\":\"\",\"validationErrorMessage\":\"\"}]"
|
||||
}
|
||||
],
|
||||
"kv": [
|
||||
|
||||
@@ -60,6 +60,53 @@ user_exists() {
|
||||
printf '%s' "$response" | jq -e --arg id "$1" '.data.users[]? | select(.id == $id)' >/dev/null
|
||||
}
|
||||
|
||||
schema_attribute_exists() {
|
||||
target="$1"
|
||||
name="$2"
|
||||
response="$(graphql '{schema{userSchema{attributes{name}} groupSchema{attributes{name}}}}' '{}')"
|
||||
|
||||
if [ "$target" = "user" ]; then
|
||||
printf '%s' "$response" | jq -e --arg name "$name" '.data.schema.userSchema.attributes[]? | select(.name == $name)' >/dev/null
|
||||
else
|
||||
printf '%s' "$response" | jq -e --arg name "$name" '.data.schema.groupSchema.attributes[]? | select(.name == $name)' >/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
create_schema_attribute() {
|
||||
target="$1"
|
||||
name="$2"
|
||||
attribute_type="$3"
|
||||
|
||||
if schema_attribute_exists "$target" "$name"; then
|
||||
echo "Schema attribute already exists: $target.$name"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$target" = "user" ]; then
|
||||
graphql "mutation {
|
||||
addUserAttribute(
|
||||
name: \"$name\"
|
||||
attributeType: $attribute_type
|
||||
isList: false
|
||||
isVisible: true
|
||||
isEditable: true
|
||||
) { ok }
|
||||
}" >/dev/null
|
||||
else
|
||||
graphql "mutation {
|
||||
addGroupAttribute(
|
||||
name: \"$name\"
|
||||
attributeType: $attribute_type
|
||||
isList: false
|
||||
isVisible: true
|
||||
isEditable: true
|
||||
) { ok }
|
||||
}" >/dev/null
|
||||
fi
|
||||
|
||||
echo "Created schema attribute: $target.$name"
|
||||
}
|
||||
|
||||
create_user() {
|
||||
id="$1"
|
||||
email="$2"
|
||||
@@ -67,6 +114,7 @@ create_user() {
|
||||
first_name="$4"
|
||||
last_name="$5"
|
||||
password="$6"
|
||||
department="$7"
|
||||
|
||||
variables="$(
|
||||
jq -cn \
|
||||
@@ -75,7 +123,18 @@ create_user() {
|
||||
--arg displayName "$display_name" \
|
||||
--arg firstName "$first_name" \
|
||||
--arg lastName "$last_name" \
|
||||
'{user: {id: $id, email: $email, displayName: $displayName, firstName: $firstName, lastName: $lastName, avatar: ""}}'
|
||||
--arg department "$department" \
|
||||
'{user: {
|
||||
id: $id,
|
||||
email: $email,
|
||||
displayName: $displayName,
|
||||
firstName: $firstName,
|
||||
lastName: $lastName,
|
||||
avatar: "",
|
||||
attributes: [
|
||||
{name: "department", value: [$department]}
|
||||
]
|
||||
}}'
|
||||
)"
|
||||
|
||||
graphql 'mutation createUser($user:CreateUserInput!){createUser(user:$user){id}}' "$variables" >/dev/null
|
||||
@@ -112,13 +171,35 @@ update_group_display_name() {
|
||||
jq -cn \
|
||||
--argjson id "$group_id" \
|
||||
--arg displayName "$display_name" \
|
||||
'{group: {id: $id, insertAttributes: {name: "display_name", value: $displayName}}}'
|
||||
'{group: {id: $id, insertAttributes: {name: "display_name", value: [$displayName]}}}'
|
||||
)"
|
||||
|
||||
graphql 'mutation updateGroup($group:UpdateGroupInput!){updateGroup(group:$group){ok}}' "$variables" >/dev/null
|
||||
echo "Attribute set for group: $name, attribute: display_name, value: $display_name"
|
||||
}
|
||||
|
||||
update_group_elevated_rights() {
|
||||
name="$1"
|
||||
elevated_rights="$2"
|
||||
group_id="$(get_group_id "$name")"
|
||||
|
||||
variables="$(
|
||||
jq -cn \
|
||||
--argjson id "$group_id" \
|
||||
--arg elevatedRights "$elevated_rights" \
|
||||
'{group: {
|
||||
id: $id,
|
||||
insertAttributes: {
|
||||
name: "elevatedRights",
|
||||
value: [$elevatedRights]
|
||||
}
|
||||
}}'
|
||||
)"
|
||||
|
||||
graphql 'mutation updateGroup($group:UpdateGroupInput!){updateGroup(group:$group){ok}}' "$variables" >/dev/null
|
||||
echo "Attribute set for group: $name, attribute: elevatedRights, value: $elevated_rights"
|
||||
}
|
||||
|
||||
add_user_to_group() {
|
||||
user_id="$1"
|
||||
group_name="$2"
|
||||
@@ -174,6 +255,10 @@ done
|
||||
|
||||
login
|
||||
|
||||
echo "Setting up LLDAP schema..."
|
||||
create_schema_attribute "user" "department" "STRING"
|
||||
create_schema_attribute "group" "elevatedRights" "STRING"
|
||||
|
||||
echo "Checking if data is already seeded..."
|
||||
if user_exists "testuser1"; then
|
||||
echo "Data already seeded, skipping setup."
|
||||
@@ -183,17 +268,19 @@ fi
|
||||
echo "Setting up LLDAP test data..."
|
||||
|
||||
echo "Creating test users..."
|
||||
create_user "testuser1" "testuser1@pocket-id.org" "Test User 1" "Test" "User" "password123"
|
||||
create_user "testuser2" "testuser2@pocket-id.org" "Test User 2" "Test2" "User2" "password123"
|
||||
create_user "testuser1" "testuser1@pocket-id.org" "Test User 1" "Test" "User" "password123" "Engineering"
|
||||
create_user "testuser2" "testuser2@pocket-id.org" "Test User 2" "Test2" "User2" "password123" "Operations"
|
||||
|
||||
echo "Creating test groups..."
|
||||
create_group "test_group"
|
||||
sleep 1
|
||||
update_group_display_name "test_group" "test_group"
|
||||
update_group_elevated_rights "test_group" "false"
|
||||
|
||||
create_group "admin_group"
|
||||
sleep 1
|
||||
update_group_display_name "admin_group" "admin_group"
|
||||
update_group_elevated_rights "admin_group" "true"
|
||||
|
||||
echo "Adding users to groups..."
|
||||
add_user_to_group_with_retry "testuser1" "test_group"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import test, { expect } from '@playwright/test';
|
||||
import { emailVerificationTokens, users } from '../data';
|
||||
import { customFields, emailVerificationTokens, users } from '../data';
|
||||
import authUtil from '../utils/auth.util';
|
||||
import { cleanupBackend } from '../utils/cleanup.util';
|
||||
import passkeyUtil from '../utils/passkey.util';
|
||||
@@ -22,6 +22,31 @@ test('Update account details', async ({ page }) => {
|
||||
);
|
||||
});
|
||||
|
||||
test('Update self editable custom fields from account settings', async ({ page }) => {
|
||||
await page.goto('/settings/account');
|
||||
|
||||
await expect(page.getByLabel(customFields.internalId.displayName)).not.toBeVisible();
|
||||
|
||||
await page.getByLabel(customFields.department.displayName).fill('Engineering3');
|
||||
await page.getByLabel(customFields.nickname.displayName).fill('');
|
||||
await page.getByRole('button', { name: 'Save' }).first().click();
|
||||
await expect(page.getByText('Department must only contain letters')).toBeVisible();
|
||||
await expect(page.getByText('Field is required')).toBeVisible();
|
||||
|
||||
await page.getByLabel(customFields.department.displayName).fill('Design');
|
||||
await page.getByLabel(customFields.nickname.displayName).fill('Timmy');
|
||||
await page.getByRole('button', { name: 'Save' }).first().click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||
'Account details updated successfully'
|
||||
);
|
||||
|
||||
await page.reload();
|
||||
|
||||
await expect(page.getByLabel(customFields.department.displayName)).toHaveValue('Design');
|
||||
await expect(page.getByLabel(customFields.nickname.displayName)).toHaveValue('Timmy');
|
||||
});
|
||||
|
||||
test('Update account details fails with already taken email', async ({ page }) => {
|
||||
await page.goto('/settings/account');
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
import { cleanupBackend } from '../utils/cleanup.util';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -31,14 +31,15 @@ test('Update general configuration', async ({ page }) => {
|
||||
|
||||
test.describe('Update user creation configuration', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Expand card' }).nth(1).click();
|
||||
await page.getByRole('tab', { name: 'Users and groups' }).click();
|
||||
await page.getByRole('button', { name: 'Expand card' }).first().click();
|
||||
});
|
||||
|
||||
test('should save sign up mode', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Enable User Signups' }).click();
|
||||
await page.getByRole('option', { name: 'Open Signup' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).nth(1).click();
|
||||
await page.getByRole('button', { name: 'Save' }).last().click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]').last()).toHaveText(
|
||||
'User creation settings updated successfully.'
|
||||
@@ -55,7 +56,7 @@ test.describe('Update user creation configuration', () => {
|
||||
await page.getByRole('option', { name: 'Designers' }).click();
|
||||
await page.getByRole('combobox', { name: 'User Groups' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).nth(1).click();
|
||||
await page.getByRole('button', { name: 'Save' }).last().click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]').last()).toHaveText(
|
||||
'User creation settings updated successfully.'
|
||||
@@ -69,31 +70,131 @@ test.describe('Update user creation configuration', () => {
|
||||
await expect(page.getByRole('option', { name: 'Designers' })).toBeChecked();
|
||||
});
|
||||
|
||||
test('should save default custom claims for new signups', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Add custom claim' }).click();
|
||||
await page.getByPlaceholder('Key').fill('test-claim');
|
||||
await page.getByPlaceholder('Value').fill('test-value');
|
||||
await page.getByRole('button', { name: 'Add another' }).click();
|
||||
await page.getByPlaceholder('Key').nth(1).fill('another-claim');
|
||||
await page.getByPlaceholder('Value').nth(1).fill('another-value');
|
||||
test('should create, edit, validate, and delete custom fields', async ({ page }) => {
|
||||
await openCustomFieldsCard(page);
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).nth(1).click();
|
||||
await addCustomField(page, {
|
||||
displayName: 'Employee code',
|
||||
key: 'employeeCode',
|
||||
defaultValue: 'WRONG',
|
||||
validationRegex: '^EMP-[0-9]+$',
|
||||
validationErrorMessage: 'Use EMP-###'
|
||||
});
|
||||
await expect(page.getByText('Use EMP-###')).toBeVisible();
|
||||
await page.getByLabel('Default value').fill('EMP-001');
|
||||
await page.getByRole('button', { name: 'Save' }).last().click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]').last()).toHaveText(
|
||||
'User creation settings updated successfully.'
|
||||
'Custom fields updated successfully'
|
||||
);
|
||||
|
||||
await addCustomField(page, {
|
||||
displayName: 'Weekly hours',
|
||||
key: 'weeklyHours',
|
||||
type: 'Number',
|
||||
target: 'Users and groups',
|
||||
required: true,
|
||||
defaultValue: '40'
|
||||
});
|
||||
|
||||
await expect(page.locator('[data-type="success"]').last()).toHaveText(
|
||||
'Custom fields updated successfully'
|
||||
);
|
||||
await expect(page.getByRole('row', { name: /Employee code/ })).toBeVisible();
|
||||
await expect(page.getByRole('row', { name: /Weekly hours/ })).toBeVisible();
|
||||
|
||||
await page.getByRole('row', { name: /Employee code/ }).click();
|
||||
await expect(page.getByLabel('Type')).toBeDisabled();
|
||||
await page.getByLabel('Display Name').fill('Employee code updated');
|
||||
await page.getByRole('button', { name: 'Save' }).last().click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]').last()).toHaveText(
|
||||
'Custom fields updated successfully'
|
||||
);
|
||||
|
||||
const weeklyHoursRow = page.getByRole('row', { name: /Weekly hours/ });
|
||||
await weeklyHoursRow.getByRole('button').click();
|
||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||
await page.getByRole('alertdialog').getByRole('button', { name: 'Delete' }).click();
|
||||
await expect(page.locator('[data-type="success"]').last()).toHaveText(
|
||||
'Custom fields updated successfully'
|
||||
);
|
||||
|
||||
await page.reload();
|
||||
|
||||
await expect(page.getByPlaceholder('Key').first()).toHaveValue('test-claim');
|
||||
await expect(page.getByPlaceholder('Value').first()).toHaveValue('test-value');
|
||||
await expect(page.getByPlaceholder('Key').nth(1)).toHaveValue('another-claim');
|
||||
await expect(page.getByPlaceholder('Value').nth(1)).toHaveValue('another-value');
|
||||
await page.getByRole('tab', { name: 'Users and groups' }).click();
|
||||
await openCustomFieldsCard(page, false);
|
||||
await expect(page.getByText('Employee code updated')).toBeVisible();
|
||||
await expect(page.getByText('Weekly hours')).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
async function openCustomFieldsCard(page: Page, navigate = true) {
|
||||
if (navigate) {
|
||||
await page.goto('/settings/admin/application-configuration');
|
||||
await page.getByRole('tab', { name: 'Users and groups' }).click();
|
||||
}
|
||||
const addButton = page.getByRole('button', { name: 'Add custom field' });
|
||||
if (!(await addButton.isVisible().catch(() => false))) {
|
||||
await page.getByRole('button', { name: 'Expand card' }).nth(1).click();
|
||||
}
|
||||
await expect(addButton).toBeVisible();
|
||||
}
|
||||
|
||||
async function addCustomField(
|
||||
page: Page,
|
||||
field: {
|
||||
displayName: string;
|
||||
key: string;
|
||||
type?: 'Text' | 'Number' | 'Boolean';
|
||||
target?: 'Users' | 'Groups' | 'Users and groups';
|
||||
required?: boolean;
|
||||
userEditable?: boolean;
|
||||
defaultValue?: string;
|
||||
validationRegex?: string;
|
||||
validationErrorMessage?: string;
|
||||
}
|
||||
) {
|
||||
await page.getByRole('button', { name: 'Add custom field' }).click();
|
||||
await page.getByLabel('Display Name').fill(field.displayName);
|
||||
await page.getByLabel('Key').fill(field.key);
|
||||
|
||||
if (field.type && field.type !== 'Text') {
|
||||
await page.getByLabel('Type').click();
|
||||
await page.getByRole('option', { name: field.type, exact: true }).click();
|
||||
}
|
||||
if (field.target && field.target !== 'Users') {
|
||||
await page.getByLabel('Available for').click();
|
||||
await page.getByRole('option', { name: field.target, exact: true }).click();
|
||||
}
|
||||
if (field.required) {
|
||||
await page.getByLabel('Required').click();
|
||||
}
|
||||
if (field.userEditable) {
|
||||
await page.getByLabel('User editable').click();
|
||||
}
|
||||
if (field.defaultValue !== undefined) {
|
||||
if (field.type === 'Boolean') {
|
||||
await page.getByLabel('Default value').click();
|
||||
await page
|
||||
.getByRole('option', { name: field.defaultValue === 'true' ? 'Yes' : 'No', exact: true })
|
||||
.click();
|
||||
} else {
|
||||
await page.getByLabel('Default value').fill(field.defaultValue);
|
||||
}
|
||||
}
|
||||
if (field.validationRegex) {
|
||||
await page.getByLabel('Validation regex').fill(field.validationRegex);
|
||||
}
|
||||
if (field.validationErrorMessage) {
|
||||
await page.getByLabel('Validation error message').fill(field.validationErrorMessage);
|
||||
}
|
||||
await page.getByRole('button', { name: 'Save' }).last().click();
|
||||
}
|
||||
|
||||
test('Update email configuration', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Expand card' }).nth(2).click();
|
||||
await page.getByRole('tab', { name: 'Email' }).click();
|
||||
await page.getByRole('button', { name: 'Expand card' }).first().click();
|
||||
|
||||
await page.getByLabel('SMTP Host').fill('smtp.gmail.com');
|
||||
await page.getByLabel('SMTP Port').fill('587');
|
||||
@@ -105,7 +206,7 @@ test('Update email configuration', async ({ page }) => {
|
||||
await page.getByLabel('Email Login Code from Admin').click();
|
||||
await page.getByLabel('API Key Expiration').click();
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).nth(1).click();
|
||||
await page.getByRole('button', { name: 'Save' }).last().click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||
'Email configuration updated successfully'
|
||||
@@ -126,7 +227,7 @@ test('Update email configuration', async ({ page }) => {
|
||||
|
||||
test.describe('Update application images', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Expand card' }).nth(4).click();
|
||||
await page.getByRole('button', { name: 'Expand card' }).last().click();
|
||||
});
|
||||
|
||||
test('should upload images', async ({ page }) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import test, { expect } from '@playwright/test';
|
||||
import { customFields } from 'data';
|
||||
import { cleanupBackend } from '../utils/cleanup.util';
|
||||
|
||||
test.beforeEach(async () => await cleanupBackend());
|
||||
@@ -12,7 +13,8 @@ test.describe('LDAP Integration', () => {
|
||||
test('LDAP configuration is working properly', async ({ page }) => {
|
||||
await page.goto('/settings/admin/application-configuration');
|
||||
|
||||
await page.getByRole('button', { name: 'Expand card' }).nth(3).click();
|
||||
await page.getByRole('tab', { name: 'Users and groups' }).click();
|
||||
await page.getByRole('button', { name: 'Expand card' }).nth(2).click();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Disable', exact: true })).toBeVisible();
|
||||
await expect(page.getByLabel('LDAP URL')).toHaveValue(/ldap:\/\/.*/);
|
||||
@@ -66,6 +68,25 @@ test.describe('LDAP Integration', () => {
|
||||
await expect(page.getByText('LDAP').first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('LDAP custom fields are synced into users and groups', async ({ page }) => {
|
||||
await page.goto('/settings/admin/users');
|
||||
await page.getByRole('row', { name: 'testuser1' }).getByRole('button').click();
|
||||
await page.getByRole('menuitem', { name: 'Edit' }).click();
|
||||
|
||||
await expect(page.getByLabel(customFields.department.displayName)).toHaveValue('Engineering');
|
||||
|
||||
await page.goto('/settings/admin/user-groups');
|
||||
await page
|
||||
.getByRole('row', { name: 'admin_group' })
|
||||
.getByRole('button', { name: 'Toggle menu' })
|
||||
.click();
|
||||
await page.getByRole('menuitem', { name: 'Edit' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('switch', { name: customFields.elevatedRights.displayName })
|
||||
).toBeChecked();
|
||||
});
|
||||
|
||||
test('LDAP users cannot be modified in PocketID', async ({ page }) => {
|
||||
// Navigate to LDAP user details
|
||||
await page.goto('/settings/admin/users');
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import test, { expect, type Page, type Request } from '@playwright/test';
|
||||
import { oidcClients, refreshTokens, users } from '../data';
|
||||
import * as jose from 'jose';
|
||||
import { customFields, oidcClients, refreshTokens, userGroups, users } from '../data';
|
||||
import { cleanupBackend } from '../utils/cleanup.util';
|
||||
import {
|
||||
updateUserCustomFieldsViaApi,
|
||||
updateUserGroupCustomFieldsViaApi
|
||||
} from '../utils/custom-fields.util';
|
||||
import { generateIdToken, generateOauthAccessToken } from '../utils/jwt.util';
|
||||
import * as oidcUtil from '../utils/oidc.util';
|
||||
import passkeyUtil from '../utils/passkey.util';
|
||||
@@ -130,7 +135,11 @@ test('End session without id token hint shows confirmation page', async ({ page
|
||||
|
||||
test('End session with id token hint redirects to callback URL', async ({ page }) => {
|
||||
const client = oidcClients.nextcloud;
|
||||
const idToken = await generateIdToken("fe81c12a-7336-4aee-bebc-d901a873bf48", users.tim, client.id);
|
||||
const idToken = await generateIdToken(
|
||||
'fe81c12a-7336-4aee-bebc-d901a873bf48',
|
||||
users.tim,
|
||||
client.id
|
||||
);
|
||||
let redirectedCorrectly = false;
|
||||
await page
|
||||
.goto(
|
||||
@@ -190,6 +199,51 @@ test('Successfully refresh tokens with valid refresh token', async ({ request })
|
||||
expect(tokenData.refresh_token).not.toBe(token);
|
||||
});
|
||||
|
||||
test('ID token includes configured custom fields when profile scope is requested', async ({
|
||||
page
|
||||
}) => {
|
||||
await updateUserCustomFieldsViaApi(page, users.tim.id, [
|
||||
{ customFieldId: customFields.department.id, value: 'Engineering' },
|
||||
{ customFieldId: customFields.nickname.id, value: 'Timi' }
|
||||
]);
|
||||
await updateUserGroupCustomFieldsViaApi(page, userGroups.designers.id, [
|
||||
{ customFieldId: customFields.elevatedRights.id, value: 'true' }
|
||||
]);
|
||||
|
||||
const { token, clientId, userId } = refreshTokens.filter(
|
||||
(refreshToken) => !refreshToken.expired
|
||||
)[0];
|
||||
const refreshToken = await page.request
|
||||
.post('/api/test/refreshtoken', {
|
||||
data: {
|
||||
rt: token,
|
||||
client: clientId,
|
||||
user: userId
|
||||
}
|
||||
})
|
||||
.then((r) => r.text());
|
||||
|
||||
const refreshResponse = await page.request.post('/api/oidc/token', {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
form: {
|
||||
grant_type: 'refresh_token',
|
||||
client_id: clientId,
|
||||
refresh_token: refreshToken,
|
||||
client_secret: oidcClients.nextcloud.secret
|
||||
}
|
||||
});
|
||||
|
||||
expect(refreshResponse.ok()).toBeTruthy();
|
||||
const tokenData = await refreshResponse.json();
|
||||
const idToken = jose.decodeJwt(tokenData.id_token);
|
||||
|
||||
expect(idToken.department).toBe('Engineering');
|
||||
expect(idToken.nickname).toBe('Timi');
|
||||
expect(idToken.elevatedRights).toBe(true);
|
||||
});
|
||||
|
||||
test('Refresh token fails when used for the wrong client', async ({ request }) => {
|
||||
const { token, clientId, userId } = refreshTokens.filter((token) => !token.expired)[0];
|
||||
const clientSecret = 'w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import test, { expect } from '@playwright/test';
|
||||
import { oidcClients, userGroups, users } from '../data';
|
||||
import { customFields, oidcClients, userGroups, users } from '../data';
|
||||
import { cleanupBackend } from '../utils/cleanup.util';
|
||||
|
||||
test.beforeEach(async () => await cleanupBackend());
|
||||
@@ -74,56 +74,29 @@ test('Delete user group', async ({ page }) => {
|
||||
await expect(page.getByRole('row', { name: group.name })).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('Update user group custom claims', async ({ page }) => {
|
||||
test('Update user group custom fields', async ({ page }) => {
|
||||
await page.goto(`/settings/admin/user-groups/${userGroups.designers.id}`);
|
||||
|
||||
await page.getByRole('button', { name: 'Expand card' }).first().click();
|
||||
await expect(
|
||||
page.getByRole('switch', { name: customFields.elevatedRights.displayName })
|
||||
).not.toBeChecked();
|
||||
|
||||
// Add two custom claims
|
||||
await page.getByRole('button', { name: 'Add custom claim' }).click();
|
||||
await page.getByRole('switch', { name: customFields.elevatedRights.displayName }).click();
|
||||
await page.getByRole('button', { name: 'Save' }).first().click();
|
||||
|
||||
await page.getByPlaceholder('Key').fill('customClaim1');
|
||||
await page.getByPlaceholder('Value').fill('customClaim1_value');
|
||||
|
||||
await page.getByRole('button', { name: 'Add another' }).click();
|
||||
await page.getByPlaceholder('Key').nth(1).fill('customClaim2');
|
||||
await page.getByPlaceholder('Value').nth(1).fill('customClaim2_value');
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).nth(2).click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||
'Custom claims updated successfully'
|
||||
);
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText('User group updated successfully');
|
||||
|
||||
await page.reload();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Check if custom claims are saved
|
||||
await expect(page.getByPlaceholder('Key').first()).toHaveValue('customClaim1');
|
||||
await expect(page.getByPlaceholder('Value').first()).toHaveValue('customClaim1_value');
|
||||
await expect(page.getByPlaceholder('Key').nth(1)).toHaveValue('customClaim2');
|
||||
await expect(page.getByPlaceholder('Value').nth(1)).toHaveValue('customClaim2_value');
|
||||
|
||||
// Remove one custom claim
|
||||
await page.getByLabel('Remove custom claim').first().click();
|
||||
await page.getByRole('button', { name: 'Save' }).nth(2).click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||
'Custom claims updated successfully'
|
||||
);
|
||||
|
||||
await page.reload();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Check if custom claim is removed
|
||||
await expect(page.getByPlaceholder('Key').first()).toHaveValue('customClaim2');
|
||||
await expect(page.getByPlaceholder('Value').first()).toHaveValue('customClaim2_value');
|
||||
await expect(
|
||||
page.getByRole('switch', { name: customFields.elevatedRights.displayName })
|
||||
).toBeChecked();
|
||||
});
|
||||
|
||||
test('Update user group allowed user groups', async ({ page }) => {
|
||||
await page.goto(`/settings/admin/user-groups/${userGroups.designers.id}`);
|
||||
|
||||
await page.getByRole('button', { name: 'Expand card' }).nth(1).click();
|
||||
await page.getByRole('button', { name: 'Expand card' }).click();
|
||||
|
||||
// Unrestricted OIDC clients should be checked and disabled
|
||||
const nextcloudRow = page
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import test, { expect } from '@playwright/test';
|
||||
import { userGroups, users } from '../data';
|
||||
import { customFields, userGroups, users } from '../data';
|
||||
import authUtil from '../utils/auth.util';
|
||||
import { cleanupBackend } from '../utils/cleanup.util';
|
||||
|
||||
@@ -184,48 +184,27 @@ test('Update user fails with already taken username in different casing', async
|
||||
await expect(page.locator('[data-type="error"]')).toHaveText('Username is already in use');
|
||||
});
|
||||
|
||||
test('Update user custom claims', async ({ page }) => {
|
||||
test('Update user custom fields', async ({ page }) => {
|
||||
await page.goto(`/settings/admin/users/${users.craig.id}`);
|
||||
|
||||
await page.getByRole('button', { name: 'Expand card' }).nth(1).click();
|
||||
await page.getByLabel(customFields.department.displayName).fill('Engineering3');
|
||||
await page.getByLabel(customFields.nickname.displayName).fill('');
|
||||
await page.getByRole('button', { name: 'Save' }).first().click();
|
||||
await expect(page.getByText('Department must only contain letters')).toBeVisible();
|
||||
await expect(page.getByText('Field is required')).toBeVisible();
|
||||
|
||||
// Add two custom claims
|
||||
await page.getByRole('button', { name: 'Add custom claim' }).click();
|
||||
await page.getByLabel(customFields.department.displayName).fill('Design');
|
||||
await page.getByLabel(customFields.internalId.displayName).fill('123');
|
||||
await page.getByLabel(customFields.nickname.displayName).fill('Timmy');
|
||||
await page.getByRole('button', { name: 'Save' }).first().click();
|
||||
|
||||
await page.getByPlaceholder('Key').fill('customClaim1');
|
||||
await page.getByPlaceholder('Value').fill('customClaim1_value');
|
||||
|
||||
await page.getByRole('button', { name: 'Add another' }).click();
|
||||
await page.getByPlaceholder('Key').nth(1).fill('customClaim2');
|
||||
await page.getByPlaceholder('Value').nth(1).fill('customClaim2_value');
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).nth(1).click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||
'Custom claims updated successfully'
|
||||
);
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText('User updated successfully');
|
||||
|
||||
await page.reload();
|
||||
|
||||
// Check if custom claims are saved
|
||||
await expect(page.getByPlaceholder('Key').first()).toHaveValue('customClaim1');
|
||||
await expect(page.getByPlaceholder('Value').first()).toHaveValue('customClaim1_value');
|
||||
await expect(page.getByPlaceholder('Key').nth(1)).toHaveValue('customClaim2');
|
||||
await expect(page.getByPlaceholder('Value').nth(1)).toHaveValue('customClaim2_value');
|
||||
|
||||
// Remove one custom claim
|
||||
await page.getByLabel('Remove custom claim').first().click();
|
||||
await page.getByRole('button', { name: 'Save' }).nth(1).click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||
'Custom claims updated successfully'
|
||||
);
|
||||
|
||||
await page.reload();
|
||||
|
||||
// Check if custom claim is removed
|
||||
await expect(page.getByPlaceholder('Key').first()).toHaveValue('customClaim2');
|
||||
await expect(page.getByPlaceholder('Value').first()).toHaveValue('customClaim2_value');
|
||||
await expect(page.getByLabel(customFields.department.displayName)).toHaveValue('Design');
|
||||
await expect(page.getByLabel(customFields.nickname.displayName)).toHaveValue('Timmy');
|
||||
await expect(page.getByLabel(customFields.internalId.displayName)).toHaveValue('123');
|
||||
});
|
||||
|
||||
test('Update user group assignments', async ({ page }) => {
|
||||
@@ -263,7 +242,11 @@ test('Admin can view another user passkeys', async ({ page }) => {
|
||||
test('Admin can delete another user passkey and audit log is created', async ({ page }) => {
|
||||
await page.goto(`/settings/admin/users/${users.craig.id}`);
|
||||
|
||||
await page.locator('[data-slot="item"]').filter({ hasText: 'Passkey 2' }).getByLabel('Delete').click();
|
||||
await page
|
||||
.locator('[data-slot="item"]')
|
||||
.filter({ hasText: 'Passkey 2' })
|
||||
.getByLabel('Delete')
|
||||
.click();
|
||||
await page.getByRole('alertdialog').getByRole('button', { name: 'Delete' }).click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]')).toHaveText('Passkey deleted successfully');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import test, { expect, type Page } from '@playwright/test';
|
||||
import { signupTokens, userGroups, users } from '../data';
|
||||
import { customFields, signupTokens, userGroups, users } from '../data';
|
||||
import { cleanupBackend } from '../utils/cleanup.util';
|
||||
import passkeyUtil from '../utils/passkey.util';
|
||||
|
||||
@@ -10,10 +10,11 @@ async function setSignupMode(
|
||||
) {
|
||||
await page.goto('/settings/admin/application-configuration');
|
||||
|
||||
await page.getByRole('button', { name: 'Expand card' }).nth(1).click();
|
||||
await page.getByRole('tab', { name: 'Users and groups' }).click();
|
||||
await page.getByRole('button', { name: 'Expand card' }).first().click();
|
||||
await page.getByRole('button', { name: 'Enable User Signups' }).click();
|
||||
await page.getByRole('option', { name: mode }).click();
|
||||
await page.getByRole('button', { name: 'Save' }).nth(1).click();
|
||||
await page.getByRole('button', { name: 'Save' }).last().click();
|
||||
|
||||
await expect(page.locator('[data-type="success"]').last()).toHaveText(
|
||||
'User creation settings updated successfully.'
|
||||
@@ -117,7 +118,7 @@ test.describe('User Signup', () => {
|
||||
await expect(page.getByText('Set up your passkey')).toBeVisible();
|
||||
|
||||
const response = await page.request.get('/api/users/me').then((res) => res.json());
|
||||
expect(response.userGroups.map((g) => g.id)).toContain(userGroups.developers.id);
|
||||
expect(response.userGroups.map((g: any) => g.id)).toContain(userGroups.developers.id);
|
||||
});
|
||||
|
||||
test('Signup with token - invalid token shows error', async ({ page }) => {
|
||||
@@ -161,6 +162,32 @@ test.describe('User Signup', () => {
|
||||
await expect(page.getByText('Set up your passkey')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Open signup - required custom fields are shown and saved', async ({ page }) => {
|
||||
await setSignupMode(page, 'Open Signup', false);
|
||||
await page.context().clearCookies();
|
||||
|
||||
await page.goto('/signup');
|
||||
|
||||
await expect(page.getByLabel(customFields.nickname.displayName)).toBeVisible();
|
||||
await expect(page.getByLabel(customFields.elevatedRights.displayName)).not.toBeVisible();
|
||||
await expect(page.getByLabel(customFields.department.displayName)).not.toBeVisible();
|
||||
await expect(page.getByLabel(customFields.internalId.displayName)).not.toBeVisible();
|
||||
|
||||
await page.getByLabel('First name').fill('Jane');
|
||||
await page.getByLabel('Last name').fill('Smith');
|
||||
await page.getByLabel('Username').fill('janecustomfield');
|
||||
await page.getByLabel('Email').fill('jane.customfield@test.com');
|
||||
await page.getByLabel(customFields.nickname.displayName).fill('123');
|
||||
|
||||
await page.getByRole('button', { name: 'Sign Up' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Skip for now' }).click();
|
||||
await page.getByRole('button', { name: 'Skip for now' }).nth(1).click();
|
||||
|
||||
await page.waitForURL('/settings/account');
|
||||
await expect(page.getByLabel(customFields.nickname.displayName)).toHaveValue('123');
|
||||
});
|
||||
|
||||
test('Open signup - validation errors', async ({ page }) => {
|
||||
await setSignupMode(page, 'Open Signup');
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { expect, type Page } from '@playwright/test';
|
||||
|
||||
export type CustomField = {
|
||||
id: string;
|
||||
key: string;
|
||||
displayName: string;
|
||||
type: 'string' | 'number' | 'boolean';
|
||||
target: 'user' | 'group' | 'both';
|
||||
required: boolean;
|
||||
userEditable: boolean;
|
||||
defaultValue?: string;
|
||||
validationRegex?: string;
|
||||
validationErrorMessage?: string;
|
||||
};
|
||||
|
||||
export type CustomFieldValue = {
|
||||
customFieldId: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export async function updateUserCustomFieldsViaApi(
|
||||
page: Page,
|
||||
userId: string,
|
||||
customFieldValues: CustomFieldValue[]
|
||||
) {
|
||||
const userResponse = await page.request.get(`/api/users/${userId}`);
|
||||
expect(userResponse.ok()).toBeTruthy();
|
||||
const user = await userResponse.json();
|
||||
|
||||
const updateResponse = await page.request.put(`/api/users/${userId}`, {
|
||||
data: {
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
displayName: user.displayName,
|
||||
email: user.email,
|
||||
emailVerified: user.emailVerified,
|
||||
username: user.username,
|
||||
isAdmin: user.isAdmin,
|
||||
disabled: user.disabled,
|
||||
customFieldValues
|
||||
}
|
||||
});
|
||||
expect(updateResponse.ok()).toBeTruthy();
|
||||
}
|
||||
|
||||
export async function updateUserGroupCustomFieldsViaApi(
|
||||
page: Page,
|
||||
userGroupId: string,
|
||||
customFieldValues: CustomFieldValue[]
|
||||
) {
|
||||
const userGroupResponse = await page.request.get(`/api/user-groups/${userGroupId}`);
|
||||
expect(userGroupResponse.ok()).toBeTruthy();
|
||||
const userGroup = await userGroupResponse.json();
|
||||
|
||||
const updateResponse = await page.request.put(`/api/user-groups/${userGroupId}`, {
|
||||
data: {
|
||||
friendlyName: userGroup.friendlyName,
|
||||
name: userGroup.name,
|
||||
ldapId: userGroup.ldapId,
|
||||
customFieldValues
|
||||
}
|
||||
});
|
||||
expect(updateResponse.ok()).toBeTruthy();
|
||||
}
|
||||
Reference in New Issue
Block a user