Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -67,25 +67,178 @@ describe('Update profile routes', () => {
it('correctly defines route.', () => {
const bodySchema = (routeConfig.validate as any).body as ObjectType;
expect(() => bodySchema.validate(0)).toThrowErrorMatchingInlineSnapshot(
`"expected value of type [object] but got [number]"`
`"expected a plain object value, but found [number] instead."`
);
expect(() => bodySchema.validate('avatar')).toThrowErrorMatchingInlineSnapshot(
`"could not parse record value from json input"`
`"could not parse object value from json input"`
);
expect(() => bodySchema.validate(true)).toThrowErrorMatchingInlineSnapshot(
`"expected value of type [object] but got [boolean]"`
);
expect(() => bodySchema.validate(null)).toThrowErrorMatchingInlineSnapshot(
`"expected value of type [object] but got [null]"`
);
expect(() => bodySchema.validate(undefined)).toThrowErrorMatchingInlineSnapshot(
`"expected value of type [object] but got [undefined]"`
`"expected a plain object value, but found [boolean] instead."`
);

expect(bodySchema.validate({})).toEqual({});
expect(
bodySchema.validate({ title: 'some-title', content: { deepProperty: { type: 'basic' } } })
).toEqual({ title: 'some-title', content: { deepProperty: { type: 'basic' } } });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: can we also add tests for newly added constraints (enums, string length and body size)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in d7d24f9

bodySchema.validate({
avatar: { initials: 'some-initials', color: 'some-color', imageUrl: 'some-image-url' },
userSettings: { darkMode: 'dark', contrastMode: 'high' },
})
).toEqual({
avatar: { initials: 'some-initials', color: 'some-color', imageUrl: 'some-image-url' },
userSettings: { darkMode: 'dark', contrastMode: 'high' },
});
});

it('rejects invalid darkMode enum values.', () => {
const bodySchema = (routeConfig.validate as any).body as ObjectType;

// Valid values should pass
expect(
bodySchema.validate({
userSettings: { darkMode: 'system' },
})
).toEqual({ userSettings: { darkMode: 'system' } });

expect(
bodySchema.validate({
userSettings: { darkMode: 'dark' },
})
).toEqual({ userSettings: { darkMode: 'dark' } });

expect(
bodySchema.validate({
userSettings: { darkMode: 'light' },
})
).toEqual({ userSettings: { darkMode: 'light' } });

expect(
bodySchema.validate({
userSettings: { darkMode: 'space_default' },
})
).toEqual({ userSettings: { darkMode: 'space_default' } });

// Invalid values should fail
expect(() =>
bodySchema.validate({
userSettings: { darkMode: 'invalid' },
})
).toThrow();

expect(() =>
bodySchema.validate({
userSettings: { darkMode: 'INVALID' },
})
).toThrow();

expect(() =>
bodySchema.validate({
userSettings: { darkMode: 123 },
})
).toThrow();
});

it('rejects invalid contrastMode enum values.', () => {
const bodySchema = (routeConfig.validate as any).body as ObjectType;

// Valid values should pass
expect(
bodySchema.validate({
userSettings: { contrastMode: 'system' },
})
).toEqual({ userSettings: { contrastMode: 'system' } });

expect(
bodySchema.validate({
userSettings: { contrastMode: 'standard' },
})
).toEqual({ userSettings: { contrastMode: 'standard' } });

expect(
bodySchema.validate({
userSettings: { contrastMode: 'high' },
})
).toEqual({ userSettings: { contrastMode: 'high' } });

// Invalid values should fail
expect(() =>
bodySchema.validate({
userSettings: { contrastMode: 'invalid' },
})
).toThrow();

expect(() =>
bodySchema.validate({
userSettings: { contrastMode: 'INVALID' },
})
).toThrow();

expect(() =>
bodySchema.validate({
userSettings: { contrastMode: 123 },
})
).toThrow();
});

it('rejects avatar initials exceeding max length.', () => {
const bodySchema = (routeConfig.validate as any).body as ObjectType;
const MAX_STRING_FIELD_LENGTH = 1024;

// Valid length should pass
const validInitials = 'a'.repeat(MAX_STRING_FIELD_LENGTH);
expect(
bodySchema.validate({
avatar: { initials: validInitials },
})
).toEqual({ avatar: { color: null, imageUrl: null, initials: validInitials } });

const invalidInitials = 'a'.repeat(MAX_STRING_FIELD_LENGTH + 1);
expect(() =>
bodySchema.validate({
avatar: { initials: invalidInitials },
})
).toThrowErrorMatchingInlineSnapshot(`
"[avatar.initials]: types that failed validation:
- [avatar.initials.0]: value has length [1025] but it must have a maximum length of [1024].
- [avatar.initials.1]: expected value to equal [null]"
`);
});

it('rejects avatar color exceeding max length.', () => {
const bodySchema = (routeConfig.validate as any).body as ObjectType;
const MAX_STRING_FIELD_LENGTH = 1024;

const validColor = 'a'.repeat(MAX_STRING_FIELD_LENGTH);
expect(
bodySchema.validate({
avatar: { color: validColor },
})
).toEqual({ avatar: { color: validColor, imageUrl: null, initials: null } });

const invalidColor = 'a'.repeat(MAX_STRING_FIELD_LENGTH + 1);
expect(() =>
bodySchema.validate({
avatar: { color: invalidColor },
})
).toThrowErrorMatchingInlineSnapshot(`
"[avatar.color]: types that failed validation:
- [avatar.color.0]: value has length [1025] but it must have a maximum length of [1024].
- [avatar.color.1]: expected value to equal [null]"
`);
});

it('allows null values for avatar initials and color.', () => {
const bodySchema = (routeConfig.validate as any).body as ObjectType;

expect(
bodySchema.validate({
avatar: { initials: null, color: null },
})
).toEqual({ avatar: { imageUrl: null, initials: null, color: null } });
});

it('validates body size limit is configured correctly.', () => {
const MAX_USER_PROFILE_DATA_SIZE_BYTES = 1000 * 1024;

expect(routeConfig.options?.body?.maxBytes).toBe(MAX_USER_PROFILE_DATA_SIZE_BYTES);
});

it('fails if session is not found.', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,36 @@ const ALLOWED_KEYS_UPDATE_CLOUD = [
'solutionNavigationTour:completed', // TODO: remove with https://github.com/elastic/kibana/issues/239313
];

const MAX_STRING_FIELD_LENGTH = 1024;
Copy link
Copy Markdown
Contributor

@azasypkin azasypkin Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question/suggestion: Is there any particular reason or use case why we want to support 1024 characters for initials and color instead of, let's say, 100? We can technically go even lower and align with the limits in UI, but I don't have a strong opinion.

If we have a user that has anything in the user profile that's larger than these limits, nothing would break except they'll have to obey the new limits if they decide to update the data, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we have a user that has anything in the user profile that's larger than these limits, nothing would break except they'll have to obey the new limits if they decide to update the data, right?

That's right. It shouldn't affect any existing data since it's just validation on save.

No particular reason for 1024. Matching the UI limits fromm the form field works for me too.


const MAX_USER_PROFILE_DATA_SIZE_BYTES = 1000 * 1024;

const userProfileUpdateSchema = schema.object({
avatar: schema.maybe(
schema.object({
initials: schema.nullable(schema.string({ maxLength: MAX_STRING_FIELD_LENGTH })),
color: schema.nullable(schema.string({ maxLength: MAX_STRING_FIELD_LENGTH })),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: not really related to this PR as it was like that before, just observation, it looks like we're too aggressive in enforcing a valid avatar color to a degree that one cannot even access their profile to fix the issue (just bypassed UI restriction via dev tools and saved invalid color) 🤦

Image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, i hadn't come across this. I'll create an issue for us to look into.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue created: #245085

imageUrl: schema.nullable(schema.string()),
Comment on lines +30 to +33
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added some defaults for maxLength for these fields.

@azasypkin (tagging you since i saw you assigned to the review 😅) - I'm not sure how to approach restricting imageUrl length.

})
),
userSettings: schema.maybe(
schema.object({
darkMode: schema.maybe(
schema.oneOf([
schema.literal('system'),
schema.literal('dark'),
schema.literal('light'),
schema.literal('space_default'),
])
),
contrastMode: schema.maybe(
schema.oneOf([schema.literal('system'), schema.literal('standard'), schema.literal('high')])
),
'solutionNavigationTour:completed': schema.maybe(schema.boolean()),
})
),
});
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to https://github.com/elastic/kibana/pull/241213/files#r2503396102

We could add special validation logic here to disallow falsy values.


export function defineUpdateUserProfileDataRoute({
router,
getSession,
Expand All @@ -39,7 +69,12 @@ export function defineUpdateUserProfileDataRoute({
},
},
validate: {
body: schema.recordOf(schema.string(), schema.any()),
body: userProfileUpdateSchema,
},
options: {
body: {
maxBytes: MAX_USER_PROFILE_DATA_SIZE_BYTES,
},
},
},
createLicensedRouteHandler(async (context, request, response) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -470,11 +470,10 @@ export default ({ getService }: FtrProviderContext): void => {
supertest,
req: {
// @ts-expect-error: types are not correct
initials: 4,
// @ts-expect-error: types are not correct
color: true,
initials: null,
// @ts-expect-error: types are not correct
imageUrl: [],
color: null,
imageUrl: null,
},
headers: superUserHeaders,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,13 @@ export default function ({ getService }: FtrProviderContext) {
.post('/internal/security/user_profile/_data')
.set('kbn-xsrf', 'xxx')
.set('Cookie', usersSessions.get(`user_${userPrefix}`)!.cookie.cookieString())
.send({ some: `data-${userPrefix}` })
.send({
avatar: {
initials: `some-initials-${userPrefix}`,
color: `some-color-${userPrefix}`,
},
userSettings: { darkMode: `dark`, contrastMode: `high` },
})
.expect(200)
)
);
Expand Down Expand Up @@ -145,7 +151,7 @@ export default function ({ getService }: FtrProviderContext) {
.set('kbn-xsrf', 'xxx')
.send({
uids: [usersSessions.get('user_one')!.uid, usersSessions.get('user_two')!.uid],
dataPath: 'some',
dataPath: 'avatar',
})
.expect(200);
expect(profiles.body).to.have.length(2);
Expand All @@ -155,7 +161,11 @@ export default function ({ getService }: FtrProviderContext) {
Array [
Object {
"data": Object {
"some": "data-one",
"avatar": Object {
"color": "some-color-one",
"imageUrl": null,
"initials": "some-initials-one",
},
},
"user": Object {
"email": "one@elastic.co",
Expand All @@ -165,7 +175,11 @@ export default function ({ getService }: FtrProviderContext) {
},
Object {
"data": Object {
"some": "data-two",
"avatar": Object {
"color": "some-color-two",
"imageUrl": null,
"initials": "some-initials-two",
},
},
"user": Object {
"email": "two@elastic.co",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ export default function ({ getService }: FtrProviderContext) {
.post('/internal/security/user_profile/_data')
.set('kbn-xsrf', 'xxx')
.set('Cookie', sessionCookie.cookieString())
.send({ some: 'data', another: 'another-data' })
.send({
avatar: { initials: 'some-initials', color: 'some-color' },
userSettings: { darkMode: 'dark', contrastMode: 'high' },
})
.expect(200);

const { body: profileWithoutData } = await supertestWithoutAuth
Expand Down Expand Up @@ -96,8 +99,15 @@ export default function ({ getService }: FtrProviderContext) {
expectSnapshot(profileWithAllData).toMatchInline(`
Object {
"data": Object {
"another": "another-data",
"some": "data",
"avatar": Object {
"color": "some-color",
"imageUrl": null,
"initials": "some-initials",
},
"userSettings": Object {
"contrastMode": "high",
"darkMode": "dark",
},
},
"enabled": true,
"labels": Object {},
Expand All @@ -119,9 +129,7 @@ export default function ({ getService }: FtrProviderContext) {
`);
expectSnapshot(profileWithSomeData).toMatchInline(`
Object {
"data": Object {
"some": "data",
},
"data": Object {},
"enabled": true,
"labels": Object {},
"uid": "u_K1WXIRQbRoHiuJylXp842IEhAO_OdqT7SDHrJSzUIjU_0",
Expand Down
Loading