fix: Validate select menu options (#7566)

* fix: validate select menu options

* chore: make requested changes

* refactor: make requested changes

* fix: tests
This commit is contained in:
Suneet Tipirneni
2022-03-24 15:57:53 -04:00
committed by GitHub
parent b520c3df3c
commit b1d63d919a
3 changed files with 86 additions and 3 deletions

View File

@@ -35,8 +35,8 @@ describe('Select Menu Components', () => {
expect(() => selectMenu().setMaxValues(10)).not.toThrowError(); expect(() => selectMenu().setMaxValues(10)).not.toThrowError();
expect(() => selectMenu().setMinValues(3)).not.toThrowError(); expect(() => selectMenu().setMinValues(3)).not.toThrowError();
expect(() => selectMenu().setDisabled(true)).not.toThrowError(); expect(() => selectMenu().setDisabled(true)).not.toThrowError();
expect(() => selectMenu().setDisabled()).not.toThrowError();
expect(() => selectMenu().setPlaceholder('description')).not.toThrowError(); expect(() => selectMenu().setPlaceholder('description')).not.toThrowError();
const option = selectMenuOption() const option = selectMenuOption()
.setLabel('test') .setLabel('test')
.setValue('test') .setValue('test')
@@ -46,6 +46,27 @@ describe('Select Menu Components', () => {
expect(() => selectMenu().addOptions(option)).not.toThrowError(); expect(() => selectMenu().addOptions(option)).not.toThrowError();
expect(() => selectMenu().setOptions(option)).not.toThrowError(); expect(() => selectMenu().setOptions(option)).not.toThrowError();
expect(() => selectMenu().setOptions({ label: 'test', value: 'test' })).not.toThrowError(); expect(() => selectMenu().setOptions({ label: 'test', value: 'test' })).not.toThrowError();
expect(() =>
selectMenu().addOptions({
label: 'test',
value: 'test',
emoji: {
id: '123',
name: 'test',
animated: true,
},
}),
).not.toThrowError();
const options = new Array<APISelectMenuOption>(25).fill({ label: 'test', value: 'test' });
expect(() => selectMenu().addOptions(...options)).not.toThrowError();
expect(() => selectMenu().setOptions(...options)).not.toThrowError();
expect(() =>
selectMenu()
.addOptions({ label: 'test', value: 'test' })
.addOptions(...new Array<APISelectMenuOption>(24).fill({ label: 'test', value: 'test' })),
).not.toThrowError();
}); });
test('GIVEN invalid inputs THEN Select Menu does throw', () => { test('GIVEN invalid inputs THEN Select Menu does throw', () => {
@@ -55,6 +76,26 @@ describe('Select Menu Components', () => {
// @ts-expect-error // @ts-expect-error
expect(() => selectMenu().setDisabled(0)).toThrowError(); expect(() => selectMenu().setDisabled(0)).toThrowError();
expect(() => selectMenu().setPlaceholder(longStr)).toThrowError(); expect(() => selectMenu().setPlaceholder(longStr)).toThrowError();
// @ts-expect-error
expect(() => selectMenu().addOptions({ label: 'test' })).toThrowError();
expect(() => selectMenu().addOptions({ label: longStr, value: 'test' })).toThrowError();
expect(() => selectMenu().addOptions({ value: longStr, label: 'test' })).toThrowError();
expect(() => selectMenu().addOptions({ label: 'test', value: 'test', description: longStr })).toThrowError();
// @ts-expect-error
expect(() => selectMenu().addOptions({ label: 'test', value: 'test', default: 100 })).toThrowError();
// @ts-expect-error
expect(() => selectMenu().addOptions({ value: 'test' })).toThrowError();
// @ts-expect-error
expect(() => selectMenu().addOptions({ default: true })).toThrowError();
const tooManyOptions = new Array<APISelectMenuOption>(26).fill({ label: 'test', value: 'test' });
expect(() => selectMenu().setOptions(...tooManyOptions)).toThrowError();
expect(() =>
selectMenu()
.addOptions({ label: 'test', value: 'test' })
.addOptions(...tooManyOptions),
).toThrowError();
expect(() => { expect(() => {
selectMenuOption() selectMenuOption()

View File

@@ -1,6 +1,7 @@
import { APIMessageComponentEmoji, ButtonStyle } from 'discord-api-types/v10'; import { APIMessageComponentEmoji, ButtonStyle } from 'discord-api-types/v10';
import { z } from 'zod'; import { z } from 'zod';
import type { SelectMenuOptionBuilder } from './selectMenu/SelectMenuOption'; import type { SelectMenuOptionBuilder } from './selectMenu/SelectMenuOption';
import { UnsafeSelectMenuOptionBuilder } from './selectMenu/UnsafeSelectMenuOption';
export const customIdValidator = z.string().min(1).max(100); export const customIdValidator = z.string().min(1).max(100);
@@ -22,7 +23,19 @@ export const buttonStyleValidator = z.number().int().min(ButtonStyle.Primary).ma
export const placeholderValidator = z.string().max(150); export const placeholderValidator = z.string().max(150);
export const minMaxValidator = z.number().int().min(0).max(25); export const minMaxValidator = z.number().int().min(0).max(25);
export const optionsValidator = z.object({}).array().nonempty(); export const labelValueDescriptionValidator = z.string().min(1).max(100);
export const optionValidator = z.union([
z.object({
label: labelValueDescriptionValidator,
value: labelValueDescriptionValidator,
description: labelValueDescriptionValidator.optional(),
emoji: emojiValidator.optional(),
default: z.boolean().optional(),
}),
z.instanceof(UnsafeSelectMenuOptionBuilder),
]);
export const optionsValidator = optionValidator.array().nonempty();
export const optionsLengthValidator = z.number().int().min(0).max(25);
export function validateRequiredSelectMenuParameters(options: SelectMenuOptionBuilder[], customId?: string) { export function validateRequiredSelectMenuParameters(options: SelectMenuOptionBuilder[], customId?: string) {
customIdValidator.parse(customId); customIdValidator.parse(customId);

View File

@@ -1,12 +1,15 @@
import type { APISelectMenuComponent } from 'discord-api-types/v10'; import type { APISelectMenuComponent, APISelectMenuOption } from 'discord-api-types/v10';
import { import {
customIdValidator, customIdValidator,
disabledValidator, disabledValidator,
minMaxValidator, minMaxValidator,
optionsLengthValidator,
optionValidator,
placeholderValidator, placeholderValidator,
validateRequiredSelectMenuParameters, validateRequiredSelectMenuParameters,
} from '../Assertions'; } from '../Assertions';
import { UnsafeSelectMenuBuilder } from './UnsafeSelectMenu'; import { UnsafeSelectMenuBuilder } from './UnsafeSelectMenu';
import { UnsafeSelectMenuOptionBuilder } from './UnsafeSelectMenuOption';
/** /**
* Represents a validated select menu component * Represents a validated select menu component
@@ -32,6 +35,32 @@ export class SelectMenuBuilder extends UnsafeSelectMenuBuilder {
return super.setDisabled(disabledValidator.parse(disabled)); return super.setDisabled(disabledValidator.parse(disabled));
} }
public override addOptions(...options: (UnsafeSelectMenuOptionBuilder | APISelectMenuOption)[]) {
optionsLengthValidator.parse(this.options.length + options.length);
this.options.push(
...options.map((option) =>
option instanceof UnsafeSelectMenuOptionBuilder
? option
: new UnsafeSelectMenuOptionBuilder(optionValidator.parse(option) as APISelectMenuOption),
),
);
return this;
}
public override setOptions(...options: (UnsafeSelectMenuOptionBuilder | APISelectMenuOption)[]) {
optionsLengthValidator.parse(options.length);
this.options.splice(
0,
this.options.length,
...options.map((option) =>
option instanceof UnsafeSelectMenuOptionBuilder
? option
: new UnsafeSelectMenuOptionBuilder(optionValidator.parse(option) as APISelectMenuOption),
),
);
return this;
}
public override toJSON(): APISelectMenuComponent { public override toJSON(): APISelectMenuComponent {
validateRequiredSelectMenuParameters(this.options, this.data.custom_id); validateRequiredSelectMenuParameters(this.options, this.data.custom_id);
return super.toJSON(); return super.toJSON();