blob: d3ba8130738ec065f47fd35513d77bc20210d861 [file] [log] [blame]
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// ignore_for_file: lines_longer_than_80_chars
import 'dart:async';
import 'package:dart_mcp/server.dart';
import 'package:test/test.dart';
import '../test_utils.dart';
void main() {
// Helper to strip path and details for comparison, keeping only the error
// field. Assumes e.error is non-null for any valid error generated by
// validate().
ValidationError onlyKeepError(ValidationError e) {
return ValidationError(
e.error, // The factory requires a non-nullable error.
path: const [],
);
}
/// Asserts that schema validation produces the expected errors, ignoring
/// error paths. Expected errors should be defined without path information.
void expectFailuresMatch(
Schema schema,
Object? data,
List<ValidationError> expectedErrorsWithoutPaths, {
String? reason,
}) {
final actualErrors = schema.validate(data);
// Create a set of path-stripped actual errors.
final actualErrorsStrippedSet = actualErrors.map(onlyKeepError).toSet();
// Create a set of expected errors (which are already path-stripped by definition for this function).
final expectedErrorsSet =
expectedErrorsWithoutPaths.map(onlyKeepError).toSet();
expect(
actualErrorsStrippedSet,
equals(expectedErrorsSet),
reason:
reason ??
'Data: $data. Expected (paths ignored): $expectedErrorsSet. '
'Actual (paths ignored): $actualErrorsStrippedSet',
);
}
/// Asserts that schema validation produces the exact expected errors, including error paths.
void expectFailuresExact(
Schema schema,
Object? data,
List<ValidationError> expectedErrorsWithPaths, {
String? reason,
}) {
final actualErrors = schema.validate(data);
// Compare sets of full ValidationError objects.
// This relies on ValidationError's equality being based on its
// underlying map (including the path if present).
expect(
actualErrors.map((e) => e.toString()).toList()..sort(),
equals(expectedErrorsWithPaths.map((e) => e.toString()).toList()..sort()),
reason:
reason ??
'Data: $data. Expected (exact): ${expectedErrorsWithPaths.map((e) => e.toString()).toSet()}. '
'Actual (exact): ${actualErrors.map((e) => e.toString()).toSet()}',
);
}
group('Schemas', () {
// No changes needed in this group as it tests schema construction, not validation.
test('ObjectSchema', () {
final schema = ObjectSchema(
title: 'Foo',
description: 'Bar',
patternProperties: {'^foo': StringSchema()},
properties: {'foo': StringSchema(), 'bar': IntegerSchema()},
required: ['foo'],
additionalProperties: false,
unevaluatedProperties: true,
propertyNames: StringSchema(pattern: r'^[a-z]+$'),
minProperties: 1,
maxProperties: 2,
);
expect(schema, {
'type': 'object',
'title': 'Foo',
'description': 'Bar',
'patternProperties': {
'^foo': {'type': 'string'},
},
'properties': {
'foo': {'type': 'string'},
'bar': {'type': 'integer'},
},
'required': ['foo'],
'additionalProperties': false,
'unevaluatedProperties': true,
'propertyNames': {'type': 'string', 'pattern': r'^[a-z]+$'},
'minProperties': 1,
'maxProperties': 2,
});
});
test('StringSchema', () {
final schema = StringSchema(
title: 'Foo',
description: 'Bar',
minLength: 1,
maxLength: 10,
pattern: r'^[a-z]+$',
);
expect(schema, {
'type': 'string',
'title': 'Foo',
'description': 'Bar',
'minLength': 1,
'maxLength': 10,
'pattern': r'^[a-z]+$',
});
});
test('NumberSchema', () {
final schema = NumberSchema(
title: 'Foo',
description: 'Bar',
minimum: 1,
maximum: 10,
exclusiveMinimum: 0,
exclusiveMaximum: 11,
multipleOf: 2,
);
expect(schema, {
'type': 'number',
'title': 'Foo',
'description': 'Bar',
'minimum': 1,
'maximum': 10,
'exclusiveMinimum': 0,
'exclusiveMaximum': 11,
'multipleOf': 2,
});
});
test('IntegerSchema', () {
final schema = IntegerSchema(
title: 'Foo',
description: 'Bar',
minimum: 1,
maximum: 10,
exclusiveMinimum: 0,
exclusiveMaximum: 11,
multipleOf: 2,
);
expect(schema, {
'type': 'integer',
'title': 'Foo',
'description': 'Bar',
'minimum': 1,
'maximum': 10,
'exclusiveMinimum': 0,
'exclusiveMaximum': 11,
'multipleOf': 2,
});
});
test('BooleanSchema', () {
final schema = BooleanSchema(title: 'Foo', description: 'Bar');
expect(schema, {'type': 'boolean', 'title': 'Foo', 'description': 'Bar'});
});
test('NullSchema', () {
final schema = NullSchema(title: 'Foo', description: 'Bar');
expect(schema, {'type': 'null', 'title': 'Foo', 'description': 'Bar'});
});
test('ListSchema', () {
final schema = ListSchema(
title: 'Foo',
description: 'Bar',
items: StringSchema(),
prefixItems: [IntegerSchema(), BooleanSchema()],
unevaluatedItems: false,
minItems: 1,
maxItems: 10,
uniqueItems: true,
);
expect(schema, {
'type': 'array',
'title': 'Foo',
'description': 'Bar',
'items': {'type': 'string'},
'prefixItems': [
{'type': 'integer'},
{'type': 'boolean'},
],
'unevaluatedItems': false,
'minItems': 1,
'maxItems': 10,
'uniqueItems': true,
});
});
test('EnumSchema', () {
final schema = EnumSchema(
title: 'Foo',
description: 'Bar',
values: {'a', 'b', 'c'},
);
expect(schema, {
'type': 'enum',
'title': 'Foo',
'description': 'Bar',
'enum': ['a', 'b', 'c'],
});
});
test('Schema', () {
final schema = Schema.combined(
type: JsonType.bool,
title: 'Foo',
description: 'Bar',
allOf: [StringSchema(), IntegerSchema()],
anyOf: [StringSchema(), IntegerSchema()],
oneOf: [StringSchema(), IntegerSchema()],
not: [StringSchema()],
);
expect(schema, {
'type': 'boolean',
'title': 'Foo',
'description': 'Bar',
'allOf': [
{'type': 'string'},
{'type': 'integer'},
],
'anyOf': [
{'type': 'string'},
{'type': 'integer'},
],
'oneOf': [
{'type': 'string'},
{'type': 'integer'},
],
'not': [
{'type': 'string'},
],
});
});
});
group('Schema Validation Tests (Paths Ignored)', () {
// Most tests will use expectFailuresMatch
group('Type Mismatch', () {
test('object schema with non-map data', () {
expectFailuresMatch(Schema.object(), 'not a map', [
ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('list schema with non-list data', () {
expectFailuresMatch(Schema.list(), 'not a list', [
ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('string schema with non-string data', () {
expectFailuresMatch(Schema.string(), 123, [
ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('number schema with non-num data', () {
expectFailuresMatch(Schema.num(), 'not a number', [
ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('integer schema with non-int data', () {
expectFailuresMatch(Schema.int(), 'not an int', [
ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('integer schema with non-integer num data', () {
expectFailuresMatch(Schema.int(), 10.5, [
ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('boolean schema with non-bool data', () {
expectFailuresMatch(Schema.bool(), 'not a bool', [
ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('null schema with non-null data', () {
expectFailuresMatch(Schema.nil(), 'not null', [
ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('integer schema with integer-like num data (e.g. 10.0)', () {
// This test expects minimumNotMet because 10.0 is converted to int 10,
// which is less than the minimum of 11.
expectFailuresMatch(IntegerSchema(minimum: 11), 10.0, [
ValidationError(ValidationErrorType.minimumNotMet, path: const []),
]);
});
});
group('Schema Combinators', () {
test('allOfNotMet - one sub-schema fails', () {
final schema = Schema.combined(
allOf: [StringSchema(minLength: 3), StringSchema(maxLength: 5)],
);
// 'hi' fails minLength: 3.
// The allOf combinator fails, and the specific sub-failure is also reported.
expectFailuresMatch(schema, 'hi', [
ValidationError(ValidationErrorType.allOfNotMet, path: const []),
ValidationError(ValidationErrorType.minLengthNotMet, path: const []),
]);
});
test('allOfNotMet - multiple sub-schemas fail', () {
final schema = Schema.combined(
allOf: [
StringSchema(minLength: 10),
StringSchema(pattern: '^[a-z]+\$'),
],
);
// 'Short123' fails minLength and pattern.
expectFailuresMatch(schema, 'Short123', [
ValidationError(ValidationErrorType.allOfNotMet, path: const []),
ValidationError(ValidationErrorType.minLengthNotMet, path: const []),
ValidationError(ValidationErrorType.patternMismatch, path: const []),
]);
});
test('anyOfNotMet - all sub-schemas fail', () {
final schema = Schema.combined(
anyOf: [StringSchema(minLength: 5), NumberSchema(minimum: 100)],
);
// `true` will cause typeMismatch for both StringSchema and NumberSchema.
expectFailuresMatch(schema, true, [
ValidationError(ValidationErrorType.anyOfNotMet, path: const []),
// The specific type mismatches from sub-schemas might also be reported
// depending on how _validateSchema handles anyOf error aggregation.
// Current tools.dart only adds anyOfNotMet for anyOf.
]);
});
test('anyOfNotMet - specific failures', () {
final schema = Schema.combined(
anyOf: [
StringSchema(minLength: 5),
StringSchema(pattern: '^[a-z]+\$'),
],
);
// "Hi1" fails minLength: 5 and pattern: '^[a-z]+$'.
// Since both sub-schemas fail, anyOfNotMet is reported.
expectFailuresMatch(schema, 'Hi1', [
ValidationError(ValidationErrorType.anyOfNotMet, path: const []),
]);
});
test('oneOfNotMet - matches none', () {
final s = Schema.combined(
oneOf: [
StringSchema(minLength: 3, maxLength: 3), // Effectively length 3
NumberSchema(minimum: 10, maximum: 10), // Effectively value 10
],
);
// `true` matches neither sub-schema.
expectFailuresMatch(s, true, [
ValidationError(ValidationErrorType.oneOfNotMet, path: const []),
]);
});
test('oneOfNotMet - matches multiple', () {
final schema = Schema.combined(
oneOf: [StringSchema(maxLength: 10), StringSchema(pattern: 'test')],
);
// 'test' matches both maxLength: 10 and pattern: 'test'.
expectFailuresMatch(schema, 'test', [
ValidationError(ValidationErrorType.oneOfNotMet, path: const []),
]);
});
test('notConditionViolated - matches 1 sub-schema in "not" list', () {
final schema = Schema.combined(
not: [StringSchema(maxLength: 2), StringSchema(pattern: 'test')],
);
// 'test' matches the second sub-schema in the "not" list.
expectFailuresMatch(schema, 'test', [
ValidationError(
ValidationErrorType.notConditionViolated,
path: const [],
),
]);
});
});
group('Object Specific', () {
test('requiredPropertyMissing', () {
final schema = ObjectSchema(required: ['name']);
expectFailuresMatch(
schema,
{'foo': 1},
[
ValidationError(
ValidationErrorType.requiredPropertyMissing,
path: const [],
details: 'Required property "name" is missing',
),
],
);
});
test('additionalPropertyNotAllowed - boolean false', () {
final schema = ObjectSchema(
properties: {'name': StringSchema()},
additionalProperties: false,
);
// 'age' is an additional property not allowed.
expectFailuresMatch(
schema,
{'name': 'test', 'age': 30},
[
ValidationError(
ValidationErrorType.additionalPropertyNotAllowed,
path: const [],
),
],
);
});
test('additionalPropertyNotAllowed - schema fails', () {
final schema = ObjectSchema(
properties: {'name': StringSchema()},
additionalProperties: StringSchema(minLength: 5),
);
// 'extra': 'abc' fails the additionalProperties schema (minLength: 5).
expectFailuresMatch(
schema,
{'name': 'test', 'extra': 'abc'},
[
ValidationError(
ValidationErrorType.minLengthNotMet,
path: const [],
), // Sub-failure from additionalProperties schema
],
);
});
test('minPropertiesNotMet', () {
final schema = ObjectSchema(minProperties: 2);
expectFailuresMatch(
schema,
{'a': 1},
[
ValidationError(
ValidationErrorType.minPropertiesNotMet,
path: const [],
),
],
);
});
test('maxPropertiesExceeded', () {
final schema = ObjectSchema(maxProperties: 1);
expectFailuresMatch(
schema,
{'a': 1, 'b': 2},
[
ValidationError(
ValidationErrorType.maxPropertiesExceeded,
path: const [],
),
],
);
});
test('propertyNamesInvalid', () {
final schema = ObjectSchema(propertyNames: StringSchema(minLength: 3));
// Property name 'ab' fails minLength: 3.
expectFailuresMatch(
schema,
{'ab': 1, 'abc': 2},
[
ValidationError(
ValidationErrorType.minLengthNotMet,
path: const [],
), // Sub-failure from propertyNames schema
],
);
});
test('propertyValueInvalid', () {
final schema = ObjectSchema(
properties: {'age': IntegerSchema(minimum: 18)},
);
// Value 10 for 'age' fails IntegerSchema(minimum: 18).
// Both propertyValueInvalid and the specific sub-failure (minimumNotMet) are reported.
expectFailuresMatch(
schema,
{'age': 10},
[ValidationError(ValidationErrorType.minimumNotMet, path: const [])],
);
});
test('patternPropertyValueInvalid', () {
final schema = ObjectSchema(
patternProperties: {r'^x-': IntegerSchema(minimum: 10)},
);
// Value 5 for 'x-custom' fails IntegerSchema(minimum: 10).
expectFailuresMatch(
schema,
{'x-custom': 5},
[ValidationError(ValidationErrorType.minimumNotMet, path: const [])],
);
});
test('unevaluatedPropertyNotAllowed', () {
final schema = ObjectSchema(
properties: {'name': StringSchema()},
unevaluatedProperties: false,
);
// 'age' is unevaluated and not allowed.
expectFailuresMatch(
schema,
{'name': 'test', 'age': 30},
[
ValidationError(
ValidationErrorType.unevaluatedPropertyNotAllowed,
path: const [],
),
],
);
});
});
group('List Specific', () {
test('minItemsNotMet', () {
final schema = ListSchema(minItems: 2);
expectFailuresMatch(
schema,
[1],
[ValidationError(ValidationErrorType.minItemsNotMet, path: const [])],
);
});
test('maxItemsExceeded', () {
final schema = ListSchema(maxItems: 1);
expectFailuresMatch(
schema,
[1, 2],
[
ValidationError(
ValidationErrorType.maxItemsExceeded,
path: const [],
),
],
);
});
test('uniqueItemsViolated', () {
final schema = ListSchema(uniqueItems: true);
expectFailuresMatch(
schema,
[1, 2, 1],
[
ValidationError(
ValidationErrorType.uniqueItemsViolated,
path: const [],
),
],
);
});
test('itemInvalid - using items', () {
final schema = ListSchema(items: IntegerSchema(minimum: 10));
// Item 5 fails IntegerSchema(minimum: 10).
expectFailuresMatch(
schema,
[10, 5, 12],
[ValidationError(ValidationErrorType.minimumNotMet, path: const [])],
);
});
test('prefixItemInvalid', () {
final schema = ListSchema(
prefixItems: [IntegerSchema(minimum: 10), StringSchema(minLength: 3)],
);
// First prefix item 5 fails IntegerSchema(minimum: 10).
expectFailuresMatch(
schema,
[5], // Not enough items for all prefixItems, but first one is checked
[ValidationError(ValidationErrorType.minimumNotMet, path: const [])],
);
// Second prefix item 'hi' fails StringSchema(minLength: 3).
expectFailuresMatch(
schema,
[10, 'hi'],
[
ValidationError(
ValidationErrorType.minLengthNotMet,
path: const [],
),
],
);
});
test(
'unevaluatedItemNotAllowed - after prefixItems, no items schema',
() {
final schema = ListSchema(
prefixItems: [IntegerSchema()],
unevaluatedItems: false,
);
// 'extra' is unevaluated and not allowed.
expectFailuresMatch(
schema,
[10, 'extra'],
[
ValidationError(
ValidationErrorType.unevaluatedItemNotAllowed,
path: const [],
),
],
);
},
);
});
group('String Specific', () {
test('minLengthNotMet', () {
final schema = StringSchema(minLength: 3);
expectFailuresMatch(schema, 'hi', [
ValidationError(ValidationErrorType.minLengthNotMet, path: const []),
]);
});
// ... other string specific tests using expectFailuresMatch
});
group('Number Specific', () {
test('minimumNotMet', () {
final schema = NumberSchema(minimum: 10);
expectFailuresMatch(schema, 5, [
ValidationError(ValidationErrorType.minimumNotMet, path: const []),
]);
});
// ... other number specific tests using expectFailuresMatch
});
group('Integer Specific', () {
test('minimumNotMet', () {
final schema = IntegerSchema(minimum: 10);
expectFailuresMatch(schema, 5, [
ValidationError(ValidationErrorType.minimumNotMet, path: const []),
]);
});
// ... other integer specific tests using expectFailuresMatch
});
});
group('Schema Validation Path Tests (Exact Paths)', () {
// Tests specifically for path validation will use expectFailuresExact
test('typeMismatch at root has empty path', () {
expectFailuresExact(Schema.string(), 123, [
ValidationError.typeMismatch(
path: [],
expectedType: String,
actualValue: 123,
),
]);
});
test('requiredPropertyMissing with correct path', () {
final schema = ObjectSchema(required: ['name']);
expectFailuresExact(
schema,
{'foo': 1},
[
ValidationError(
ValidationErrorType.requiredPropertyMissing,
path:
[], // Missing property is checked at the current object level (root in this case)
details: 'Required property "name" is missing',
),
],
);
});
test('propertyValueInvalid with path and sub-failure', () {
final schema = ObjectSchema(
properties: {'age': IntegerSchema(minimum: 18)},
);
expectFailuresExact(
schema,
{'age': 10},
[
ValidationError(
ValidationErrorType.minimumNotMet,
path: ['age'],
details: 'Value 10 is less than the minimum of 18',
), // Sub-failure also has the path
],
);
});
test('Deeper path for nested object property invalid', () {
final schema = ObjectSchema(
properties: {
'user': ObjectSchema(
properties: {
'address': ObjectSchema(required: ['street']),
},
required: ['address'], // Let's make 'address' itself required
),
},
required: ['user'], // And 'user' required
);
expectFailuresExact(
schema,
{
'user': {
'address': {'city': 'Testville'},
},
}, // 'street' is missing
[
ValidationError(
ValidationErrorType.requiredPropertyMissing,
path: [
'user',
'address',
], // Path to the object where 'street' is missing
details: 'Required property "street" is missing',
),
],
);
});
test('itemInvalid in list with path and sub-failure', () {
final schema = ListSchema(items: IntegerSchema(minimum: 100));
expectFailuresExact(
schema,
[101, 50, 200], // Item at index 1 (value 50) is invalid
[
ValidationError(
ValidationErrorType.minimumNotMet,
path: ['1'],
details: 'Value 50 is less than the minimum of 100',
),
],
);
});
test('prefixItemInvalid with path and sub-failure', () {
final schema = ListSchema(
prefixItems: [StringSchema(minLength: 3), IntegerSchema(maximum: 10)],
);
expectFailuresExact(
schema,
['ok', 20], // Item at index 1 (value 20) fails prefixItem schema
[
ValidationError(
ValidationErrorType.minLengthNotMet,
path: ['0'],
details: 'String "ok" is not at least 3 characters long',
),
ValidationError(
ValidationErrorType.maximumExceeded,
path: ['1'],
details: 'Value 20 is more than the maximum of 10',
),
],
);
});
test('allOfNotMet with path and sub-failures', () {
final schema = Schema.combined(
allOf: [StringSchema(minLength: 3), StringSchema(maxLength: 1)],
);
expectFailuresExact(schema, 'hi', [
// 'hi' fails maxLength:1 and thus minLength:3 is also not met in a sense for allOf
ValidationError(ValidationErrorType.allOfNotMet, path: []),
ValidationError(
ValidationErrorType.minLengthNotMet,
path: [],
details: 'String "hi" is not at least 3 characters long',
), // from first sub-schema
ValidationError(
ValidationErrorType.maxLengthExceeded,
path: [],
details: 'String "hi" is more than 1 characters long',
), // from second sub-schema
]);
});
test('additionalPropertyNotAllowed with path and sub-failure', () {
final schema = ObjectSchema(
properties: {'name': StringSchema()},
additionalProperties: StringSchema(minLength: 5),
);
expectFailuresExact(
schema,
{'name': 'test', 'extra': 'abc'},
[
ValidationError(
ValidationErrorType.minLengthNotMet,
path: ['extra'],
details: 'String "abc" is not at least 5 characters long',
),
],
);
});
});
group('Schema Validation Tests', () {
group('Type Mismatch', () {
test('object schema with non-map data', () {
expectFailuresMatch(Schema.object(), 'not a map', [
ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('list schema with non-list data', () {
expectFailuresMatch(Schema.list(), 'not a list', [
ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('string schema with non-string data', () {
expectFailuresMatch(Schema.string(), 123, [
ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('number schema with non-num data', () {
expectFailuresMatch(Schema.num(), 'not a number', [
ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('integer schema with non-int data', () {
expectFailuresMatch(Schema.int(), 'not an int', [
ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('integer schema with non-integer num data', () {
expectFailuresMatch(Schema.int(), 10.5, [
ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('boolean schema with non-bool data', () {
expectFailuresMatch(Schema.bool(), 'not a bool', [
ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('null schema with non-null data', () {
expectFailuresMatch(Schema.nil(), 'not null', [
ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('integer schema with integer-like num data (e.g. 10.0)', () {
expectFailuresMatch(IntegerSchema(minimum: 11), 10.0, [
ValidationError(ValidationErrorType.minimumNotMet, path: const []),
]);
});
});
group('Enum Specific', () {
test('enumValueNotAllowed', () {
final schema = EnumSchema(values: {'a', 'b'});
expectFailuresMatch(schema, 'c', [
ValidationError(
ValidationErrorType.enumValueNotAllowed,
path: const [],
),
]);
});
test('valid enum value', () {
final schema = EnumSchema(values: {'a', 'b'});
expectFailuresMatch(schema, 'a', []);
});
test('enum with non-string data', () {
final schema = EnumSchema(values: {'a', 'b'});
expectFailuresMatch(schema, 1, [
ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
});
group('Schema Combinators', () {
test('allOfNotMet - one sub-schema fails', () {
final schema = Schema.combined(
allOf: [StringSchema(minLength: 3), StringSchema(maxLength: 5)],
);
expectFailuresMatch(schema, 'hi', [
ValidationError(ValidationErrorType.allOfNotMet, path: const []),
ValidationError(ValidationErrorType.minLengthNotMet, path: const []),
]);
});
test('allOfNotMet - multiple sub-schemas fail', () {
final schema = Schema.combined(
allOf: [
StringSchema(minLength: 10),
StringSchema(pattern: '^[a-z]+\$'),
],
);
expectFailuresMatch(schema, 'Short123', [
ValidationError(ValidationErrorType.allOfNotMet, path: const []),
ValidationError(ValidationErrorType.minLengthNotMet, path: const []),
ValidationError(ValidationErrorType.patternMismatch, path: const []),
]);
});
test('anyOfNotMet - all sub-schemas fail', () {
final schema = Schema.combined(
anyOf: [StringSchema(minLength: 5), NumberSchema(minimum: 100)],
);
// Data that fails both type checks if types were strictly enforced by anyOf sub-schemas alone
// However, anyOf itself doesn't enforce type, the subschemas do.
// If data is `true`, StringSchema(minLength: 5).validate(true) -> [typeMismatch]
// NumberSchema(minimum: 100).validate(true) -> [typeMismatch]
// So anyOf fails.
expectFailuresMatch(schema, true, [
ValidationError(ValidationErrorType.anyOfNotMet, path: const []),
]);
});
test('anyOfNotMet - specific failures', () {
final schema = Schema.combined(
anyOf: [
StringSchema(minLength: 5),
StringSchema(pattern: '^[a-z]+\$'),
],
);
// "Hi1" fails minLength and pattern.
// StringSchema(minLength: 5).validate("Hi1") -> [minLengthNotMet]
// StringSchema(pattern: '^[a-z]+$').validate("Hi1") -> [patternMismatch]
// Since both fail, anyOf fails.
expectFailuresMatch(schema, 'Hi1', [
ValidationError(ValidationErrorType.anyOfNotMet, path: const []),
]);
});
test('oneOfNotMet - matches none', () {
// Using minLength/maxLength to simulate exactLength, and minimum/maximum for exactValue
final s = Schema.combined(
oneOf: [
Schema.combined(
allOf: [StringSchema(minLength: 3), StringSchema(maxLength: 3)],
),
Schema.combined(
allOf: [NumberSchema(minimum: 10), NumberSchema(maximum: 10)],
),
],
);
expectFailuresMatch(s, true, [
ValidationError(ValidationErrorType.oneOfNotMet, path: const []),
]);
});
test('oneOfNotMet - matches multiple', () {
final schema = Schema.combined(
oneOf: [StringSchema(maxLength: 10), StringSchema(pattern: 'test')],
);
expectFailuresMatch(schema, 'test', [
ValidationError(ValidationErrorType.oneOfNotMet, path: const []),
]);
});
test('notConditionViolated - matches 1 sub-schemas in "not" list', () {
final schema = Schema.combined(
not: [StringSchema(maxLength: 2), StringSchema(pattern: 'test')],
);
expectFailuresMatch(schema, 'test', [
ValidationError(
ValidationErrorType.notConditionViolated,
path: const [],
),
]);
});
test('notConditionViolated - matches >0 sub-schemas in "not" list', () {
final schema = Schema.combined(
not: [StringSchema(maxLength: 10), StringSchema(pattern: 'test')],
);
expectFailuresMatch(schema, 'test', [
ValidationError(
ValidationErrorType.notConditionViolated,
path: const [],
),
ValidationError(
ValidationErrorType.notConditionViolated,
path: const [],
),
]);
});
});
group('Object Specific', () {
test('requiredPropertyMissing', () {
final schema = ObjectSchema(required: ['name']);
expectFailuresMatch(
schema,
{'foo': 1},
[
ValidationError(
ValidationErrorType.requiredPropertyMissing,
path: const [],
),
],
);
});
test('additionalPropertyNotAllowed - boolean false', () {
final schema = ObjectSchema(
properties: {'name': StringSchema()},
additionalProperties: false,
);
expectFailuresMatch(
schema,
{'name': 'test', 'age': 30},
[
ValidationError(
ValidationErrorType.additionalPropertyNotAllowed,
path: const [],
),
],
);
});
test('additionalPropertyNotAllowed - schema fails', () {
final schema = ObjectSchema(
properties: {'name': StringSchema()},
additionalProperties: StringSchema(minLength: 5),
);
expectFailuresMatch(
schema,
{'name': 'test', 'extra': 'abc'},
[
ValidationError(
ValidationErrorType.minLengthNotMet,
path: const [],
),
],
);
});
test('minPropertiesNotMet', () {
final schema = ObjectSchema(minProperties: 2);
expectFailuresMatch(
schema,
{'a': 1},
[
ValidationError(
ValidationErrorType.minPropertiesNotMet,
path: const [],
),
],
);
});
test('maxPropertiesExceeded', () {
final schema = ObjectSchema(maxProperties: 1);
expectFailuresMatch(
schema,
{'a': 1, 'b': 2},
[
ValidationError(
ValidationErrorType.maxPropertiesExceeded,
path: const [],
),
],
);
});
test('propertyNamesInvalid', () {
final schema = ObjectSchema(propertyNames: StringSchema(minLength: 3));
expectFailuresMatch(
schema,
{'ab': 1, 'abc': 2},
[
ValidationError(
ValidationErrorType.minLengthNotMet,
path: const [],
),
],
);
});
test('propertyValueInvalid', () {
final schema = ObjectSchema(
properties: {'age': IntegerSchema(minimum: 18)},
);
expectFailuresMatch(
schema,
{'age': 10},
[ValidationError(ValidationErrorType.minimumNotMet, path: const [])],
);
});
test('patternPropertyValueInvalid', () {
final schema = ObjectSchema(
patternProperties: {r'^x-': IntegerSchema(minimum: 10)},
);
expectFailuresMatch(
schema,
{'x-custom': 5},
[ValidationError(ValidationErrorType.minimumNotMet, path: const [])],
);
});
test('unevaluatedPropertyNotAllowed', () {
final schema = ObjectSchema(
properties: {'name': StringSchema()},
unevaluatedProperties: false,
// additionalProperties is implicitly null/not defined here
);
expectFailuresMatch(
schema,
{'name': 'test', 'age': 30},
[
ValidationError(
ValidationErrorType.unevaluatedPropertyNotAllowed,
path: const [],
),
],
);
});
test('additionalPropertyAllowed - boolean true', () {
final schema = ObjectSchema(
properties: {'name': StringSchema()},
additionalProperties: true,
);
expectFailuresMatch(
schema,
{'name': 'test', 'age': 30},
[], // No errors expected
);
});
test('unevaluatedPropertyAllowed - default behavior', () {
// When additionalProperties is not set and unevaluatedProperties is not false,
// extra properties should be allowed.
final schema = ObjectSchema(properties: {'name': StringSchema()});
expectFailuresMatch(
schema,
{'name': 'test', 'age': 30},
[], // No errors expected
);
});
});
group('List Specific', () {
test('minItemsNotMet', () {
final schema = ListSchema(minItems: 2);
expectFailuresMatch(
schema,
[1],
[ValidationError(ValidationErrorType.minItemsNotMet, path: const [])],
);
});
test('maxItemsExceeded', () {
final schema = ListSchema(maxItems: 1);
expectFailuresMatch(
schema,
[1, 2],
[
ValidationError(
ValidationErrorType.maxItemsExceeded,
path: const [],
),
],
);
});
test('uniqueItemsViolated', () {
final schema = ListSchema(uniqueItems: true);
expectFailuresMatch(
schema,
[1, 2, 1],
[
ValidationError(
ValidationErrorType.uniqueItemsViolated,
path: const [],
),
],
);
});
test('itemInvalid - using items', () {
final schema = ListSchema(items: IntegerSchema(minimum: 10));
// _validate for IntegerSchema(minimum:10) on 5 returns [minimumNotMet]
// The list validation adds itemInvalid if the item's validation is not empty.
expectFailuresMatch(
schema,
[10, 5, 12],
[ValidationError(ValidationErrorType.minimumNotMet, path: const [])],
);
});
test('prefixItemInvalid', () {
final schema = ListSchema(
prefixItems: [IntegerSchema(minimum: 10), StringSchema(minLength: 3)],
);
expectFailuresMatch(
schema,
[5],
[ValidationError(ValidationErrorType.minimumNotMet, path: const [])],
);
expectFailuresMatch(
schema,
[10, 'hi'],
[
ValidationError(
ValidationErrorType.minLengthNotMet,
path: const [],
),
],
);
});
test(
'unevaluatedItemNotAllowed - after prefixItems, no items schema',
() {
final schema = ListSchema(
prefixItems: [IntegerSchema()],
unevaluatedItems: false,
);
expectFailuresMatch(
schema,
[10, 'extra'],
[
ValidationError(
ValidationErrorType.unevaluatedItemNotAllowed,
path: const [],
),
],
);
},
);
test('unevaluatedItemNotAllowed - no prefixItems, no items schema', () {
final schema = ListSchema(unevaluatedItems: false);
expectFailuresMatch(
schema,
['extra'],
[
ValidationError(
ValidationErrorType.unevaluatedItemNotAllowed,
path: const [],
),
],
);
});
test('unevaluatedItemNotAllowed - after items that cover some elements', () {
// This case implies items schema applies to elements after prefixItems.
// If unevaluatedItems is false, and an item exists beyond what items schema covers (if items is not for all remaining)
// or if items schema itself doesn't match.
// The current `_validateList` logic for `items` applies it to all elements from `startIndex`.
// So, `unevaluatedItems: false` would only trigger if `items` is null and `prefixItems` doesn't cover all.
// Let's re-test with `items` present.
final schemaWithItems = ListSchema(
prefixItems: [IntegerSchema()],
items: StringSchema(
minLength: 2,
), // Applies to items after prefixItems
unevaluatedItems: false, // Should not be hit if items covers the rest
);
// [10, "a"] -> "a" fails StringSchema(minLength:2), so itemInvalid
expectFailuresMatch(
schemaWithItems,
[10, 'a'],
[
ValidationError(
ValidationErrorType.minLengthNotMet,
path: const [],
),
],
);
// If items is null, then unevaluatedItems:false applies to items after prefixItems
final schemaNoItems = ListSchema(
prefixItems: [IntegerSchema()],
// items: null, (implicit)
unevaluatedItems: false,
);
expectFailuresMatch(
schemaNoItems,
[10, 'extra string'],
[
ValidationError(
ValidationErrorType.unevaluatedItemNotAllowed,
path: const [],
),
],
);
});
test('unevaluatedItemAllowed - default behavior', () {
// When unevaluatedItems is not false, extra items should be allowed.
final schema = ListSchema(
prefixItems: [IntegerSchema()],
items: StringSchema(),
// unevaluatedItems is null (default), equivalent to true.
);
expectFailuresMatch(
schema,
[10, 'hello', true], // `true` is unevaluated but allowed
[ValidationError(ValidationErrorType.typeMismatch, path: const [])],
reason:
'Item `true` at index 2 is evaluated by `items: StringSchema()` '
'and fails. `unevaluatedItems` (defaulting to true) does not apply '
'to items already evaluated by `items` or `prefixItems`.',
);
});
});
group('String Specific', () {
test('minLengthNotMet', () {
final schema = StringSchema(minLength: 3);
expectFailuresMatch(schema, 'hi', [
ValidationError(ValidationErrorType.minLengthNotMet, path: const []),
]);
});
test('maxLengthExceeded', () {
final schema = StringSchema(maxLength: 3);
expectFailuresMatch(schema, 'hello', [
ValidationError(
ValidationErrorType.maxLengthExceeded,
path: const [],
),
]);
});
test('patternMismatch', () {
final schema = StringSchema(pattern: r'^\d+$');
expectFailuresMatch(schema, 'abc', [
ValidationError(ValidationErrorType.patternMismatch, path: const []),
]);
});
});
group('Number Specific', () {
test('minimumNotMet', () {
final schema = NumberSchema(minimum: 10);
expectFailuresMatch(schema, 5, [
ValidationError(ValidationErrorType.minimumNotMet, path: const []),
]);
});
test('maximumExceeded', () {
final schema = NumberSchema(maximum: 10);
expectFailuresMatch(schema, 15, [
ValidationError(ValidationErrorType.maximumExceeded, path: const []),
]);
});
test('exclusiveMinimumNotMet - equal value', () {
final schema = NumberSchema(exclusiveMinimum: 10);
expectFailuresMatch(schema, 10, [
ValidationError(
ValidationErrorType.exclusiveMinimumNotMet,
path: const [],
),
]);
});
test('exclusiveMinimumNotMet - smaller value', () {
final schema = NumberSchema(exclusiveMinimum: 10);
expectFailuresMatch(schema, 9, [
ValidationError(
ValidationErrorType.exclusiveMinimumNotMet,
path: const [],
),
]);
});
test('exclusiveMaximumExceeded - equal value', () {
final schema = NumberSchema(exclusiveMaximum: 10);
expectFailuresMatch(schema, 10, [
ValidationError(
ValidationErrorType.exclusiveMaximumExceeded,
path: const [],
),
]);
});
test('exclusiveMaximumExceeded - larger value', () {
final schema = NumberSchema(exclusiveMaximum: 10);
expectFailuresMatch(schema, 11, [
ValidationError(
ValidationErrorType.exclusiveMaximumExceeded,
path: const [],
),
]);
});
test('multipleOfInvalid', () {
final schema = NumberSchema(multipleOf: 3);
expectFailuresMatch(schema, 10, [
ValidationError(
ValidationErrorType.multipleOfInvalid,
path: const [],
),
]);
});
test('multipleOfInvalid - floating point', () {
final schema = NumberSchema(multipleOf: 0.1);
expectFailuresMatch(schema, 0.25, [
ValidationError(
ValidationErrorType.multipleOfInvalid,
path: const [],
),
]);
});
test('multipleOfInvalid - valid floating point', () {
final schema = NumberSchema(multipleOf: 0.1);
expectFailuresMatch(schema, 0.3, []);
});
test('multipleOf zero is ignored (NumberSchema)', () {
// JSON Schema spec says multipleOf MUST be > 0.
// Current implementation ignores multipleOf if it's 0.
final schema = NumberSchema(multipleOf: 0);
expectFailuresMatch(
schema,
10.5,
[],
reason:
'multipleOf: 0 is currently ignored, data should pass regardless',
);
});
});
group('Integer Specific', () {
test('minimumNotMet', () {
final schema = IntegerSchema(minimum: 10);
expectFailuresMatch(schema, 5, [
ValidationError(ValidationErrorType.minimumNotMet, path: const []),
]);
});
test('maximumExceeded', () {
final schema = IntegerSchema(maximum: 10);
expectFailuresMatch(schema, 15, [
ValidationError(ValidationErrorType.maximumExceeded, path: const []),
]);
});
test('exclusiveMinimumNotMet - equal value', () {
final schema = IntegerSchema(exclusiveMinimum: 10);
expectFailuresMatch(schema, 10, [
ValidationError(
ValidationErrorType.exclusiveMinimumNotMet,
path: const [],
),
]);
});
test('exclusiveMinimumNotMet - smaller value', () {
final schema = IntegerSchema(exclusiveMinimum: 10);
expectFailuresMatch(schema, 9, [
ValidationError(
ValidationErrorType.exclusiveMinimumNotMet,
path: const [],
),
]);
});
test('exclusiveMaximumExceeded - equal value', () {
final schema = IntegerSchema(exclusiveMaximum: 10);
expectFailuresMatch(schema, 10, [
ValidationError(
ValidationErrorType.exclusiveMaximumExceeded,
path: const [],
),
]);
});
test('exclusiveMaximumExceeded - larger value', () {
final schema = IntegerSchema(exclusiveMaximum: 10);
expectFailuresMatch(schema, 11, [
ValidationError(
ValidationErrorType.exclusiveMaximumExceeded,
path: const [],
),
]);
});
test('multipleOfInvalid', () {
final schema = IntegerSchema(multipleOf: 3);
expectFailuresMatch(schema, 10, [
ValidationError(
ValidationErrorType.multipleOfInvalid,
path: const [],
),
]);
});
test('multipleOf zero is ignored (IntegerSchema)', () {
// JSON Schema spec says multipleOf MUST be > 0.
// Current implementation ignores multipleOf if it's 0.
final schema = IntegerSchema(multipleOf: 0);
expectFailuresMatch(
schema,
7,
[],
reason:
'multipleOf: 0 is currently ignored, data should pass regardless',
);
});
});
group('Schema Combinators - Advanced', () {
test('empty combinator lists', () {
// Per JSON Schema spec, allOf, anyOf, oneOf arrays must be non-empty.
// 'not' takes a single schema. Assuming dart_mcp's List<Schema> for 'not'
// means data must not match any in the list.
final schemaAllOfEmpty = Schema.combined(allOf: []);
expectFailuresMatch(
schemaAllOfEmpty,
'any data',
[],
reason: 'allOf:[] is vacuously true',
);
final schemaAnyOfEmpty = Schema.combined(anyOf: []);
expectFailuresMatch(schemaAnyOfEmpty, 'any data', [
ValidationError(ValidationErrorType.anyOfNotMet, path: const []),
]);
final schemaOneOfEmpty = Schema.combined(oneOf: []);
expectFailuresMatch(schemaOneOfEmpty, 'any data', [
ValidationError(ValidationErrorType.oneOfNotMet, path: const []),
]);
// If 'not' is a list of schemas, and the list is empty,
// then the data doesn't match any schema in the 'not' list.
// So, the 'not' condition is satisfied.
final schemaNotEmpty = Schema.combined(not: []);
expectFailuresMatch(
schemaNotEmpty,
'any data',
[],
reason: 'not:[] means no forbidden schemas, so it passes validation',
);
});
});
group('Complex scenarios and interactions', () {
test('allOf with type constraint on parent schema', () {
// Schema is explicitly a string, and also has allOf constraints.
// To achieve this, we construct the map directly.
final schema = Schema.fromMap({
'type': JsonType.string.typeName,
'minLength': 2, // This is from the main schema's direct validation
'allOf': [
// This is from allOf
StringSchema(maxLength: 5),
StringSchema(pattern: r'^[a-z]+$'),
],
});
// Fails minLength (from parent StringSchema) and pattern (from allOf)
expectFailuresExact(schema, 'A', [
ValidationError(ValidationErrorType.allOfNotMet, path: []),
ValidationError(
ValidationErrorType.minLengthNotMet,
path: [],
details: 'String "A" is not at least 2 characters long',
),
ValidationError(
ValidationErrorType.patternMismatch,
path: [],
details: 'String "A" doesn\'t match the pattern "^[a-z]+\$"',
),
]);
// Fails maxLength (from allOf)
expectFailuresExact(schema, 'abcdef', [
ValidationError(ValidationErrorType.allOfNotMet, path: []),
ValidationError(
ValidationErrorType.maxLengthExceeded,
path: [],
details: 'String "abcdef" is more than 5 characters long',
),
]);
});
test('Object with property value invalid (deeper failure)', () {
final schema = ObjectSchema(
properties: {
'user': ObjectSchema(
properties: {'name': StringSchema(minLength: 5)},
),
},
);
// 'user.name' is "hi" which fails minLength: 5
// _validate(StringSchema(minLength:5), "hi") -> [minLengthNotMet]
// This becomes propertyValueInvalid for 'name'
// Then this becomes propertyValueInvalid for 'user'
expectFailuresExact(
schema,
{
'user': {'name': 'hi'},
},
[
ValidationError(
ValidationErrorType.minLengthNotMet,
path: ['user', 'name'],
details: 'String "hi" is not at least 5 characters long',
),
],
);
});
test('List with item invalid (deeper failure)', () {
final schema = ListSchema(
items: ObjectSchema(properties: {'id': IntegerSchema(minimum: 100)}),
);
// The object {'id': 10} fails IntegerSchema(minimum:100)
// _validate(ObjectSchema(...), {'id':10}) -> [propertyValueInvalid] (due to id:10 failing)
// So the list item is invalid.
expectFailuresExact(
schema,
[
{'id': 101},
{'id': 10}, // This item is invalid
],
[
ValidationError(
ValidationErrorType.minimumNotMet,
path: ['1', 'id'],
details: 'Value 10 is less than the minimum of 100',
),
],
);
});
test('Object with additionalProperties schema', () {
final schema = ObjectSchema(
properties: {'known': StringSchema()},
additionalProperties: IntegerSchema(minimum: 0),
);
// Valid: known property and valid additional property
expectFailuresMatch(schema, {'known': 'yes', 'extraNum': 10}, []);
// Invalid: additional property fails its schema
expectFailuresMatch(
schema,
{'known': 'yes', 'extraNum': -5},
[ValidationError(ValidationErrorType.minimumNotMet, path: const [])],
);
// Invalid: additional property is wrong type for its schema
expectFailuresMatch(
schema,
{'known': 'yes', 'extraStr': 'text'},
[ValidationError(ValidationErrorType.typeMismatch, path: const [])],
);
});
test('Object with unevaluatedProperties: false and patternProperties', () {
final schema = ObjectSchema(
patternProperties: {r'^x-': StringSchema()},
unevaluatedProperties: false,
);
// Valid: matches patternProperty
expectFailuresMatch(schema, {'x-foo': 'bar'}, []);
// Invalid: does not match patternProperty, and unevaluatedProperties is false
expectFailuresMatch(
schema,
{'y-foo': 'bar'},
[
ValidationError(
ValidationErrorType.unevaluatedPropertyNotAllowed,
path: const [],
),
],
);
});
test(
'Object with unevaluatedProperties: false, properties, and additionalProperties: true (should allow unevaluated)',
() {
final schema = ObjectSchema(
properties: {'name': StringSchema()},
additionalProperties: true, // Allows any additional properties
unevaluatedProperties:
false, // This should be superseded by additionalProperties: true for evaluation purposes
);
// 'age' is covered by additionalProperties: true, so it's evaluated (and allowed).
// Thus, unevaluatedProperties: false should not trigger.
expectFailuresMatch(schema, {'name': 'test', 'age': 30}, []);
},
);
test(
'Object with unevaluatedProperties: false, properties, and additionalProperties: Schema (valid additional)',
() {
final schema = ObjectSchema(
properties: {'name': StringSchema()},
additionalProperties: IntegerSchema(),
unevaluatedProperties: false,
);
// 'age' is covered by additionalProperties: IntegerSchema, and 30 is a valid integer.
// So it's evaluated. unevaluatedProperties: false should not trigger.
expectFailuresMatch(schema, {'name': 'test', 'age': 30}, []);
},
);
test(
'Object with unevaluatedProperties: false, properties, and additionalProperties: Schema (invalid additional)',
() {
final schema = ObjectSchema(
properties: {'name': StringSchema()},
additionalProperties: IntegerSchema(minimum: 100),
unevaluatedProperties: false,
);
// 'age' is covered by additionalProperties: IntegerSchema(minimum:100), but 30 is invalid.
// This should result in `additionalPropertyNotAllowed`.
// `unevaluatedProperties` should not trigger because 'age' was subject to evaluation by `additionalProperties`.
expectFailuresMatch(
schema,
{'name': 'test', 'age': 30},
[
ValidationError(
ValidationErrorType.minimumNotMet,
path: const [],
),
],
);
},
);
test('List with unevaluatedItems: false and items schema', () {
final schema = ListSchema(
items: IntegerSchema(), // Applies to all items if no prefixItems
unevaluatedItems: false,
);
// Valid: all items match IntegerSchema
expectFailuresMatch(schema, [1, 2, 3], []);
// Invalid: one item does not match IntegerSchema, results in itemInvalid
// unevaluatedItems:false should not trigger because items schema applies.
expectFailuresMatch(
schema,
[1, 'b', 3],
[ValidationError(ValidationErrorType.typeMismatch, path: const [])],
);
});
test(
'List with unevaluatedItems: false, prefixItems, and items schema',
() {
final schema = ListSchema(
prefixItems: [StringSchema()],
items: IntegerSchema(), // Applies to items after prefixItems
unevaluatedItems: false,
);
// Valid
expectFailuresMatch(schema, ['a', 1, 2], []);
// Invalid: item after prefixItems fails IntegerSchema
expectFailuresMatch(
schema,
['a', 1, 'c'],
[ValidationError(ValidationErrorType.typeMismatch, path: const [])],
);
// Invalid: prefixItem fails StringSchema
expectFailuresMatch(
schema,
[10, 1, 2],
[ValidationError(ValidationErrorType.typeMismatch, path: const [])],
);
},
);
test(
'Schema with no type but with type-specific constraints (e.g. minLength on a generic schema)',
() {
// This schema is malformed from a JSON Schema perspective, as
// minLength only applies to strings. The current validator will apply
// combinators first. Then, if schema.type is null, it won't call
// _validateString, _validateNumber etc. So, minLength would be
// ignored if not for a sub-schema in allOf/anyOf/oneOf/not.
final schemaWithMinLengthNoType = Schema.fromMap({'minLength': 5});
expectFailuresMatch(
schemaWithMinLengthNoType,
'hi',
[],
); // minLength is ignored as type is not string
expectFailuresMatch(
schemaWithMinLengthNoType,
12345,
[],
); // minLength is ignored
final s = Schema.fromMap({
'type': JsonType.string.typeName,
'minLength': 5,
'allOf': [
Schema.fromMap({'pattern': r'^[a-z]+$'}),
],
});
expectFailuresExact(s, 'Hi', [
ValidationError(
ValidationErrorType.minLengthNotMet,
path: [],
details: 'String "Hi" is not at least 5 characters long',
),
ValidationError(ValidationErrorType.allOfNotMet, path: []),
ValidationError(
ValidationErrorType.patternMismatch,
path: [],
details: 'String "Hi" doesn\'t match the pattern "^[a-z]+\$"',
),
]);
// "Hiall" passes minLength.
// allOf part passes.
// Result: []
expectFailuresExact(s, 'Hiall', [
ValidationError(ValidationErrorType.allOfNotMet, path: []),
ValidationError(
ValidationErrorType.patternMismatch,
path: [],
details: 'String "Hiall" doesn\'t match the pattern "^[a-z]+\$"',
),
]);
// To make the pattern apply, it needs to be a StringSchema too.
final s2 = Schema.fromMap({
'type': JsonType.string.typeName,
'minLength': 5,
'allOf': [StringSchema(pattern: r'^[a-z]+$')],
});
// "LongEnoughButCAPS"
// minLength:5 passes.
// allOf: StringSchema(pattern:
// '^[a-z]+$').validate("LongEnoughButCAPS") -> [patternMismatch]
// So allOf fails.
// Result: [allOfNotMet, patternMismatch]
expectFailuresExact(s2, 'LongEnoughButCAPS', [
ValidationError(ValidationErrorType.allOfNotMet, path: []),
ValidationError(
ValidationErrorType.patternMismatch,
path: [],
details:
'String "LongEnoughButCAPS" doesn\'t match the pattern "^[a-z]+\$"',
),
]);
},
);
});
});
group('Tool Communication', () {
test('can call a tool', () async {
final environment = TestEnvironment(
TestMCPClient(),
(channel) => TestMCPServerWithTools(
channel,
tools: [
Tool(
name: 'foo',
inputSchema: ObjectSchema(properties: {'bar': StringSchema()}),
),
],
toolHandlers: {
'foo': (CallToolRequest request) {
return CallToolResult(
content: [
TextContent(
text: (request.arguments as Map)['bar'] as String,
),
],
);
},
},
),
);
final serverConnection = environment.serverConnection;
await serverConnection.initialize(
InitializeRequest(
protocolVersion: ProtocolVersion.latestSupported,
capabilities: environment.client.capabilities,
clientInfo: environment.client.implementation,
),
);
final request = CallToolRequest(name: 'foo', arguments: {'bar': 'baz'});
final result = await serverConnection.callTool(request);
expect(result.content, hasLength(1));
expect(result.content.first, isA<TextContent>());
final textContent = result.content.first as TextContent;
expect(textContent.text, 'baz');
});
test('can return a resource link', () async {
final environment = TestEnvironment(
TestMCPClient(),
(channel) => TestMCPServerWithTools(
channel,
tools: [Tool(name: 'foo', inputSchema: ObjectSchema())],
toolHandlers: {
'foo': (request) {
return CallToolResult(
content: [
ResourceLink(
name: 'foo',
description: 'a description',
uri: 'https://google.com',
mimeType: 'text/html',
),
],
);
},
},
),
);
final serverConnection = environment.serverConnection;
await serverConnection.initialize(
InitializeRequest(
protocolVersion: ProtocolVersion.latestSupported,
capabilities: environment.client.capabilities,
clientInfo: environment.client.implementation,
),
);
final request = CallToolRequest(name: 'foo', arguments: {});
final result = await serverConnection.callTool(request);
expect(result.content, hasLength(1));
expect(result.content.first, isA<ResourceLink>());
final resourceLink = result.content.first as ResourceLink;
expect(resourceLink.name, 'foo');
expect(resourceLink.description, 'a description');
expect(resourceLink.uri, 'https://google.com');
expect(resourceLink.mimeType, 'text/html');
});
test('can return structured content', () async {
final environment = TestEnvironment(
TestMCPClient(),
(channel) => TestMCPServerWithTools(
channel,
tools: [
Tool(
name: 'foo',
inputSchema: ObjectSchema(),
outputSchema: ObjectSchema(properties: {'bar': StringSchema()}),
),
],
toolHandlers: {
'foo': (request) {
return CallToolResult(
content: [],
structuredContent: {'bar': 'baz'},
);
},
},
),
);
final serverConnection = environment.serverConnection;
await serverConnection.initialize(
InitializeRequest(
protocolVersion: ProtocolVersion.latestSupported,
capabilities: environment.client.capabilities,
clientInfo: environment.client.implementation,
),
);
final request = CallToolRequest(name: 'foo', arguments: {});
final result = await serverConnection.callTool(request);
expect(result.structuredContent, {'bar': 'baz'});
});
});
}
base class TestMCPServerWithTools extends TestMCPServer with ToolsSupport {
final List<Tool> _initialTools;
final Map<String, FutureOr<CallToolResult> Function(CallToolRequest)>
_initialToolHandlers;
TestMCPServerWithTools(
super.channel, {
List<Tool> tools = const [],
Map<String, FutureOr<CallToolResult> Function(CallToolRequest)>
toolHandlers =
const {},
}) : _initialTools = tools,
_initialToolHandlers = toolHandlers;
@override
FutureOr<InitializeResult> initialize(InitializeRequest request) async {
final result = await super.initialize(request);
for (final tool in _initialTools) {
final handler = _initialToolHandlers[tool.name];
if (handler != null) {
registerTool(tool, handler);
} else {
throw StateError('No handler provided for tool: ${tool.name}');
}
}
return result;
}
}