Find crop codes under crop and document unreadable avatar files

This commit is contained in:
envoyr committed 2026-11-07 00:36:52 +00:00
1 parent 0ccf46d69a
commit 57efac2bf5
5 files changed
+51 -4

No files matched your search

+1 -1
View File
@@ -1187,7 +1187,7 @@ $user->removeAvatar(); // back
| 401 | `unauthenticated` | | 401 | `unauthenticated` |
| 403 | `missing_scope` (no `user` scope), `user_token_required` (an app token: there is no user to change) | | 403 | `missing_scope` (no `user` scope), `user_token_required` (an app token: there is no user to change) |
| 413 | `avatar_too_large` (the upload is larger than the server accepts) | | 413 | `avatar_too_large` (the upload is larger than the server accepts) |
| 422 | `ValidationFailedException`, codes per field: `name_invalid`, `username_taken`, `username_invalid`, `email_taken`, `email_invalid`, `avatar_invalid`, `avatar_too_large`, `crop_invalid`, else `validation_failed` | | 422 | `ValidationFailedException`, codes per field: `name_invalid`, `username_taken`, `username_invalid`, `email_taken`, `email_invalid`, `avatar_invalid`, `avatar_too_large`, `crop_invalid` (keyed by `crop.x`, `crop.y` or `crop.size`; `errorCodeFor('crop')` finds it), else `validation_failed` |
| 429 | `too_many_requests` | | 429 | `too_many_requests` |
Branch on the codes, the messages are for people and may change. Branch on the codes, the messages are for people and may change.
+1 -1
View File
@@ -1187,7 +1187,7 @@ $user->removeAvatar(); // back
| 401 | `unauthenticated` | | 401 | `unauthenticated` |
| 403 | `missing_scope` (no `user` scope), `user_token_required` (an app token: there is no user to change) | | 403 | `missing_scope` (no `user` scope), `user_token_required` (an app token: there is no user to change) |
| 413 | `avatar_too_large` (the upload is larger than the server accepts) | | 413 | `avatar_too_large` (the upload is larger than the server accepts) |
| 422 | `ValidationFailedException`, codes per field: `name_invalid`, `username_taken`, `username_invalid`, `email_taken`, `email_invalid`, `avatar_invalid`, `avatar_too_large`, `crop_invalid`, else `validation_failed` | | 422 | `ValidationFailedException`, codes per field: `name_invalid`, `username_taken`, `username_invalid`, `email_taken`, `email_invalid`, `avatar_invalid`, `avatar_too_large`, `crop_invalid` (keyed by `crop.x`, `crop.y` or `crop.size`; `errorCodeFor('crop')` finds it), else `validation_failed` |
| 429 | `too_many_requests` | | 429 | `too_many_requests` |
Branch on the codes, the messages are for people and may change. Branch on the codes, the messages are for people and may change.
+2 -1
View File
@@ -72,9 +72,10 @@ trait ManagesProfile
* @param array{x: int, y: int, size: int}|null $crop the square to use, in pixels of the * @param array{x: int, y: int, size: int}|null $crop the square to use, in pixels of the
* upright picture; the centre without one * upright picture; the centre without one
* @throws ValidationFailedException 422 with the codes "avatar_invalid", "avatar_too_large", * @throws ValidationFailedException 422 with the codes "avatar_invalid", "avatar_too_large",
* "crop_invalid" * "crop_invalid" (keyed by crop.x, crop.y or crop.size; errorCodeFor('crop') finds them)
* @throws ApiException 413 "avatar_too_large", 401, 403 "missing_scope", "user_token_required", * @throws ApiException 413 "avatar_too_large", 401, 403 "missing_scope", "user_token_required",
* 429 "too_many_requests" (also more than 10 uploads an hour) * 429 "too_many_requests" (also more than 10 uploads an hour)
* @throws \RuntimeException when the file can't be opened (nothing is sent then)
*/ */
public function uploadAvatar(string|SplFileInfo $file, ?array $crop = null): User public function uploadAvatar(string|SplFileInfo $file, ?array $crop = null): User
{ {
@@ -19,9 +19,25 @@ class ValidationFailedException extends ApiException
return is_object($codes) || is_array($codes) ? array_map('strval', (array) $codes) : []; return is_object($codes) || is_array($codes) ? array_map('strval', (array) $codes) : [];
} }
/**
* The code of $field, or of its first failed sub-field when the codes are keyed by those
* (`crop` finds `crop.x`, `crop.y` or `crop.size`).
*/
public function errorCodeFor(string $field): ?string public function errorCodeFor(string $field): ?string
{ {
return $this->errorCodes()[$field] ?? null; $codes = $this->errorCodes();
if (isset($codes[$field])) {
return $codes[$field];
}
foreach ($codes as $key => $code) {
if (str_starts_with((string) $key, $field . '.')) {
return $code;
}
}
return null;
} }
/** /**
+30
View File
@@ -198,6 +198,36 @@ class UserProfileTest extends TestCase
} }
} }
public function test_crop_codes_are_found_under_the_crop_field(): void
{
$path = tempnam(sys_get_temp_dir(), 'avatar');
file_put_contents($path, 'a picture');
$this->respond(422, $this->validationError(['crop.size' => 'crop_invalid']));
try {
$this->owner()->uploadAvatar($path, ['x' => 0, 'y' => 0]);
$this->fail('No exception thrown');
} catch (ValidationFailedException $e) {
$this->assertSame('crop_invalid', $e->errorCodeFor('crop'));
$this->assertSame('crop_invalid', $e->errorCodeFor('crop.size'));
$this->assertNull($e->errorCodeFor('cro'));
$this->assertNull($e->errorCodeFor('avatar'));
} finally {
unlink($path);
}
}
public function test_an_unreadable_file_throws_before_anything_is_sent(): void
{
try {
$this->owner()->uploadAvatar(sys_get_temp_dir() . '/missing-avatar-' . uniqid() . '.png');
$this->fail('No exception thrown');
} catch (\RuntimeException $e) {
$this->assertNotInstanceOf(ApiException::class, $e);
$this->assertCount(0, $this->sent);
}
}
public function test_remove_avatar_deletes_the_upload_and_clears_the_cache(): void public function test_remove_avatar_deletes_the_upload_and_clears_the_cache(): void
{ {
$owner = $this->owner(); $owner = $this->owner();