Skip to content

Commit 9a8a926

Browse files
committed
improved tests
1 parent 578d28f commit 9a8a926

9 files changed

Lines changed: 849 additions & 11 deletions

File tree

.github/workflows/tests.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
coverage: none
2121

2222
- run: composer install --no-progress --prefer-dist
23-
- run: vendor/bin/tester tests -s -C
23+
- run: composer tester
2424
- if: failure()
2525
uses: actions/upload-artifact@v4
2626
with:
@@ -39,7 +39,7 @@ jobs:
3939
coverage: none
4040

4141
- run: composer update --no-progress --prefer-dist --prefer-lowest --prefer-stable
42-
- run: vendor/bin/tester tests -s -C
42+
- run: composer tester
4343

4444

4545
code_coverage:
@@ -53,7 +53,7 @@ jobs:
5353
coverage: none
5454

5555
- run: composer install --no-progress --prefer-dist
56-
- run: vendor/bin/tester -p phpdbg tests -s -C --coverage ./coverage.xml --coverage-src ./src
56+
- run: composer tester -- -p phpdbg --coverage ./coverage.xml --coverage-src ./src
5757
- run: wget https://github.com/php-coveralls/php-coveralls/releases/download/v2.4.3/php-coveralls.phar
5858
- env:
5959
COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"nette/utils": "^4.0"
2020
},
2121
"require-dev": {
22-
"nette/tester": "^2.5.2",
22+
"nette/tester": "^2.6",
2323
"tracy/tracy": "^2.8",
2424
"phpstan/phpstan-nette": "^2.0@stable"
2525
},
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
<?php declare(strict_types=1);
2+
3+
use Nette\Schema\Expect;
4+
use Nette\Schema\Processor;
5+
use Tester\Assert;
6+
7+
8+
require __DIR__ . '/../bootstrap.php';
9+
10+
11+
test('discriminated union with valid variant one', function () {
12+
$schema = Expect::anyOf(
13+
Expect::structure([
14+
'type' => Expect::anyOf('session')->required(),
15+
'expiration' => Expect::string(),
16+
]),
17+
Expect::structure([
18+
'type' => Expect::anyOf('cookie')->required(),
19+
'name' => Expect::string()->required(),
20+
'domain' => Expect::string(),
21+
]),
22+
);
23+
24+
Assert::equal(
25+
(object) ['type' => 'session', 'expiration' => '20 minutes'],
26+
(new Processor)->process($schema, ['type' => 'session', 'expiration' => '20 minutes']),
27+
);
28+
});
29+
30+
31+
test('discriminated union with valid variant two', function () {
32+
$schema = Expect::anyOf(
33+
Expect::structure([
34+
'type' => Expect::anyOf('session')->required(),
35+
'expiration' => Expect::string(),
36+
]),
37+
Expect::structure([
38+
'type' => Expect::anyOf('cookie')->required(),
39+
'name' => Expect::string()->required(),
40+
'domain' => Expect::string(),
41+
]),
42+
);
43+
44+
Assert::equal(
45+
(object) ['type' => 'cookie', 'name' => 'auth', 'domain' => null],
46+
(new Processor)->process($schema, ['type' => 'cookie', 'name' => 'auth']),
47+
);
48+
});
49+
50+
51+
test('discriminated union with invalid discriminator', function () {
52+
$schema = Expect::anyOf(
53+
Expect::structure([
54+
'type' => Expect::anyOf('session')->required(),
55+
'expiration' => Expect::string(),
56+
]),
57+
Expect::structure([
58+
'type' => Expect::anyOf('cookie')->required(),
59+
'name' => Expect::string()->required(),
60+
]),
61+
);
62+
63+
checkValidationErrors(function () use ($schema) {
64+
(new Processor)->process($schema, ['type' => 'redis']);
65+
}, [
66+
"The item 'type' expects to be 'session', 'redis' given.",
67+
"The item 'type' expects to be 'cookie', 'redis' given.",
68+
"The mandatory item 'name' is missing.",
69+
]);
70+
});
71+
72+
73+
test('discriminated union with missing required field', function () {
74+
$schema = Expect::anyOf(
75+
Expect::structure([
76+
'type' => Expect::anyOf('session')->required(),
77+
'expiration' => Expect::string(),
78+
]),
79+
Expect::structure([
80+
'type' => Expect::anyOf('cookie')->required(),
81+
'name' => Expect::string()->required(),
82+
]),
83+
);
84+
85+
checkValidationErrors(function () use ($schema) {
86+
(new Processor)->process($schema, ['type' => 'cookie']);
87+
}, [
88+
"The item 'type' expects to be 'session', 'cookie' given.",
89+
"The mandatory item 'name' is missing.",
90+
]);
91+
});
92+
93+
94+
test('discriminated union with three variants', function () {
95+
$schema = Expect::anyOf(
96+
Expect::structure([
97+
'driver' => Expect::anyOf('mysql')->required(),
98+
'host' => Expect::string()->default('localhost'),
99+
'port' => Expect::int()->default(3306),
100+
]),
101+
Expect::structure([
102+
'driver' => Expect::anyOf('sqlite')->required(),
103+
'path' => Expect::string()->required(),
104+
]),
105+
Expect::structure([
106+
'driver' => Expect::anyOf('pgsql')->required(),
107+
'host' => Expect::string()->default('localhost'),
108+
'port' => Expect::int()->default(5432),
109+
]),
110+
);
111+
112+
Assert::equal(
113+
(object) ['driver' => 'sqlite', 'path' => '/tmp/db.sqlite'],
114+
(new Processor)->process($schema, ['driver' => 'sqlite', 'path' => '/tmp/db.sqlite']),
115+
);
116+
117+
Assert::equal(
118+
(object) ['driver' => 'mysql', 'host' => 'localhost', 'port' => 3306],
119+
(new Processor)->process($schema, ['driver' => 'mysql']),
120+
);
121+
122+
Assert::equal(
123+
(object) ['driver' => 'pgsql', 'host' => 'db.example.com', 'port' => 5432],
124+
(new Processor)->process($schema, ['driver' => 'pgsql', 'host' => 'db.example.com']),
125+
);
126+
});
127+
128+
129+
test('nested discriminated unions', function () {
130+
$schema = Expect::structure([
131+
'cache' => Expect::anyOf(
132+
Expect::structure([
133+
'driver' => Expect::anyOf('file')->required(),
134+
'dir' => Expect::string()->required(),
135+
]),
136+
Expect::structure([
137+
'driver' => Expect::anyOf('redis')->required(),
138+
'host' => Expect::string()->default('localhost'),
139+
'port' => Expect::int()->default(6379),
140+
]),
141+
false,
142+
),
143+
]);
144+
145+
Assert::equal(
146+
(object) ['cache' => (object) ['driver' => 'file', 'dir' => '/tmp/cache']],
147+
(new Processor)->process($schema, ['cache' => ['driver' => 'file', 'dir' => '/tmp/cache']]),
148+
);
149+
150+
Assert::equal(
151+
(object) ['cache' => false],
152+
(new Processor)->process($schema, ['cache' => false]),
153+
);
154+
});
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
<?php declare(strict_types=1);
2+
3+
use Nette\Schema\Context;
4+
use Nette\Schema\Expect;
5+
use Nette\Schema\Processor;
6+
use Tester\Assert;
7+
8+
9+
require __DIR__ . '/../bootstrap.php';
10+
11+
12+
test('chaining before, assert, and transform', function () {
13+
$schema = Expect::string()
14+
->before(fn($v) => strtolower($v))
15+
->assert(fn($s) => ctype_alpha($s), 'Must contain only letters')
16+
->transform(fn($v) => ucfirst($v));
17+
18+
Assert::same('Hello', (new Processor)->process($schema, 'HELLO'));
19+
Assert::same('World', (new Processor)->process($schema, 'world'));
20+
21+
checkValidationErrors(function () use ($schema) {
22+
(new Processor)->process($schema, 'HELLO123');
23+
}, ["Failed assertion 'Must contain only letters' for item with value 'hello123'."]);
24+
});
25+
26+
27+
test('multiple assertions in sequence', function () {
28+
$schema = Expect::string()
29+
->assert(ctype_digit(...), 'Must be numeric')
30+
->assert(fn($s) => strlen($s) >= 3, 'Too short')
31+
->assert(fn($s) => strlen($s) <= 10, 'Too long');
32+
33+
Assert::same('12345', (new Processor)->process($schema, '12345'));
34+
35+
checkValidationErrors(function () use ($schema) {
36+
(new Processor)->process($schema, 'abc');
37+
}, ["Failed assertion 'Must be numeric' for item with value 'abc'."]);
38+
39+
checkValidationErrors(function () use ($schema) {
40+
(new Processor)->process($schema, '12');
41+
}, ["Failed assertion 'Too short' for item with value '12'."]);
42+
43+
checkValidationErrors(function () use ($schema) {
44+
(new Processor)->process($schema, '12345678901');
45+
}, ["Failed assertion 'Too long' for item with value '12345678901'."]);
46+
});
47+
48+
49+
test('pattern() combined with min/max', function () {
50+
$schema = Expect::string()
51+
->pattern('[a-z]+')
52+
->min(3)
53+
->max(10);
54+
55+
Assert::same('hello', (new Processor)->process($schema, 'hello'));
56+
57+
checkValidationErrors(function () use ($schema) {
58+
(new Processor)->process($schema, 'Hello');
59+
}, ["The item expects to match pattern '[a-z]+', 'Hello' given."]);
60+
61+
checkValidationErrors(function () use ($schema) {
62+
(new Processor)->process($schema, 'ab');
63+
}, ['The length of item expects to be in range 3..10, 2 bytes given.']);
64+
65+
checkValidationErrors(function () use ($schema) {
66+
(new Processor)->process($schema, 'abcdefghijk');
67+
}, ['The length of item expects to be in range 3..10, 11 bytes given.']);
68+
});
69+
70+
71+
test('transform with validation using context', function () {
72+
$schema = Expect::string()
73+
->transform(function (string $s, Context $context) {
74+
if (!ctype_lower($s)) {
75+
$context->addError('All characters must be lowercase', 'validation.lowercase');
76+
return null;
77+
}
78+
return strtoupper($s);
79+
});
80+
81+
Assert::same('HELLO', (new Processor)->process($schema, 'hello'));
82+
83+
checkValidationErrors(function () use ($schema) {
84+
(new Processor)->process($schema, 'Hello');
85+
}, ['All characters must be lowercase']);
86+
});
87+
88+
89+
test('multiple transforms in sequence', function () {
90+
$schema = Expect::string()
91+
->transform(fn($s) => trim($s))
92+
->transform(fn($s) => strtolower($s))
93+
->transform(fn($s) => ucwords($s));
94+
95+
Assert::same('Hello World', (new Processor)->process($schema, ' HELLO WORLD '));
96+
});
97+
98+
99+
test('assert() and transform interleaved', function () {
100+
$schema = Expect::int()
101+
->assert(fn($v) => $v > 0, 'Must be positive')
102+
->transform(fn($v) => $v * 2)
103+
->assert(fn($v) => $v < 100, 'Result too large')
104+
->transform(fn($v) => (string) $v);
105+
106+
Assert::same('20', (new Processor)->process($schema, 10));
107+
108+
checkValidationErrors(function () use ($schema) {
109+
(new Processor)->process($schema, -5);
110+
}, ["Failed assertion 'Must be positive' for item with value -5."]);
111+
112+
checkValidationErrors(function () use ($schema) {
113+
(new Processor)->process($schema, 60);
114+
}, ["Failed assertion 'Result too large' for item with value 120."]);
115+
});
116+
117+
118+
test('before() normalization with type conversion', function () {
119+
$schema = Expect::int()
120+
->before(fn($v) => is_string($v) ? (int) $v : $v)
121+
->min(1)
122+
->max(100);
123+
124+
Assert::same(42, (new Processor)->process($schema, '42'));
125+
Assert::same(42, (new Processor)->process($schema, 42));
126+
127+
checkValidationErrors(function () use ($schema) {
128+
(new Processor)->process($schema, '0');
129+
}, ['The item expects to be in range 1..100, 0 given.']);
130+
});
131+
132+
133+
test('castTo() combined with transformations', function () {
134+
$schema = Expect::string()
135+
->pattern('\d{4}-\d{2}-\d{2}')
136+
->castTo(DateTime::class);
137+
138+
$result = (new Processor)->process($schema, '2024-01-15');
139+
Assert::type(DateTime::class, $result);
140+
Assert::same('2024-01-15', $result->format('Y-m-d'));
141+
142+
checkValidationErrors(function () use ($schema) {
143+
(new Processor)->process($schema, 'invalid-date');
144+
}, ["The item expects to match pattern '\\d{4}-\\d{2}-\\d{2}', 'invalid-date' given."]);
145+
});
146+
147+
148+
test('nullable with all validators', function () {
149+
$schema = Expect::string()
150+
->nullable()
151+
->pattern('[a-z]+')
152+
->min(3)
153+
->transform(fn($v) => $v ? strtoupper($v) : null);
154+
155+
Assert::same('HELLO', (new Processor)->process($schema, 'hello'));
156+
Assert::same(null, (new Processor)->process($schema, null));
157+
158+
checkValidationErrors(function () use ($schema) {
159+
(new Processor)->process($schema, 'ab');
160+
}, ['The length of item expects to be in range 3.., 2 bytes given.']);
161+
});
162+
163+
164+
test('deprecated combined with other validators', function () {
165+
$schema = Expect::string()
166+
->deprecated('Use newField instead')
167+
->pattern('\w+');
168+
169+
$processor = new Processor;
170+
Assert::same('test', $processor->process($schema, 'test'));
171+
Assert::same(['Use newField instead'], $processor->getWarnings());
172+
});
173+
174+
175+
test('deprecated does not warn when value not provided', function () {
176+
$schema = Expect::structure([
177+
'oldField' => Expect::string()->deprecated('Use newField instead'),
178+
'newField' => Expect::string(),
179+
]);
180+
181+
$processor = new Processor;
182+
Assert::equal(
183+
(object) ['oldField' => null, 'newField' => 'value'],
184+
$processor->process($schema, ['newField' => 'value']),
185+
);
186+
Assert::same([], $processor->getWarnings());
187+
});
188+
189+
190+
test('complex validation chain on array items', function () {
191+
$schema = Expect::arrayOf(
192+
Expect::string()
193+
->pattern('[a-z]+')
194+
->min(2)
195+
->transform(fn($s) => ucfirst($s)),
196+
);
197+
198+
Assert::same(['Hello', 'World'], (new Processor)->process($schema, ['hello', 'world']));
199+
200+
checkValidationErrors(function () use ($schema) {
201+
(new Processor)->process($schema, ['hello', 'X']);
202+
}, ["The length of item '1' expects to be in range 2.., 1 bytes given."]);
203+
204+
checkValidationErrors(function () use ($schema) {
205+
(new Processor)->process($schema, ['hello', 'World']);
206+
}, ["The item '1' expects to match pattern '[a-z]+', 'World' given."]);
207+
});

0 commit comments

Comments
 (0)