Skip to content

Commit 6c50e6a

Browse files
committed
Add tests
1 parent 1f36feb commit 6c50e6a

File tree

1 file changed

+177
-0
lines changed

1 file changed

+177
-0
lines changed
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
<?php
2+
3+
use Code16\Sharp\Auth\Password\Command\IsChangePasswordCommandTrait;
4+
use Code16\Sharp\EntityList\Commands\SingleInstanceCommand;
5+
use Code16\Sharp\Tests\Fixtures\Entities\SinglePersonEntity;
6+
use Code16\Sharp\Tests\Fixtures\Sharp\SinglePersonShow;
7+
use Code16\Sharp\Tests\Fixtures\User;
8+
use Illuminate\Support\Facades\Hash;
9+
use Illuminate\Testing\Fluent\AssertableJson as Assert;
10+
use Illuminate\Validation\Rules\Password;
11+
12+
beforeEach(function () {
13+
sharp()->config()->declareEntity(SinglePersonEntity::class);
14+
15+
login(new User([
16+
'id' => 123, // ensure RateLimiter key is unique in tests
17+
'password' => Hash::make('secret'),
18+
]));
19+
});
20+
21+
it('exposes proper form fields and label (without confirmation) for change password command', function () {
22+
fakeShowFor(SinglePersonEntity::class, new class() extends SinglePersonShow
23+
{
24+
public function getInstanceCommands(): ?array
25+
{
26+
return [
27+
'change_password' => new class() extends SingleInstanceCommand
28+
{
29+
use IsChangePasswordCommandTrait;
30+
31+
protected function executeSingle(array $data): array
32+
{
33+
// no-op in tests
34+
return $this->reload();
35+
}
36+
},
37+
];
38+
}
39+
});
40+
41+
// Fetch the command form (single show variant)
42+
$this
43+
->getJson(route('code16.sharp.api.show.command.singleInstance.form', [
44+
'entityKey' => 'single-person',
45+
'commandKey' => 'change_password',
46+
]))
47+
->assertOk()
48+
->assertJson(function (Assert $json) {
49+
$json
50+
->where('config.title', trans('sharp::auth.password_change.command.label'))
51+
->where('fields.password.key', 'password')
52+
->where('fields.new_password.key', 'new_password')
53+
->missing('fields.new_password_confirmation')
54+
->etc();
55+
});
56+
});
57+
58+
it('shows confirmation field when enabled and enforces custom password rule and confirmation', function () {
59+
fakeShowFor(SinglePersonEntity::class, new class() extends SinglePersonShow
60+
{
61+
public function getInstanceCommands(): ?array
62+
{
63+
return [
64+
// enable confirmation + a stronger rule
65+
'change_password_confirm' => new class() extends SingleInstanceCommand
66+
{
67+
use IsChangePasswordCommandTrait;
68+
69+
public function buildCommandConfig(): void
70+
{
71+
$this->configureConfirmPassword()
72+
->configurePasswordRule(Password::min(8)->numbers());
73+
}
74+
75+
protected function executeSingle(array $data): array
76+
{
77+
return $this->reload();
78+
}
79+
},
80+
];
81+
}
82+
});
83+
84+
// Form contains the confirmation field
85+
$this
86+
->getJson(route('code16.sharp.api.show.command.singleInstance.form', [
87+
'entityKey' => 'single-person',
88+
'commandKey' => 'change_password_confirm',
89+
]))
90+
->assertOk()
91+
->assertJson(function (Assert $json) {
92+
$json
93+
->where('fields.password.key', 'password')
94+
->where('fields.new_password.key', 'new_password')
95+
->where('fields.new_password_confirmation.key', 'new_password_confirmation')
96+
->etc();
97+
});
98+
99+
// Fails when confirmation is missing/mismatch
100+
$this
101+
->postJson(route('code16.sharp.api.show.command.instance', ['single-person', 'change_password_confirm']), [
102+
'data' => [
103+
'password' => 'secret',
104+
'new_password' => 'Password1', // missing confirmation
105+
],
106+
])
107+
->assertUnprocessable()
108+
->assertJsonValidationErrors(['new_password']);
109+
110+
// Fails when password rule is not satisfied (requires number)
111+
$this
112+
->postJson(route('code16.sharp.api.show.command.instance', ['single-person', 'change_password_confirm']), [
113+
'data' => [
114+
'password' => 'secret',
115+
'new_password' => 'Password!',
116+
'new_password_confirmation' => 'Password!',
117+
],
118+
])
119+
->assertUnprocessable()
120+
->assertJsonValidationErrors(['new_password']);
121+
122+
// Succeeds with valid data
123+
$this
124+
->postJson(route('code16.sharp.api.show.command.instance', ['single-person', 'change_password_confirm']), [
125+
'data' => [
126+
'password' => 'secret',
127+
'new_password' => 'Password1!',
128+
'new_password_confirmation' => 'Password1!',
129+
],
130+
])
131+
->assertOk();
132+
});
133+
134+
it('rate limits after too many attempts and returns a helpful message', function () {
135+
fakeShowFor(SinglePersonEntity::class, new class() extends SinglePersonShow
136+
{
137+
public function getInstanceCommands(): ?array
138+
{
139+
return [
140+
'change_password_rl' => new class() extends SingleInstanceCommand
141+
{
142+
use IsChangePasswordCommandTrait;
143+
144+
protected function executeSingle(array $data): array
145+
{
146+
return $this->reload();
147+
}
148+
},
149+
];
150+
}
151+
});
152+
153+
// Trigger 3 attempts (invalid to keep trying)
154+
for ($i = 0; $i < 3; $i++) {
155+
$this
156+
->postJson(route('code16.sharp.api.show.command.instance', ['single-person', 'change_password_rl']), [
157+
'data' => [
158+
// missing fields triggers validation and consumes an attempt
159+
],
160+
])
161+
->assertUnprocessable();
162+
}
163+
164+
// 4th attempt should be blocked by rate limiter with SharpApplicativeException (417)
165+
$this
166+
->postJson(route('code16.sharp.api.show.command.instance', ['single-person', 'change_password_rl']), [
167+
'data' => [
168+
// still invalid
169+
],
170+
])
171+
->assertStatus(417)
172+
->assertJson(function (Assert $json) {
173+
$json->where('message', function ($message) {
174+
return is_string($message) && str_starts_with($message, 'You have made too many attempts.');
175+
});
176+
});
177+
});

0 commit comments

Comments
 (0)