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
89 changes: 88 additions & 1 deletion src/__tests__/server.assertions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import {
assertInputString,
assertInputStringLength,
assertInputStringArrayEntryLength,
assertInputStringNumberEnumLike
assertInputStringNumberEnumLike,
assertInputUrlWhiteListed
} from '../server.assertions';

describe('assertInput', () => {
Expand Down Expand Up @@ -32,6 +33,16 @@ describe('assertInput', () => {
it('should pass for a valid input', () => {
expect(() => assertInput('dolor'.length > 1, 'Lorem Ipsum')).not.toThrow();
});

it('should allow custom errors when provided', () => {
const errorMessage = 'Lorem ipsum error message for validation.';

expect(() => assertInput(
false,
errorMessage,
(message, cause) => new Error(`Custom: ${message}`, { cause })
)).toThrow(`Custom: ${errorMessage}`);
});
});

describe('assertInputString', () => {
Expand Down Expand Up @@ -63,6 +74,15 @@ describe('assertInputString', () => {
it('should pass for a valid string', () => {
expect(() => assertInputString('dolor')).not.toThrow();
});

it('should allow custom errors when provided', () => {
const errorMessage = 'Lorem ipsum error message for validation.';

expect(() => assertInputString(
false,
{ message: errorMessage, codeOrError: (message, cause) => new Error(`Custom: ${message}`, { cause }) }
)).toThrow(`Custom: ${errorMessage}`);
});
});

describe('assertInputStringLength', () => {
Expand Down Expand Up @@ -120,6 +140,15 @@ describe('assertInputStringLength', () => {
it('should pass for a valid string within range', () => {
expect(() => assertInputStringLength('dolor', { min: 1, max: 10 })).not.toThrow();
});

it('should allow custom errors when provided', () => {
const errorMessage = 'Lorem ipsum error message for validation.';

expect(() => assertInputStringLength(
false,
{ min: 1, max: 10, message: errorMessage, codeOrError: (message, cause) => new Error(`Custom: ${message}`, { cause }) }
)).toThrow(`Custom: ${errorMessage}`);
});
});

describe('assertInputStringArrayEntryLength', () => {
Expand Down Expand Up @@ -177,6 +206,15 @@ describe('assertInputStringArrayEntryLength', () => {
it('should pass for a valid array of strings', () => {
expect(() => assertInputStringArrayEntryLength(['dolor'], { min: 1, max: 10 })).not.toThrow();
});

it('should allow custom errors when provided', () => {
const errorMessage = 'Lorem ipsum error message for validation.';

expect(() => assertInputStringArrayEntryLength(
false,
{ min: 1, max: 10, message: errorMessage, codeOrError: (message, cause) => new Error(`Custom: ${message}`, { cause }) }
)).toThrow(`Custom: ${errorMessage}`);
});
});

describe('assertInputStringNumberEnumLike', () => {
Expand Down Expand Up @@ -240,4 +278,53 @@ describe('assertInputStringNumberEnumLike', () => {
it('should pass for a valid value in enum-like array', () => {
expect(() => assertInputStringNumberEnumLike('dolor', ['dolor'])).not.toThrow();
});

it('should allow custom errors when provided', () => {
const errorMessage = 'Lorem ipsum error message for validation.';

expect(() => assertInputStringNumberEnumLike(
false,
['dolor'],
{ message: errorMessage, codeOrError: (message, cause) => new Error(`Custom: ${message}`, { cause }) }
)).toThrow(`Custom: ${errorMessage}`);
});
});

describe('assertInputUrlWhiteListed', () => {
it.each([
{
description: 'URL and display name',
input: 'https://github.com/patternfly',
compare: ['https://patternfly.org'],
options: { inputDisplayName: 'lorem ipsum' }
},
{
description: 'URL and description',
input: 'https://github.com/patternfly',
compare: ['https://patternfly.org'],
options: { message: 'dolor sit amet, consectetur adipiscing elit.' }
}
])('should throw an error for validation, $description', ({ input, compare, options }) => {
const errorMessage = options?.message || `"${options?.inputDisplayName || 'URL input'}" must be within the whitelisted URLs`;

expect(() => assertInputUrlWhiteListed(
input,
compare as any,
{ ...options }
)).toThrow(errorMessage);
});

it('should pass for a valid value in a whitelist array', () => {
expect(() => assertInputUrlWhiteListed('https://patternfly.org', ['https://patternfly.org'])).not.toThrow();
});

it('should allow custom errors when provided', () => {
const errorMessage = 'Lorem ipsum error message for validation.';

expect(() => assertInputUrlWhiteListed(
'https://github.com/patternfly',
['http://patternfly.org'],
{ message: errorMessage, codeOrError: (message, cause) => new Error(`Custom: ${message}`, { cause }) }
)).toThrow(`Custom: ${errorMessage}`);
});
});
15 changes: 12 additions & 3 deletions src/options.assertions.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import { z } from 'zod';
import { mcpAssert } from './server.assertions';
import { mcpAssert, type AssertCodeOrError } from './server.assertions';

/**
* Validates that each URL in the provided list uses one of the allowed schemes.
*
* @param urls - The list of URLs to be validated.
* @param protocols - Allowed scheme names (e.g. `['http', 'https']`). Each URL’s scheme is compared
* after normalizing to include a trailing colon (e.g. `http` matches `http:`).
* @param [options] - Validation options
* @param [options.codeOrError] - Thrown error code when validation fails OR a function that returns an
* error. Defaults to `ErrorCode.InvalidParams`.
*
* @throws {Error} Throws an error if any URL does not use one of the specified protocols.
* @throws Error When `codeOrError` is provided an error factory.
*/
const assertProtocol = (urls: string[], protocols: string[]) => {
const assertProtocol = (
urls: string[],
protocols: string[],
{ codeOrError }: { codeOrError?: AssertCodeOrError } = {}
) => {
const validate = z.array(
z.string().url().refine(
url => {
Expand All @@ -36,7 +44,8 @@ const assertProtocol = (urls: string[], protocols: string[]) => {

mcpAssert(
result.success,
() => `Invalid URL protocol configuration: ${result.error?.message || urls.map(url => url.slice(0, 50)).join(', ')}`
() => `Invalid URL protocol configuration: ${result.error?.message || urls.map(url => url.slice(0, 50)).join(', ')}`,
codeOrError
);
};

Expand Down
Loading
Loading