fix: regressions

This commit is contained in:
Sami Mazouz 2024-02-16 14:36:05 +01:00
parent 4e3b5f7c2c
commit bc5bac0ac4
No known key found for this signature in database
58 changed files with 551 additions and 476 deletions

View File

@ -91,11 +91,12 @@ class GroupMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@"InvalidGroup"#g99',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
]
],
],
@ -166,11 +167,12 @@ class GroupMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@"Mods"#g4',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
]
]
]
@ -198,11 +200,12 @@ class GroupMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@"Admins"#g1 @"Mods"#g4',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
]
]
]
@ -232,11 +235,12 @@ class GroupMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@"Members"#g3 @"Guests"#g2',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
]
]
]
@ -288,11 +292,12 @@ class GroupMentionsTest extends TestCase
'authenticatedAs' => 3,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@"Mods"#g4',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
],
],
],
@ -319,11 +324,12 @@ class GroupMentionsTest extends TestCase
'authenticatedAs' => 4,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@"Mods"#g4',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
],
],
],
@ -350,11 +356,12 @@ class GroupMentionsTest extends TestCase
'authenticatedAs' => 4,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@"Ninjas"#g10',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
],
],
],
@ -381,6 +388,7 @@ class GroupMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => 'New content with @"Mods"#g4 mention',
],

View File

@ -82,11 +82,12 @@ class PostMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@potato#4',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
],
],
],
@ -113,11 +114,12 @@ class PostMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@"POTATO$"#p4',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
],
],
],
@ -144,11 +146,12 @@ class PostMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@"potato"#p50',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
],
],
],
@ -175,11 +178,12 @@ class PostMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@“POTATO$”#p4',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
],
],
],
@ -206,11 +210,12 @@ class PostMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@"franzofflarum"#p215',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
],
],
],
@ -237,11 +242,12 @@ class PostMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@"TOBY$"#p5 @"flarum"#2015 @"franzofflarum"#220 @"POTATO$"#3 @"POTATO$"#p4',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
],
],
],
@ -384,11 +390,12 @@ class PostMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@"Bad "#p6 User"#p9',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
],
],
],
@ -436,11 +443,12 @@ class PostMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@"Bad _ User"#p9',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
],
],
],
@ -467,6 +475,7 @@ class PostMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@"Bad _ User"#p9',
],
@ -495,6 +504,7 @@ class PostMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@"Bad _ User"#p9',
],
@ -523,6 +533,7 @@ class PostMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@"acme"#p11',
],

View File

@ -68,11 +68,12 @@ class TagMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '#flarum',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
],
],
],
@ -96,11 +97,12 @@ class TagMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '#戦い',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
],
],
],
@ -125,11 +127,12 @@ class TagMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '#franzofflarum',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
],
],
],
@ -155,11 +158,12 @@ class TagMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '#test',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
],
],
],
@ -183,11 +187,12 @@ class TagMentionsTest extends TestCase
'authenticatedAs' => 3,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '#dev',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
],
],
],
@ -211,11 +216,12 @@ class TagMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '#dev',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
],
],
],
@ -239,11 +245,12 @@ class TagMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '#test #flarum #support #laravel #franzofflarum',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
],
],
],
@ -365,6 +372,7 @@ class TagMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '#laravel',
],

View File

@ -72,11 +72,12 @@ class UserMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@potato',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
],
],
],
@ -105,11 +106,12 @@ class UserMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@potato',
],
'relationships' => [
'discussion' => ['data' => ['id' => 2]],
'discussion' => ['data' => ['type' => 'discussions', 'id' => 2]],
],
],
],
@ -136,6 +138,7 @@ class UserMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@"POTATO$"#3',
],
@ -167,6 +170,7 @@ class UserMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@“POTATO$”#3',
],
@ -198,6 +202,7 @@ class UserMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@"franzofflarum"#82',
],
@ -229,6 +234,7 @@ class UserMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@"TOBY$"#4 @"POTATO$"#p4 @"franzofflarum"#82 @"POTATO$"#3',
],
@ -282,6 +288,7 @@ class UserMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@"potato_"#3',
],
@ -312,6 +319,7 @@ class UserMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@"potato_"#3',
],
@ -367,6 +375,7 @@ class UserMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@"Bad "#p6 User"#5',
],
@ -419,6 +428,7 @@ class UserMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@"Bad _ User"#5',
],
@ -450,6 +460,7 @@ class UserMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@"Bad _ User"#5',
],
@ -478,6 +489,7 @@ class UserMentionsTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '@"Bad _ User"#5',
],

View File

@ -45,6 +45,7 @@ class UpdateTest extends TestCase
'authenticatedAs' => 2,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'nickname' => 'new nickname',
],
@ -72,6 +73,7 @@ class UpdateTest extends TestCase
'authenticatedAs' => 2,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'nickname' => 'new nickname',
],

View File

@ -119,6 +119,7 @@ class ReplyNotificationTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'title' => 'ACME',
],
@ -133,6 +134,7 @@ class ReplyNotificationTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'lastReadPostNumber' => 2,
],
@ -148,6 +150,7 @@ class ReplyNotificationTest extends TestCase
'authenticatedAs' => 2,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => 'reply with predetermined content for automated testing - too-obscure',
],
@ -203,6 +206,7 @@ class ReplyNotificationTest extends TestCase
'authenticatedAs' => 3,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => 'reply with predetermined content for automated testing - too-obscure',
],
@ -249,6 +253,7 @@ class ReplyNotificationTest extends TestCase
'authenticatedAs' => 4,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => 'reply with predetermined content for automated testing - too-obscure',
],
@ -270,6 +275,7 @@ class ReplyNotificationTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'isApproved' => 1,
],
@ -309,6 +315,7 @@ class ReplyNotificationTest extends TestCase
'authenticatedAs' => 3,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => 'restricted-test-post',
],

View File

@ -45,6 +45,7 @@ class UseForumTest extends TestCase
'authenticatedAs' => 2,
'json' => [
'data' => [
'type' => 'discussions',
'attributes' => [
'title' => 'Test post',
'content' => '<t><p>Hello, world!</p></t>'
@ -65,6 +66,7 @@ class UseForumTest extends TestCase
'authenticatedAs' => 2,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => '<t><p>Hello, world!</p></t>'
],

View File

@ -91,6 +91,7 @@ class SuspendUserTest extends TestCase
'authenticatedAs' => $authenticatedAs,
'json' => [
'data' => [
'type' => 'users',
'attributes' => [
'suspendedUntil' => Carbon::now()->addDay(),
'suspendReason' => 'Suspended for acme reasons.',

View File

@ -53,6 +53,7 @@ class CreateTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'discussions',
'attributes' => [
'title' => 'test - too-obscure',
'content' => 'predetermined content for automated testing - too-obscure',
@ -75,6 +76,7 @@ class CreateTest extends TestCase
'authenticatedAs' => 2,
'json' => [
'data' => [
'type' => 'discussions',
'attributes' => [
'title' => 'test - too-obscure',
'content' => 'predetermined content for automated testing - too-obscure',
@ -103,6 +105,7 @@ class CreateTest extends TestCase
'authenticatedAs' => 2,
'json' => [
'data' => [
'type' => 'discussions',
'attributes' => [
'title' => 'test - too-obscure',
'content' => 'predetermined content for automated testing - too-obscure',
@ -125,6 +128,7 @@ class CreateTest extends TestCase
'authenticatedAs' => 2,
'json' => [
'data' => [
'type' => 'discussions',
'attributes' => [
'title' => 'test - too-obscure',
'content' => 'predetermined content for automated testing - too-obscure',
@ -154,6 +158,7 @@ class CreateTest extends TestCase
'authenticatedAs' => 2,
'json' => [
'data' => [
'type' => 'discussions',
'attributes' => [
'title' => 'test - too-obscure',
'content' => 'predetermined content for automated testing - too-obscure',
@ -189,6 +194,7 @@ class CreateTest extends TestCase
'authenticatedAs' => 2,
'json' => [
'data' => [
'type' => 'discussions',
'attributes' => [
'title' => 'test - too-obscure',
'content' => 'predetermined content for automated testing - too-obscure',
@ -218,6 +224,7 @@ class CreateTest extends TestCase
'authenticatedAs' => 2,
'json' => [
'data' => [
'type' => 'discussions',
'attributes' => [
'title' => 'test - too-obscure',
'content' => 'predetermined content for automated testing - too-obscure',
@ -248,6 +255,7 @@ class CreateTest extends TestCase
'authenticatedAs' => 2,
'json' => [
'data' => [
'type' => 'discussions',
'attributes' => [
'title' => 'test - too-obscure',
'content' => 'predetermined content for automated testing - too-obscure',
@ -277,6 +285,7 @@ class CreateTest extends TestCase
'authenticatedAs' => 2,
'json' => [
'data' => [
'type' => 'discussions',
'attributes' => [
'title' => 'test - too-obscure',
'content' => 'predetermined content for automated testing - too-obscure',
@ -307,6 +316,7 @@ class CreateTest extends TestCase
'authenticatedAs' => 2,
'json' => [
'data' => [
'type' => 'discussions',
'attributes' => [
'title' => 'test - too-obscure',
'content' => 'predetermined content for automated testing - too-obscure',

View File

@ -225,6 +225,7 @@ class UpdateTest extends TestCase
'authenticatedAs' => 2,
'json' => [
'data' => [
'type' => 'discussions',
'attributes' => [
'title' => 'test - too-obscure',
'content' => 'predetermined content for automated testing - too-obscure',

View File

@ -75,6 +75,7 @@ class CreateTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'tags',
'attributes' => [
'name' => 'Dev Blog',
'slug' => 'dev-blog',

View File

@ -1,106 +1,139 @@
validation:
accepted: "The :attribute must be accepted."
active_url: "The :attribute is not a valid URL."
after: "The :attribute must be a date after :date."
after_or_equal: "The :attribute must be a date after or equal to :date."
alpha: "The :attribute must only contain letters."
alpha_dash: "The :attribute must only contain letters, numbers, dashes and underscores."
alpha_num: "The :attribute must only contain letters and numbers."
array: "The :attribute must be an array."
before: "The :attribute must be a date before :date."
before_or_equal: "The :attribute must be a date before or equal to :date."
accepted: "The :attribute field must be accepted."
accepted_if: "The :attribute field must be accepted when :other is :value."
active_url: "The :attribute field must be a valid URL."
after: "The :attribute field must be a date after :date."
after_or_equal: "The :attribute field must be a date after or equal to :date."
alpha: "The :attribute field must only contain letters."
alpha_dash: "The :attribute field must only contain letters, numbers, dashes, and underscores."
alpha_num: "The :attribute field must only contain letters and numbers."
array: "The :attribute field must be an array."
ascii: "The :attribute field must only contain single-byte alphanumeric characters and symbols."
before: "The :attribute field must be a date before :date."
before_or_equal: "The :attribute field must be a date before or equal to :date."
between:
numeric: "The :attribute must be between :min and :max."
file: "The :attribute must be between :min and :max kilobytes."
string: "The :attribute must be between :min and :max characters."
array: "The :attribute must have between :min and :max items."
array: "The :attribute field must have between :min and :max items."
file: "The :attribute field must be between :min and :max kilobytes."
numeric: "The :attribute field must be between :min and :max."
string: "The :attribute field must be between :min and :max characters."
boolean: "The :attribute field must be true or false."
confirmed: "The :attribute confirmation does not match."
date: "The :attribute is not a valid date."
date_equals: "The :attribute must be a date equal to :date."
date_format: "The :attribute does not match the format :format."
different: "The :attribute and :other must be different."
digits: "The :attribute must be :digits digits."
digits_between: "The :attribute must be between :min and :max digits."
dimensions: "The :attribute has invalid image dimensions."
can: "The :attribute field contains an unauthorized value."
confirmed: "The :attribute field confirmation does not match."
current_password: "The password is incorrect."
date: "The :attribute field must be a valid date."
date_equals: "The :attribute field must be a date equal to :date."
date_format: "The :attribute field must match the format :format."
decimal: "The :attribute field must have :decimal decimal places."
declined: "The :attribute field must be declined."
declined_if: "The :attribute field must be declined when :other is :value."
different: "The :attribute field and :other must be different."
digits: "The :attribute field must be :digits digits."
digits_between: "The :attribute field must be between :min and :max digits."
dimensions: "The :attribute field has invalid image dimensions."
distinct: "The :attribute field has a duplicate value."
email: "The :attribute must be a valid email address."
ends_with: "The :attribute must end with one of the following: :values."
doesnt_end_with: "The :attribute field must not end with one of the following: :values."
doesnt_start_with: "The :attribute field must not start with one of the following: :values."
email: "The :attribute field must be a valid email address."
ends_with: "The :attribute field must end with one of the following: :values."
enum: "The selected :attribute is invalid."
exists: "The selected :attribute is invalid."
file: "The :attribute must be a file."
file_too_large: "The :attribute is too large."
file_upload_failed: "The :attribute failed to upload."
extensions: "The :attribute field must have one of the following extensions: :values."
file: "The :attribute field must be a file."
filled: "The :attribute field must have a value."
gt:
numeric: "The :attribute must be greater than :value."
file: "The :attribute must be greater than :value kilobytes."
string: "The :attribute must be greater than :value characters."
array: "The :attribute must have more than :value items."
array: "The :attribute field must have more than :value items."
file: "The :attribute field must be greater than :value kilobytes."
numeric: "The :attribute field must be greater than :value."
string: "The :attribute field must be greater than :value characters."
gte:
numeric: "The :attribute must be greater than or equal :value."
file: "The :attribute must be greater than or equal :value kilobytes."
string: "The :attribute must be greater than or equal :value characters."
array: "The :attribute must have :value items or more."
image: "The :attribute must be an image."
array: "The :attribute field must have :value items or more."
file: "The :attribute field must be greater than or equal to :value kilobytes."
numeric: "The :attribute field must be greater than or equal to :value."
string: "The :attribute field must be greater than or equal to :value characters."
hex_color: "The :attribute field must be a valid hexadecimal color."
image: "The :attribute field must be an image."
in: "The selected :attribute is invalid."
in_array: "The :attribute field does not exist in :other."
integer: "The :attribute must be an integer."
ip: "The :attribute must be a valid IP address."
ipv4: "The :attribute must be a valid IPv4 address."
ipv6: "The :attribute must be a valid IPv6 address."
json: "The :attribute must be a valid JSON string."
in_array: "The :attribute field must exist in :other."
integer: "The :attribute field must be an integer."
ip: "The :attribute field must be a valid IP address."
ipv4: "The :attribute field must be a valid IPv4 address."
ipv6: "The :attribute field must be a valid IPv6 address."
json: "The :attribute field must be a valid JSON string."
lowercase: "The :attribute field must be lowercase."
lt:
numeric: "The :attribute must be less than :value."
file: "The :attribute must be less than :value kilobytes."
string: "The :attribute must be less than :value characters."
array: "The :attribute must have less than :value items."
array: "The :attribute field must have less than :value items."
file: "The :attribute field must be less than :value kilobytes."
numeric: "The :attribute field must be less than :value."
string: "The :attribute field must be less than :value characters."
lte:
numeric: "The :attribute must be less than or equal :value."
file: "The :attribute must be less than or equal :value kilobytes."
string: "The :attribute must be less than or equal :value characters."
array: "The :attribute must not have more than :value items."
array: "The :attribute field must not have more than :value items."
file: "The :attribute field must be less than or equal to :value kilobytes."
numeric: "The :attribute field must be less than or equal to :value."
string: "The :attribute field must be less than or equal to :value characters."
mac_address: "The :attribute field must be a valid MAC address."
max:
numeric: "The :attribute must not be greater than :max."
file: "The :attribute must not be greater than :max kilobytes."
string: "The :attribute must not be greater than :max characters."
array: "The :attribute must not have more than :max items."
mimes: "The :attribute must be a file of type: :values."
mimetypes: "The :attribute must be a file of type: :values."
array: "The :attribute field must not have more than :max items."
file: "The :attribute field must not be greater than :max kilobytes."
numeric: "The :attribute field must not be greater than :max."
string: "The :attribute field must not be greater than :max characters."
max_digits: "The :attribute field must not have more than :max digits."
mimes: "The :attribute field must be a file of type: :values."
mimetypes: "The :attribute field must be a file of type: :values."
min:
numeric: "The :attribute must be at least :min."
file: "The :attribute must be at least :min kilobytes."
string: "The :attribute must be at least :min characters."
array: "The :attribute must have at least :min items."
multiple_of: "The :attribute must be a multiple of :value."
array: "The :attribute field must have at least :min items."
file: "The :attribute field must be at least :min kilobytes."
numeric: "The :attribute field must be at least :min."
string: "The :attribute field must be at least :min characters."
min_digits: "The :attribute field must have at least :min digits."
missing: "The :attribute field must be missing."
missing_if: "The :attribute field must be missing when :other is :value."
missing_unless: "The :attribute field must be missing unless :other is :value."
missing_with: "The :attribute field must be missing when :values is present."
missing_with_all: "The :attribute field must be missing when :values are present."
multiple_of: "The :attribute field must be a multiple of :value."
not_in: "The selected :attribute is invalid."
not_regex: "The :attribute format is invalid."
numeric: "The :attribute must be a number."
password: "The password is incorrect."
not_regex: "The :attribute field format is invalid."
numeric: "The :attribute field must be a number."
password:
letters: "The :attribute field must contain at least one letter."
mixed: "The :attribute field must contain at least one uppercase and one lowercase letter."
numbers: "The :attribute field must contain at least one number."
symbols: "The :attribute field must contain at least one symbol."
uncompromised: "The given :attribute has appeared in a data leak. Please choose a different :attribute."
present: "The :attribute field must be present."
regex: "The :attribute format is invalid."
present_if: "The :attribute field must be present when :other is :value."
present_unless: "The :attribute field must be present unless :other is :value."
present_with: "The :attribute field must be present when :values is present."
present_with_all: "The :attribute field must be present when :values are present."
prohibited: "The :attribute field is prohibited."
prohibited_if: "The :attribute field is prohibited when :other is :value."
prohibited_unless: "The :attribute field is prohibited unless :other is in :values."
prohibits: "The :attribute field prohibits :other from being present."
regex: "The :attribute field format is invalid."
required: "The :attribute field is required."
required_array_keys: "The :attribute field must contain entries for: :values."
required_if: "The :attribute field is required when :other is :value."
required_if_accepted: "The :attribute field is required when :other is accepted."
required_unless: "The :attribute field is required unless :other is in :values."
required_with: "The :attribute field is required when :values is present."
required_with_all: "The :attribute field is required when :values are present."
required_without: "The :attribute field is required when :values is not present."
required_without_all: "The :attribute field is required when none of :values are present."
prohibited: "The :attribute field is prohibited."
prohibited_if: "The :attribute field is prohibited when :other is :value."
prohibited_unless: "The :attribute field is prohibited unless :other is in :values."
same: "The :attribute and :other must match."
same: "The :attribute field must match :other."
size:
numeric: "The :attribute must be :size."
file: "The :attribute must be :size kilobytes."
string: "The :attribute must be :size characters."
array: "The :attribute must contain :size items."
starts_with: "The :attribute must start with one of the following: :values."
string: "The :attribute must be a string."
timezone: "The :attribute must be a valid zone."
array: "The :attribute field must contain :size items."
file: "The :attribute field must be :size kilobytes."
numeric: "The :attribute field must be :size."
string: "The :attribute field must be :size characters."
starts_with: "The :attribute field must start with one of the following: :values."
string: "The :attribute field must be a string."
timezone: "The :attribute field must be a valid timezone."
unique: "The :attribute has already been taken."
uploaded: "The :attribute failed to upload."
url: "The :attribute format is invalid."
uuid: "The :attribute must be a valid UUID."
uppercase: "The :attribute field must be uppercase."
url: "The :attribute field must be a valid URL."
ulid: "The :attribute field must be a valid ULID."
uuid: "The :attribute field must be a valid UUID."
attributes:
username: username

View File

@ -123,12 +123,6 @@ class ApiServiceProvider extends AbstractServiceProvider
return $pipe;
});
$this->container->singleton('flarum.api.notification_serializers', function () {
return [
'discussionRenamed' => BasicDiscussionSerializer::class
];
});
$this->container->singleton('flarum.api_client.exclude_middleware', function () {
return [
HttpMiddleware\InjectActorReference::class,
@ -159,22 +153,10 @@ class ApiServiceProvider extends AbstractServiceProvider
public function boot(Container $container): void
{
$this->setNotificationSerializers();
AbstractSerializeController::setContainer($container);
AbstractSerializer::setContainer($container);
}
protected function setNotificationSerializers(): void
{
$serializers = $this->container->make('flarum.api.notification_serializers');
foreach ($serializers as $type => $serializer) {
NotificationSerializer::setSubjectSerializer($type, $serializer);
}
}
protected function populateRoutes(RouteCollection $routes): void
{
/** @var RouteHandlerFactory $factory */

View File

@ -12,6 +12,7 @@ class Context extends BaseContext
protected ?SearchResults $search = null;
protected int|string|null $modelId = null;
protected array $internal = [];
protected array $parameters = [];
public function withModelId(int|string|null $id): static
{
@ -53,4 +54,15 @@ class Context extends BaseContext
{
return RequestUtil::getActor($this->request);
}
public function setParam(string $key, mixed $default = null): static
{
$this->parameters[$key] = $default;
return $this;
}
public function getParam(string $key, mixed $default = null): mixed
{
return $this->parameters[$key] ?? $default;
}
}

View File

@ -0,0 +1,32 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Api\Controller;
use Flarum\Api\Endpoint\Show;
use Flarum\Api\JsonApi;
use Flarum\Api\Resource\ForumResource;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
class ShowForumController implements RequestHandlerInterface
{
public function __construct(
protected JsonApi $api
) {}
public function handle(ServerRequestInterface $request): ResponseInterface
{
return $this->api
->forResource(ForumResource::class)
->forEndpoint(Show::class)
->handle($request);
}
}

View File

@ -78,8 +78,8 @@ trait ExtractsListingParams
return [
'filter' => RequestUtil::extractFilter($context->request),
'sort' => RequestUtil::extractSort($context->request, $this->defaultSort, $this->getAvailableSorts($context)),
'limit' => RequestUtil::extractLimit($context->request, $this->limit, $this->maxLimit) ?? -1,
'offset' => RequestUtil::extractOffset($context->request),
'limit' => $limit = (RequestUtil::extractLimit($context->request, $this->limit, $this->maxLimit) ?? -1),
'offset' => RequestUtil::extractOffset($context->request, $limit),
];
}

View File

@ -1,45 +0,0 @@
<?php
namespace Flarum\Api\Endpoint\Concerns;
use Flarum\Api\Context;
use Flarum\Api\Schema\Attribute;
use Illuminate\Contracts\Validation\Factory;
use Illuminate\Validation\ValidationException;
trait ValidatesData
{
/**
* @throws ValidationException
*/
protected function assertDataIsValid(Context $context, array $data, bool $validateAll): void
{
$rules = [
'attributes' => [],
'relationships' => [],
];
$messages = [];
$attributes = [];
foreach ($context->fields($context->resource) as $field) {
$writable = $field->isWritable($context->withField($field));
if (! $writable) {
continue;
}
$type = $field instanceof Attribute ? 'attributes' : 'relationships';
$rules[$type] = array_merge($rules[$type], $field->getValidationRules($context));
$messages = array_merge($messages, $field->getValidationMessages($context));
$attributes = array_merge($attributes, $field->getValidationAttributes($context));
}
// @todo: merge into a single validator.
$attributeValidator = resolve(Factory::class)->make($data['attributes'], $rules['attributes'], $messages, $attributes);
$relationshipValidator = resolve(Factory::class)->make($data['relationships'], $rules['relationships'], $messages, $attributes);
$attributeValidator->validate();
$relationshipValidator->validate();
}
}

View File

@ -7,7 +7,6 @@ use Flarum\Api\Endpoint\Concerns\HasCustomRoute;
use Flarum\Api\Endpoint\Concerns\HasEagerLoading;
use Flarum\Api\Endpoint\Concerns\HasHooks;
use Flarum\Api\Endpoint\Concerns\SavesData;
use Flarum\Api\Endpoint\Concerns\ValidatesData;
use Illuminate\Database\Eloquent\Collection;
use Psr\Http\Message\ResponseInterface;
use RuntimeException;
@ -29,7 +28,6 @@ class Create extends BaseCreate implements Endpoint
use HasAuthorization;
use HasEagerLoading;
use HasCustomRoute;
use ValidatesData;
use HasHooks;
public function handle(Context $context): ?ResponseInterface
@ -67,7 +65,7 @@ class Create extends BaseCreate implements Endpoint
$this->fillDefaultValues($context, $data);
$this->deserializeValues($context, $data);
$this->mutateDataBeforeValidation($context, $data, true);
$this->assertDataIsValid($context, $data, true);
$this->assertDataValid($context, $data);
$this->setValues($context, $data);

View File

@ -7,7 +7,6 @@ use Flarum\Api\Endpoint\Concerns\HasCustomRoute;
use Flarum\Api\Endpoint\Concerns\HasEagerLoading;
use Flarum\Api\Endpoint\Concerns\HasHooks;
use Flarum\Api\Endpoint\Concerns\SavesData;
use Flarum\Api\Endpoint\Concerns\ValidatesData;
use Illuminate\Database\Eloquent\Collection;
use Psr\Http\Message\ResponseInterface;
use RuntimeException;
@ -27,7 +26,6 @@ class Update extends BaseUpdate implements Endpoint
use HasAuthorization;
use HasEagerLoading;
use HasCustomRoute;
use ValidatesData;
use HasHooks;
public function handle(Context $context): ?ResponseInterface
@ -70,7 +68,7 @@ class Update extends BaseUpdate implements Endpoint
$this->assertFieldsValid($context, $data);
$this->deserializeValues($context, $data);
$this->mutateDataBeforeValidation($context, $data, false);
$this->assertDataIsValid($context, $data, false);
$this->assertDataValid($context, $data);
$this->setValues($context, $data);
$context = $context->withModel($model = $resource->update($model, $context));

View File

@ -5,6 +5,7 @@ namespace Flarum\Api;
use Flarum\Api\Endpoint\Endpoint;
use Flarum\Api\Endpoint\EndpointRoute;
use Flarum\Api\Resource\AbstractDatabaseResource;
use Flarum\Http\RequestUtil;
use Laminas\Diactoros\ServerRequestFactory;
use Laminas\Diactoros\Uri;
use Psr\Http\Message\ResponseInterface as Response;
@ -18,6 +19,7 @@ class JsonApi extends BaseJsonApi
{
protected string $resourceClass;
protected string $endpoint;
protected ?Request $baseRequest = null;
public function forResource(string $resourceClass): self
{
@ -58,6 +60,13 @@ class JsonApi extends BaseJsonApi
throw new BadRequestException('Invalid endpoint specified');
}
public function withRequest(Request $request): self
{
$this->baseRequest = $request;
return $this;
}
public function handle(Request $request): Response
{
$context = $this->makeContext($request);
@ -65,27 +74,31 @@ class JsonApi extends BaseJsonApi
return $context->endpoint->handle($context);
}
public function execute(ServerRequestInterface|array $request, array $internal = []): mixed
public function execute(array $body, array $internal = [], array $options = []): mixed
{
/** @var EndpointRoute $route */
$route = (new $this->endpoint)->route();
if (is_array($request)) {
$request = ServerRequestFactory::fromGlobals()->withParsedBody($request);
$request = $this->baseRequest ?? ServerRequestFactory::fromGlobals();
if (! empty($options['actor'])) {
$request = RequestUtil::withActor($request, $options['actor']);
}
$request = $request
->withMethod($route->method)
->withUri(new Uri($route->path))
->withParsedBody([
...$body,
'data' => [
...($request->getParsedBody()['data'] ?? []),
...($body['data'] ?? []),
'type' => (new $this->resourceClass)->type(),
],
]);
$context = $this->makeContext($request)
->withModelId($data['id'] ?? null);
->withModelId($body['data']['id'] ?? null);
foreach ($internal as $key => $value) {
$context = $context->withInternal($key, $value);

View File

@ -12,8 +12,11 @@ use Flarum\Api\Resource\Contracts\{
Deletable
};
use Flarum\Api\Resource\Concerns\Bootable;
use Flarum\Api\Resource\Concerns\ResolvesValidationFactory;
use Flarum\Foundation\DispatchEventsTrait;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\Arr;
use RuntimeException;
use Tobyz\JsonApiServer\Context;
use Tobyz\JsonApiServer\Laravel\EloquentResource as BaseResource;
@ -28,6 +31,7 @@ abstract class AbstractDatabaseResource extends BaseResource implements
{
use Bootable;
use DispatchEventsTrait;
use ResolvesValidationFactory;
abstract public function model(): string;
@ -36,9 +40,20 @@ abstract class AbstractDatabaseResource extends BaseResource implements
return new ($this->model());
}
public function resource(object $model, Context $context): ?string
{
$baseModel = $this->model();
if ($model instanceof $baseModel) {
return $this->type();
}
return null;
}
public function filters(): array
{
throw new \RuntimeException('Not supported in Flarum, please use a model searcher instead https://docs.flarum.org/extend/search.');
throw new RuntimeException('Not supported in Flarum, please use a model searcher instead https://docs.flarum.org/extend/search.');
}
public function create(object $model, Context $context): object
@ -110,27 +125,26 @@ abstract class AbstractDatabaseResource extends BaseResource implements
//
}
protected function bcSavingEvent(Context $context, array $data): ?object
protected function newSavingEvent(Context $context, array $data): ?object
{
return null;
}
public function mutateDataBeforeValidation(Context $context, array $data, bool $validateAll): array
{
return $data;
$dirty = $context->model->getDirty();
// @todo: decided to completely drop this.
$savingEvent = $this->bcSavingEvent($context, $data);
$savingEvent = $this->newSavingEvent($context, Arr::get($context->body(), 'data', []));
if ($savingEvent) {
// BC Layer for Flarum 1.0
// @todo: should we drop this or keep it for 2.0? another massive BC break.
// @todo: replace with resource extenders
$this->container->make(Dispatcher::class)->dispatch(
$savingEvent
);
$this->container->make(Dispatcher::class)->dispatch($savingEvent);
return array_merge($data, $context->model->getDirty());
$dirtyAfterEvent = $context->model->getDirty();
// Unlike 1.0, the saving events in 2.0 do not allow modifying the model.
if ($dirtyAfterEvent !== $dirty) {
throw new RuntimeException('You should modify the model through the saving event. Please use the resource extenders instead.');
}
}
return $data;

View File

@ -3,9 +3,11 @@
namespace Flarum\Api\Resource;
use Flarum\Api\Resource\Concerns\Bootable;
use Flarum\Api\Resource\Concerns\ResolvesValidationFactory;
use Tobyz\JsonApiServer\Resource\AbstractResource as BaseResource;
abstract class AbstractResource extends BaseResource
{
use Bootable;
use ResolvesValidationFactory;
}

View File

@ -0,0 +1,16 @@
<?php
namespace Flarum\Api\Resource\Concerns;
use Illuminate\Contracts\Validation\Factory;
trait ResolvesValidationFactory
{
/**
* Called by the JSON:API server package to resolve the validation factory.
*/
public function validationFactory(): Factory
{
return resolve(Factory::class);
}
}

View File

@ -289,7 +289,8 @@ class DiscussionResource extends AbstractDatabaseResource
// We will do this by running the PostReply command.
$post = $api->forResource(PostResource::class)
->forEndpoint(Create::class)
->execute($context->request->withParsedBody([
->withRequest($context->request)
->execute([
'data' => [
'attributes' => [
'content' => $context->request->getParsedBody()['data']['attributes']['content'],
@ -303,7 +304,7 @@ class DiscussionResource extends AbstractDatabaseResource
],
],
],
]), ['isFirstPost' => true]);
], ['isFirstPost' => true]);
// Before we dispatch events, refresh our discussion instance's
// attributes as posting the reply will have changed some of them (e.g.
@ -327,7 +328,7 @@ class DiscussionResource extends AbstractDatabaseResource
);
}
protected function bcSavingEvent(\Tobyz\JsonApiServer\Context $context, array $data): ?object
protected function newSavingEvent(\Tobyz\JsonApiServer\Context $context, array $data): ?object
{
return new Saving($context->model, $context->getActor(), $data);
}

View File

@ -71,8 +71,10 @@ class GroupResource extends AbstractDatabaseResource
->writable()
->required(),
Schema\Str::make('color')
->nullable()
->writable(),
Schema\Str::make('icon')
->nullable()
->writable(),
Schema\Boolean::make('isHidden')
->writable(),
@ -99,7 +101,7 @@ class GroupResource extends AbstractDatabaseResource
return $name;
}
protected function bcSavingEvent(Context $context, array $data): ?object
protected function newSavingEvent(Context $context, array $data): ?object
{
return new Saving($context->model, RequestUtil::getActor($context->request), $data);
}

View File

@ -82,7 +82,7 @@ class NotificationResource extends AbstractDatabaseResource
->type('users')
->includable(),
Schema\Relationship\ToOne::make('subject')
->type($subjectTypes)
->collection($subjectTypes)
->includable(),
];
}

View File

@ -169,7 +169,7 @@ class PostResource extends AbstractDatabaseResource
}
}
})
->serialize(function (string|array $value, Context $context) {
->serialize(function (null|string|array $value, Context $context) {
// Prevent the string type from trying to convert array content (for event posts) to a string.
$context->field->type = null;
@ -204,7 +204,10 @@ class PostResource extends AbstractDatabaseResource
Schema\DateTime::make('editedAt'),
Schema\Boolean::make('isHidden')
->visible(fn (Post $post) => $post->hidden_at !== null)
->writable(fn (Post $post, Context $context) => $context->getActor()->can('hide', $post))
->writable(function (Post $post, Context $context) {
return $context->endpoint instanceof Endpoint\Update
&& $context->getActor()->can('hide', $post);
})
->set(function (Post $post, bool $value, Context $context) {
if ($post instanceof CommentPost) {
if ($value) {
@ -271,7 +274,7 @@ class PostResource extends AbstractDatabaseResource
);
}
protected function bcSavingEvent(\Tobyz\JsonApiServer\Context $context, array $data): ?object
protected function newSavingEvent(\Tobyz\JsonApiServer\Context $context, array $data): ?object
{
return new Saving($context->model, $context->getActor(), $data);
}

View File

@ -5,6 +5,7 @@ namespace Flarum\Api\Resource;
use Flarum\Api\Context;
use Flarum\Api\Endpoint;
use Flarum\Api\Schema;
use Flarum\Foundation\ValidationException;
use Flarum\Http\SlugManager;
use Flarum\Locale\TranslatorInterface;
use Flarum\Settings\SettingsRepositoryInterface;
@ -12,6 +13,7 @@ use Flarum\User\Event\Deleting;
use Flarum\User\Event\GroupsChanged;
use Flarum\User\Event\RegisteringFromProvider;
use Flarum\User\Event\Saving;
use Flarum\User\Exception\NotAuthenticatedException;
use Flarum\User\RegistrationToken;
use Flarum\User\User;
use Illuminate\Database\Eloquent\Builder;
@ -65,7 +67,25 @@ class UserResource extends AbstractDatabaseResource
return true;
}),
Endpoint\Update::make()
->authenticated()
->visible(function (User $user, Context $context) {
$actor = $context->getActor();
$body = $context->body();
// Require the user's current password if they are attempting to change
// their own email address.
if (isset($body['data']['attributes']['email']) && $actor->id === $user->id) {
$password = (string) Arr::get($body, 'meta.password');
if (! $actor->checkPassword($password)) {
throw new NotAuthenticatedException;
}
}
$actor->assertRegistered();
return true;
})
->defaultInclude(['groups']),
Endpoint\Delete::make()
->authenticated()
@ -81,13 +101,16 @@ class UserResource extends AbstractDatabaseResource
public function fields(): array
{
$translator = resolve(TranslatorInterface::class);
return [
Schema\Str::make('username')
->requiredOnCreate()
->requiredOnCreateWithout(['token'])
->unique('users', 'username', true)
->regex('/^[a-z0-9_-]+$/i')
->validationMessages([
'username.regex' => resolve(TranslatorInterface::class)->trans('core.api.invalid_username_message')
'username.regex' => $translator->trans('core.api.invalid_username_message'),
'username.required_without' => $translator->trans('validation.required', ['attribute' => $translator->trans('validation.attributes.username')])
])
->minLength(3)
->maxLength(30)
@ -103,7 +126,10 @@ class UserResource extends AbstractDatabaseResource
}
}),
Schema\Str::make('email')
->requiredOnCreate()
->requiredOnCreateWithout(['token'])
->validationMessages([
'email.required_without' => $translator->trans('validation.required', ['attribute' => $translator->trans('validation.attributes.email')])
])
->email(['filter'])
->unique('users', 'email', true)
->visible(function (User $user, Context $context) {
@ -144,6 +170,9 @@ class UserResource extends AbstractDatabaseResource
}),
Schema\Str::make('password')
->requiredOnCreateWithout(['token'])
->validationMessages([
'password.required_without' => $translator->trans('validation.required', ['attribute' => $translator->trans('validation.attributes.password')])
])
->minLength(8)
->visible(false)
->writable(function (User $user, Context $context) {
@ -159,16 +188,17 @@ class UserResource extends AbstractDatabaseResource
->writable(function (User $user, Context $context) {
return $context->endpoint instanceof Endpoint\Create;
})
->set(function (User $user, ?string $value) {
->set(function (User $user, ?string $value, Context $context) {
if ($value) {
$token = RegistrationToken::validOrFail($value);
$user->setAttribute('token', $token);
$context->setParam('token', $token);
$user->password ??= Str::random(20);
$this->applyToken($user, $token);
}
}),
})
->save(fn () => null),
Schema\Str::make('displayName'),
Schema\Str::make('avatarUrl'),
Schema\Str::make('slug')
@ -289,7 +319,7 @@ class UserResource extends AbstractDatabaseResource
/** @param User $model */
public function saved(object $model, \Tobyz\JsonApiServer\Context $context): ?object
{
if (($token = $model->getAttribute('token')) instanceof RegistrationToken) {
if (($token = $context->getParam('token')) instanceof RegistrationToken) {
$this->fulfillToken($model, $token);
}
@ -303,7 +333,7 @@ class UserResource extends AbstractDatabaseResource
);
}
protected function bcSavingEvent(\Tobyz\JsonApiServer\Context $context, array $data): ?object
protected function newSavingEvent(\Tobyz\JsonApiServer\Context $context, array $data): ?object
{
return new Saving($context->model, $context->getActor(), $data);
}
@ -343,13 +373,17 @@ class UserResource extends AbstractDatabaseResource
]);
if ($urlValidator->fails()) {
throw new InvalidArgumentException('Provided avatar URL must be a valid URI.', 503);
throw new ValidationException([
'avatar_url' => 'Provided avatar URL must be a valid URI.',
]);
}
$scheme = parse_url($url, PHP_URL_SCHEME);
if (! in_array($scheme, ['http', 'https'])) {
throw new InvalidArgumentException("Provided avatar URL must have scheme http or https. Scheme provided was $scheme.", 503);
throw new ValidationException([
'avatar_url' => "Provided avatar URL must have scheme http or https. Scheme provided was $scheme.",
]);
}
$image = $this->imageManager->make($url);

View File

@ -2,12 +2,9 @@
namespace Flarum\Api\Schema;
use Flarum\Api\Schema\Concerns\EvaluatesCallbacks;
use Flarum\Api\Schema\Concerns\HasValidationRules;
use Tobyz\JsonApiServer\Schema\Field\Attribute as BaseAttribute;
class Attribute extends BaseAttribute
{
use HasValidationRules;
use EvaluatesCallbacks;
//
}

View File

@ -1,19 +0,0 @@
<?php
namespace Flarum\Api\Schema\Concerns;
use Tobyz\JsonApiServer\Context;
trait EvaluatesCallbacks
{
protected function evaluate(Context $context, mixed $callback): mixed
{
if (is_string($callback) || ! is_callable($callback)) {
return $callback;
}
return (isset($context->model))
? $callback($context->model, $context)
: $callback($context);
}
}

View File

@ -1,152 +0,0 @@
<?php
namespace Flarum\Api\Schema\Concerns;
use Flarum\Api\Endpoint\Create;
use Flarum\Api\Endpoint\Update;
use Illuminate\Validation\Rule;
use Tobyz\JsonApiServer\Context;
trait HasValidationRules
{
/**
* @var array<array{rule: string|callable, condition: bool|callable}>
*/
protected array $rules = [];
/**
* @var string[]
*/
protected array $validationMessages = [];
/**
* @var string[]
*/
protected array $validationAttributes = [];
public function rules(array|string $rules, bool|callable $condition, bool $override = true): static
{
if (is_string($rules)) {
$rules = explode('|', $rules);
}
$rules = array_map(function ($rule) use ($condition) {
return compact('rule', 'condition');
}, $rules);
$this->rules = $override ? $rules : array_merge($this->rules, $rules);
return $this;
}
public function validationMessages(array $messages): static
{
$this->validationMessages = array_merge($this->validationMessages, $messages);
return $this;
}
public function validationAttributes(array $attributes): static
{
$this->validationAttributes = array_merge($this->validationAttributes, $attributes);
return $this;
}
public function rule(string|callable $rule, bool|callable $condition = true): static
{
$this->rules[] = compact('rule', 'condition');
return $this;
}
public function getRules(): array
{
return $this->rules;
}
public function getValidationRules(Context $context): array
{
$rules = array_map(
fn ($rule) => $this->evaluate($context, $rule['rule']),
array_filter(
$this->rules,
fn ($rule) => $this->evaluate($context, $rule['condition'])
)
);
return [
$this->name => $rules
];
}
public function getValidationMessages(Context $context): array
{
return $this->validationMessages;
}
public function getValidationAttributes(Context $context): array
{
return $this->validationAttributes;
}
public function requiredOnCreate(): static
{
return $this->rule('required', fn ($model, Context $context) => $context->endpoint instanceof Create);
}
public function requiredOnUpdate(): static
{
return $this->rule('required', fn ($model, Context $context) => !$context->endpoint instanceof Update);
}
public function requiredWith(array $fields, bool|callable $condition): static
{
return $this->rule('required_with:' . implode(',', $fields), $condition);
}
public function requiredWithout(array $fields, bool|callable $condition): static
{
return $this->rule('required_without:' . implode(',', $fields), $condition);
}
public function requiredOnCreateWith(array $fields): static
{
return $this->requiredWith($fields, fn ($model, Context $context) => $context->endpoint instanceof Create);
}
public function requiredOnUpdateWith(array $fields): static
{
return $this->requiredWith($fields, fn ($model, Context $context) => $context->endpoint instanceof Update);
}
public function requiredOnCreateWithout(array $fields): static
{
return $this->requiredWithout($fields, fn ($model, Context $context) => $context->endpoint instanceof Create);
}
public function requiredOnUpdateWithout(array $fields): static
{
return $this->requiredWithout($fields, fn ($model, Context $context) => $context->endpoint instanceof Update);
}
public function nullable(bool $nullable = true): static
{
parent::nullable($nullable);
return $this->rule('nullable');
}
public function unique(string $table, string $column, bool $ignorable = false, bool|callable $condition = true): static
{
return $this->rule(function ($model, Context $context) use ($table, $column, $ignorable) {
$rule = Rule::unique($table, $column);
if ($ignorable && ($modelId = $context->model?->getKey())) {
$rule = $rule->ignore($modelId, $context->model->getKeyName());
}
return $rule;
}, $condition);
}
}

View File

@ -2,12 +2,9 @@
namespace Flarum\Api\Schema\Relationship;
use Flarum\Api\Schema\Concerns\EvaluatesCallbacks;
use Flarum\Api\Schema\Concerns\HasValidationRules;
use Tobyz\JsonApiServer\Schema\Field\ToMany as BaseToMany;
class ToMany extends BaseToMany
{
use HasValidationRules;
use EvaluatesCallbacks;
//
}

View File

@ -2,12 +2,9 @@
namespace Flarum\Api\Schema\Relationship;
use Flarum\Api\Schema\Concerns\EvaluatesCallbacks;
use Flarum\Api\Schema\Concerns\HasValidationRules;
use Tobyz\JsonApiServer\Schema\Field\ToOne as BaseToOne;
class ToOne extends BaseToOne
{
use HasValidationRules;
use EvaluatesCallbacks;
//
}

View File

@ -10,7 +10,6 @@
namespace Flarum\Forum\Content;
use Flarum\Api\Client;
use Flarum\Api\Controller\ListDiscussionsController;
use Flarum\Frontend\Document;
use Flarum\Http\UrlGenerator;
use Flarum\Locale\TranslatorInterface;
@ -27,7 +26,6 @@ class Index
protected SettingsRepositoryInterface $settings,
protected UrlGenerator $url,
protected TranslatorInterface $translator,
protected ListDiscussionsController $controller
) {
}
@ -38,17 +36,14 @@ class Index
$sort = Arr::pull($queryParams, 'sort');
$q = Arr::pull($queryParams, 'q');
$page = max(1, intval(Arr::pull($queryParams, 'page')));
$filters = Arr::pull($queryParams, 'filter', []);
$sortMap = resolve('flarum.forum.discussions.sortmap');
$limit = $this->controller->limit;
$params = [
...$queryParams,
'sort' => $sort && isset($sortMap[$sort]) ? $sortMap[$sort] : null,
'filter' => $filters,
'page' => [
'offset' => ($page - 1) * $limit,
'limit' => $limit
'number' => $page
],
];

View File

@ -28,7 +28,7 @@ class RegisterController implements RequestHandlerInterface
public function handle(Request $request): ResponseInterface
{
$params = ['data' => ['attributes' => $request->getParsedBody()]];
$params = ['data' => ['type' => 'users', 'attributes' => $request->getParsedBody() ?? []]];
$response = $this->api->withParentRequest($request)->withBody($params)->post('/users');

View File

@ -0,0 +1,26 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\Foundation\ErrorHandling\ExceptionHandler;
use Flarum\Foundation\ErrorHandling\HandledError;
use Throwable;
use Tobyz\JsonApiServer\Exception\ErrorProvider;
class JsonApiExceptionHandler
{
public function handle(ErrorProvider&Throwable $e): HandledError
{
return (new HandledError(
$e,
'validation_error',
$e->getJsonApiStatus()
))->withDetails($e->getJsonApiErrors());
}
}

View File

@ -73,12 +73,11 @@ class Registry
private function handleCustomTypes(Throwable $error): ?HandledError
{
$errorClass = $error::class;
if (isset($this->handlerMap[$errorClass])) {
$handler = new $this->handlerMap[$errorClass];
return $handler->handle($error);
foreach ($this->handlerMap as $class => $handler) {
if ($error instanceof $class) {
$handler = new $handler;
return $handler->handle($error);
}
}
return null;

View File

@ -52,12 +52,6 @@ class ErrorServiceProvider extends AbstractServiceProvider
return [
InvalidParameterException::class => 'invalid_parameter',
ModelNotFoundException::class => 'not_found',
TobyzJsonApiServerException\BadRequestException::class => 'invalid_parameter',
TobyzJsonApiServerException\MethodNotAllowedException::class => 'method_not_allowed',
TobyzJsonApiServerException\ForbiddenException::class => 'permission_denied',
TobyzJsonApiServerException\ConflictException::class => 'io_error',
// TobyzJsonApiServerException\UnprocessableEntityException::class => 'invalid_parameter', @todo
];
});
@ -68,6 +62,7 @@ class ErrorServiceProvider extends AbstractServiceProvider
ExtensionException\CircularDependenciesException::class => ExtensionException\CircularDependenciesExceptionHandler::class,
ExtensionException\DependentExtensionsException::class => ExtensionException\DependentExtensionsExceptionHandler::class,
ExtensionException\MissingDependenciesException::class => ExtensionException\MissingDependenciesExceptionHandler::class,
TobyzJsonApiServerException\ErrorProvider::class => Handling\ExceptionHandler\JsonApiExceptionHandler::class,
];
});

View File

@ -24,8 +24,7 @@ class CheckCsrfToken implements Middleware
public function process(Request $request, Handler $handler): Response
{
// @todo: debugging
if (true || in_array($request->getAttribute('routeName'), $this->exemptRoutes, true)) {
if (in_array($request->getAttribute('routeName'), $this->exemptRoutes, true)) {
return $handler->handle($request);
}

View File

@ -99,10 +99,10 @@ class RequestUtil
return ($page - 1) * $limit;
}
public static function extractOffset(Request $request): int
public static function extractOffset(Request $request, ?int $limit = 0): int
{
if ($request->getQueryParams()['page']['number'] ?? false) {
return self::extractOffsetFromNumber($request, self::extractLimit($request));
return self::extractOffsetFromNumber($request, $limit);
}
$offset = (int) ($request->getQueryParams()['page']['offset'] ?? 0);

View File

@ -52,6 +52,7 @@ class CreateTest extends TestCase
'authenticatedAs' => $authenticatedAs,
'json' => [
'data' => [
'type' => 'access-tokens',
'attributes' => [
'title' => 'Dev'
]
@ -74,6 +75,7 @@ class CreateTest extends TestCase
'authenticatedAs' => $authenticatedAs,
'json' => [
'data' => [
'type' => 'access-tokens',
'attributes' => [
'title' => 'Dev'
]

View File

@ -42,6 +42,7 @@ class CreateTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'discussions',
'attributes' => [
'title' => 'Test post',
'content' => '',
@ -51,10 +52,11 @@ class CreateTest extends TestCase
])
);
$this->assertEquals(422, $response->getStatusCode());
$body = (string) $response->getBody();
$this->assertEquals(422, $response->getStatusCode(), $body);
// The response body should contain details about the failed validation
$body = (string) $response->getBody();
$this->assertJson($body);
$this->assertEquals([
'errors' => [
@ -78,6 +80,7 @@ class CreateTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'discussions',
'attributes' => [
'title' => '',
'content' => 'Test post',
@ -114,6 +117,7 @@ class CreateTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'discussions',
'attributes' => [
'title' => 'test - too-obscure',
'content' => 'predetermined content for automated testing - too-obscure',
@ -123,11 +127,13 @@ class CreateTest extends TestCase
])
);
$this->assertEquals(201, $response->getStatusCode());
$body = $response->getBody()->getContents();
$this->assertEquals(201, $response->getStatusCode(), $body);
/** @var Discussion $discussion */
$discussion = Discussion::firstOrFail();
$data = json_decode($response->getBody()->getContents(), true);
$data = json_decode($body, true);
$this->assertEquals('test - too-obscure', $discussion->title);
$this->assertEquals('test - too-obscure', Arr::get($data, 'data.attributes.title'));
@ -146,6 +152,7 @@ class CreateTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'discussions',
'attributes' => [
'title' => '我是一个土豆',
'content' => 'predetermined content for automated testing',
@ -178,6 +185,7 @@ class CreateTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'discussions',
'attributes' => [
'title' => '我是一个土豆',
'content' => 'predetermined content for automated testing',
@ -205,6 +213,7 @@ class CreateTest extends TestCase
'authenticatedAs' => 2,
'json' => [
'data' => [
'type' => 'discussions',
'attributes' => [
'title' => 'test - too-obscure',
'content' => 'predetermined content for automated testing - too-obscure',
@ -219,6 +228,7 @@ class CreateTest extends TestCase
'authenticatedAs' => 2,
'json' => [
'data' => [
'type' => 'discussions',
'attributes' => [
'title' => 'test - too-obscure',
'content' => 'Second predetermined content for automated testing - too-obscure',
@ -241,6 +251,7 @@ class CreateTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'discussions',
'attributes' => [
'title' => 'test - too-obscure',
'content' => 'predetermined content for automated testing - too-obscure',
@ -255,6 +266,7 @@ class CreateTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'discussions',
'attributes' => [
'title' => 'test - too-obscure',
'content' => 'Second predetermined content for automated testing - too-obscure',

View File

@ -44,7 +44,8 @@ class ShowTest extends TestCase
$json = json_decode($response->getBody()->getContents(), true);
$this->assertArrayNotHasKey('actor', Arr::get($json, 'data.relationships'));
$this->assertArrayHasKey('actor', Arr::get($json, 'data.relationships'));
$this->assertNull(Arr::get($json, 'data.relationships.actor.data'));
}
/**

View File

@ -44,7 +44,7 @@ class CreateTest extends TestCase
])
);
$this->assertEquals(422, $response->getStatusCode());
$this->assertEquals(400, $response->getStatusCode(), (string) $response->getBody());
}
/**
@ -57,6 +57,7 @@ class CreateTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'groups',
'attributes' => [
'nameSingular' => 'flarumite',
'namePlural' => 'flarumites',
@ -68,10 +69,12 @@ class CreateTest extends TestCase
])
);
$this->assertEquals(201, $response->getStatusCode());
$body = $response->getBody()->getContents();
$this->assertEquals(201, $response->getStatusCode(), $body);
// Verify API response body
$data = json_decode($response->getBody()->getContents(), true);
$data = json_decode($body, true);
$this->assertEquals('flarumite', Arr::get($data, 'data.attributes.nameSingular'));
$this->assertEquals('flarumites', Arr::get($data, 'data.attributes.namePlural'));
$this->assertEquals('test', Arr::get($data, 'data.attributes.icon'));
@ -95,6 +98,7 @@ class CreateTest extends TestCase
'authenticatedAs' => 2,
'json' => [
'data' => [
'type' => 'groups',
'attributes' => [
'nameSingular' => 'flarumite',
'namePlural' => 'flarumites',

View File

@ -75,7 +75,7 @@ class ShowTest extends TestCase
);
// Hidden group should not be returned for guest
$this->assertEquals(404, $response->getStatusCode());
$this->assertEquals(404, $response->getStatusCode(), (string) $response->getBody());
}
/**
@ -109,7 +109,7 @@ class ShowTest extends TestCase
// If group does not exist in database, controller
// should reject the request with 404 Not found
$this->assertEquals(404, $response->getStatusCode());
$this->assertEquals(404, $response->getStatusCode(), (string) $response->getBody());
}
protected function hiddenGroup(): array

View File

@ -53,6 +53,6 @@ class ListTest extends TestCase
])
);
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals(200, $response->getStatusCode(), (string) $response->getBody());
}
}

View File

@ -68,18 +68,21 @@ class CreateTest extends TestCase
'authenticatedAs' => $actorId,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => 'reply with predetermined content for automated testing - too-obscure',
],
'relationships' => [
'discussion' => ['data' => ['id' => $discussionId]],
'discussion' => [
'data' => ['type' => 'discussions', 'id' => $discussionId]
],
],
],
],
])
);
$this->assertEquals($responseStatus, $response->getStatusCode());
$this->assertEquals($responseStatus, $response->getStatusCode(), (string) $response->getBody());
}
public function discussionRepliesPrvider(): array
@ -103,6 +106,7 @@ class CreateTest extends TestCase
'authenticatedAs' => 2,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => 'reply with predetermined content for automated testing - too-obscure',
],
@ -119,6 +123,7 @@ class CreateTest extends TestCase
'authenticatedAs' => 2,
'json' => [
'data' => [
'type' => 'posts',
'attributes' => [
'content' => 'Second reply with predetermined content for automated testing - too-obscure',
],
@ -130,6 +135,6 @@ class CreateTest extends TestCase
])
);
$this->assertEquals(429, $response->getStatusCode());
$this->assertEquals(429, $response->getStatusCode(), (string) $response->getBody());
}
}

View File

@ -71,8 +71,10 @@ class ListTest extends TestCase
$this->request('GET', '/api/posts', ['authenticatedAs' => 1])
);
$this->assertEquals(200, $response->getStatusCode());
$data = json_decode($response->getBody()->getContents(), true);
$body = $response->getBody()->getContents();
$this->assertEquals(200, $response->getStatusCode(), $body);
$data = json_decode($body, true);
$this->assertEquals(5, count($data['data']));
}

View File

@ -39,15 +39,19 @@ class CreateTest extends TestCase
'POST',
'/api/users',
[
'json' => ['data' => ['attributes' => []]],
'json' => ['data' => [
'type' => 'users',
'attributes' => [],
]],
]
)->withAttribute('bypassCsrfToken', true)
);
$this->assertEquals(422, $response->getStatusCode());
$body = (string) $response->getBody();
$this->assertEquals(422, $response->getStatusCode(), $body);
// The response body should contain details about the failed validation
$body = (string) $response->getBody();
$this->assertJson($body);
$this->assertEquals([
'errors' => [
@ -96,7 +100,7 @@ class CreateTest extends TestCase
)->withAttribute('bypassCsrfToken', true)
);
$this->assertEquals(201, $response->getStatusCode());
$this->assertEquals(201, $response->getStatusCode(), (string) $response->getBody());
/** @var User $user */
$user = User::where('username', 'test')->firstOrFail();
@ -227,12 +231,12 @@ class CreateTest extends TestCase
$this->assertJson($body);
$decodedBody = json_decode($body, true);
$this->assertEquals(500, $response->getStatusCode());
$this->assertEquals(422, $response->getStatusCode(), $body);
$firstError = $decodedBody['errors'][0];
// Check that the error is an invalid URI
$this->assertStringStartsWith('InvalidArgumentException: Provided avatar URL must have scheme http or https. Scheme provided was '.$regToken['scheme'].'.', $firstError['detail']);
$this->assertStringContainsString('Provided avatar URL must have scheme http or https. Scheme provided was '.$regToken['scheme'].'.', $firstError['detail']);
}
}
@ -301,12 +305,12 @@ class CreateTest extends TestCase
$this->assertJson($body);
$decodedBody = json_decode($body, true);
$this->assertEquals(500, $response->getStatusCode());
$this->assertEquals(422, $response->getStatusCode(), $body);
$firstError = $decodedBody['errors'][0];
// Check that the error is an invalid URI
$this->assertStringStartsWith('InvalidArgumentException: Provided avatar URL must be a valid URI.', $firstError['detail']);
$this->assertStringContainsString('Provided avatar URL must be a valid URI.', $firstError['detail']);
}
}
@ -374,7 +378,7 @@ class CreateTest extends TestCase
)->withAttribute('bypassCsrfToken', true)
);
$this->assertEquals(201, $response->getStatusCode());
$this->assertEquals(201, $response->getStatusCode(), (string) $response->getBody());
$user = User::where('username', $regToken->user_attributes['username'])->firstOrFail();

View File

@ -84,7 +84,7 @@ class GroupSearchTest extends TestCase
$responseBodyContents = json_decode($response->getBody()->getContents(), true);
$this->assertCount(0, $responseBodyContents['data'], json_encode($responseBodyContents));
$this->assertArrayNotHasKey('included', $responseBodyContents, json_encode($responseBodyContents));
$this->assertCount(0, $responseBodyContents['included'], json_encode($responseBodyContents));
$response = $this->createRequest(['admins'], 2);
$responseBodyContents = json_decode($response->getBody()->getContents(), true);
@ -97,7 +97,7 @@ class GroupSearchTest extends TestCase
$responseBodyContents = json_decode($response->getBody()->getContents(), true);
$this->assertCount(0, $responseBodyContents['data'], json_encode($responseBodyContents));
$this->assertArrayNotHasKey('included', $responseBodyContents, json_encode($responseBodyContents));
$this->assertCount(0, $responseBodyContents['included'], json_encode($responseBodyContents));
$response = $this->createRequest(['1'], 2);
$responseBodyContents = json_decode($response->getBody()->getContents(), true);
@ -110,7 +110,7 @@ class GroupSearchTest extends TestCase
$responseBodyContents = json_decode($response->getBody()->getContents(), true);
$this->assertCount(0, $responseBodyContents['data'], json_encode($responseBodyContents));
$this->assertArrayNotHasKey('included', $responseBodyContents, json_encode($responseBodyContents));
$this->assertCount(0, $responseBodyContents['included'], json_encode($responseBodyContents));
}
/**
@ -129,7 +129,7 @@ class GroupSearchTest extends TestCase
$responseBodyContents = json_decode($response->getBody()->getContents(), true);
$this->assertCount(0, $responseBodyContents['data'], json_encode($responseBodyContents));
$this->assertArrayNotHasKey('included', $responseBodyContents, json_encode($responseBodyContents));
$this->assertCount(0, $responseBodyContents['included'], json_encode($responseBodyContents));
}
/**
@ -169,7 +169,7 @@ class GroupSearchTest extends TestCase
$responseBodyContents = json_decode($response->getBody()->getContents(), true);
$this->assertCount(0, $responseBodyContents['data'], json_encode($responseBodyContents));
$this->assertArrayNotHasKey('included', $responseBodyContents, json_encode($responseBodyContents));
$this->assertCount(0, $responseBodyContents['included'], json_encode($responseBodyContents));
$response = $this->createRequest(['admins'], 1);
$responseBodyContents = json_decode($response->getBody()->getContents(), true);
@ -182,7 +182,7 @@ class GroupSearchTest extends TestCase
$responseBodyContents = json_decode($response->getBody()->getContents(), true);
$this->assertCount(0, $responseBodyContents['data'], json_encode($responseBodyContents));
$this->assertArrayNotHasKey('included', $responseBodyContents, json_encode($responseBodyContents));
$this->assertCount(0, $responseBodyContents['included'], json_encode($responseBodyContents));
$response = $this->createRequest(['1'], 1);
$responseBodyContents = json_decode($response->getBody()->getContents(), true);
@ -195,7 +195,7 @@ class GroupSearchTest extends TestCase
$responseBodyContents = json_decode($response->getBody()->getContents(), true);
$this->assertCount(0, $responseBodyContents['data'], json_encode($responseBodyContents));
$this->assertArrayNotHasKey('included', $responseBodyContents, json_encode($responseBodyContents));
$this->assertCount(0, $responseBodyContents['included'], json_encode($responseBodyContents));
}
/**

View File

@ -101,6 +101,7 @@ class PasswordEmailTokensTest extends TestCase
'authenticatedAs' => 2,
'json' => [
'data' => [
'type' => 'users',
'attributes' => [
'email' => 'new-normal@machine.local'
]
@ -112,7 +113,7 @@ class PasswordEmailTokensTest extends TestCase
])
);
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals(200, $response->getStatusCode(), $response->getBody());
$this->assertEquals(1, EmailToken::query()->where('user_id', 2)->count());
}

View File

@ -44,7 +44,7 @@ class SendActivationEmailTest extends TestCase
);
// We don't want to delay tests too long.
EmailActivationThrottler::$timeout = 5;
EmailActivationThrottler::$timeout = 1;
sleep(EmailActivationThrottler::$timeout + 1);
}

View File

@ -47,7 +47,7 @@ class SendPasswordResetEmailTest extends TestCase
);
// We don't want to delay tests too long.
PasswordResetThrottler::$timeout = 5;
PasswordResetThrottler::$timeout = 1;
sleep(PasswordResetThrottler::$timeout + 1);
}

View File

@ -68,13 +68,15 @@ class UpdateTest extends TestCase
$response = $this->send(
$this->request('PATCH', '/api/users/2', [
'authenticatedAs' => 2,
'json' => [],
'json' => [
'data' => []
],
])
);
// Test for successful response and that the email is included in the response
$this->assertEquals(200, $response->getStatusCode());
$this->assertStringContainsString('normal@machine.local', (string) $response->getBody());
$this->assertEquals(200, $response->getStatusCode(), $body = (string) $response->getBody());
$this->assertStringContainsString('normal@machine.local', $body);
}
/**
@ -85,13 +87,15 @@ class UpdateTest extends TestCase
$response = $this->send(
$this->request('PATCH', '/api/users/1', [
'authenticatedAs' => 2,
'json' => [],
'json' => [
'data' => []
],
])
);
// Make sure sensitive information is not made public
$this->assertEquals(200, $response->getStatusCode());
$this->assertStringNotContainsString('admin@machine.local', (string) $response->getBody());
$this->assertEquals(200, $response->getStatusCode(), $body = (string) $response->getBody());
$this->assertStringNotContainsString('admin@machine.local', $body);
}
/**
@ -120,6 +124,7 @@ class UpdateTest extends TestCase
'authenticatedAs' => 2,
'json' => [
'data' => [
'type' => 'users',
'attributes' => [
'email' => 'someOtherEmail@example.com',
]
@ -131,7 +136,7 @@ class UpdateTest extends TestCase
])
);
$this->assertEquals(401, $response->getStatusCode());
$this->assertEquals(401, $response->getStatusCode(), (string) $response->getBody());
}
/**
@ -144,6 +149,7 @@ class UpdateTest extends TestCase
'authenticatedAs' => 2,
'json' => [
'data' => [
'type' => 'users',
'attributes' => [
'email' => 'someOtherEmail@example.com',
]
@ -180,7 +186,7 @@ class UpdateTest extends TestCase
);
// We don't want to delay tests too long.
EmailChangeThrottler::$timeout = 5;
EmailChangeThrottler::$timeout = 1;
sleep(EmailChangeThrottler::$timeout + 1);
}
@ -223,6 +229,7 @@ class UpdateTest extends TestCase
'authenticatedAs' => 2,
'json' => [
'data' => [
'type' => 'users',
'attributes' => [
'username' => 'iCantChangeThis',
],
@ -243,6 +250,7 @@ class UpdateTest extends TestCase
'authenticatedAs' => 2,
'json' => [
'data' => [
'type' => 'users',
'attributes' => [
'preferences' => [
'something' => 'else'
@ -268,7 +276,7 @@ class UpdateTest extends TestCase
'relationships' => [
'groups' => [
'data' => [
['id' => 1, 'type' => 'group']
['id' => 1, 'type' => 'groups']
]
]
],
@ -289,6 +297,7 @@ class UpdateTest extends TestCase
'authenticatedAs' => 2,
'json' => [
'data' => [
'type' => 'users',
'attributes' => [
'markedAllAsReadAt' => Carbon::now()
],
@ -309,6 +318,7 @@ class UpdateTest extends TestCase
'authenticatedAs' => 2,
'json' => [
'data' => [
'type' => 'users',
'attributes' => [
'isEmailConfirmed' => true
],
@ -345,6 +355,7 @@ class UpdateTest extends TestCase
'authenticatedAs' => 3,
'json' => [
'data' => [
'type' => 'users',
'attributes' => [
'email' => 'someOtherEmail@example.com',
]
@ -368,6 +379,7 @@ class UpdateTest extends TestCase
'authenticatedAs' => 3,
'json' => [
'data' => [
'type' => 'users',
'attributes' => [
'username' => 'iCantChangeThis',
],
@ -391,7 +403,7 @@ class UpdateTest extends TestCase
'relationships' => [
'groups' => [
'data' => [
['id' => 1, 'type' => 'group']
['id' => 1, 'type' => 'groups']
]
]
],
@ -412,6 +424,7 @@ class UpdateTest extends TestCase
'authenticatedAs' => 2,
'json' => [
'data' => [
'type' => 'users',
'attributes' => [
'isEmailConfirmed' => true
],
@ -450,6 +463,7 @@ class UpdateTest extends TestCase
'authenticatedAs' => 3,
'json' => [
'data' => [
'type' => 'users',
'attributes' => [
'email' => 'someOtherEmail@example.com',
]
@ -471,6 +485,7 @@ class UpdateTest extends TestCase
'authenticatedAs' => 3,
'json' => [
'data' => [
'type' => 'users',
'attributes' => [
'username' => 'iCanChangeThis',
],
@ -492,6 +507,7 @@ class UpdateTest extends TestCase
'authenticatedAs' => 3,
'json' => [
'data' => [
'type' => 'users',
'attributes' => [
'email' => 'someOtherEmail@example.com',
]
@ -513,6 +529,7 @@ class UpdateTest extends TestCase
'authenticatedAs' => 3,
'json' => [
'data' => [
'type' => 'users',
'attributes' => [
'username' => 'iCanChangeThis',
],
@ -537,7 +554,7 @@ class UpdateTest extends TestCase
'relationships' => [
'groups' => [
'data' => [
['id' => 4, 'type' => 'group']
['id' => 4, 'type' => 'groups']
]
]
],
@ -545,7 +562,7 @@ class UpdateTest extends TestCase
],
])
);
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals(200, $response->getStatusCode(), (string) $response->getBody());
}
/**
@ -585,7 +602,7 @@ class UpdateTest extends TestCase
'relationships' => [
'groups' => [
'data' => [
['id' => 1, 'type' => 'group']
['id' => 1, 'type' => 'groups']
]
]
],
@ -610,7 +627,7 @@ class UpdateTest extends TestCase
'relationships' => [
'groups' => [
'data' => [
['id' => 1, 'type' => 'group']
['id' => 1, 'type' => 'groups']
]
]
],
@ -632,6 +649,7 @@ class UpdateTest extends TestCase
'authenticatedAs' => 2,
'json' => [
'data' => [
'type' => 'users',
'attributes' => [
'isEmailConfirmed' => true
],
@ -652,6 +670,7 @@ class UpdateTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'users',
'attributes' => [
'preferences' => [
'something' => 'else'
@ -674,6 +693,7 @@ class UpdateTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'users',
'attributes' => [
'markedAllAsReadAt' => Carbon::now()
],
@ -694,6 +714,7 @@ class UpdateTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => 'users',
'attributes' => [
'isEmailConfirmed' => true
],
@ -724,7 +745,7 @@ class UpdateTest extends TestCase
],
])
);
$this->assertEquals(403, $response->getStatusCode());
$this->assertEquals(403, $response->getStatusCode(), (string) $response->getBody());
}
/**

View File

@ -33,12 +33,14 @@ class EventTest extends TestCase
return $api->forResource(GroupResource::class)
->forEndpoint(Create::class)
->execute([
'attributes' => [
'nameSingular' => 'test group',
'namePlural' => 'test groups',
'color' => '#000000',
'icon' => 'fas fa-crown',
]
'data' => [
'attributes' => [
'nameSingular' => 'test group',
'namePlural' => 'test groups',
'color' => '#000000',
'icon' => 'fas fa-crown',
]
],
]);
}

View File

@ -60,6 +60,7 @@ class SearchIndexTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => $type,
'attributes' => [
$attribute => 'test',
],
@ -93,6 +94,7 @@ class SearchIndexTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => $type,
'attributes' => [
$attribute => 'changed'
]
@ -137,6 +139,7 @@ class SearchIndexTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => $type,
'attributes' => [
'isHidden' => true
]
@ -162,6 +165,7 @@ class SearchIndexTest extends TestCase
'authenticatedAs' => 1,
'json' => [
'data' => [
'type' => $type,
'attributes' => [
'isHidden' => false
]

View File

@ -34,10 +34,11 @@ class RegisterTest extends TestCase
$this->request('POST', '/register')
);
$this->assertEquals(422, $response->getStatusCode());
$body = (string) $response->getBody();
$this->assertEquals(422, $response->getStatusCode(), $body);
// The response body should contain details about the failed validation
$body = (string) $response->getBody();
$this->assertJson($body);
$this->assertEquals([
'errors' => [

View File

@ -10,6 +10,9 @@
namespace Flarum\Tests\integration\policy;
use Carbon\Carbon;
use Flarum\Api\Endpoint\Create;
use Flarum\Api\JsonApi;
use Flarum\Api\Resource\PostResource;
use Flarum\Bus\Dispatcher;
use Flarum\Discussion\Discussion;
use Flarum\Foundation\DispatchEventsTrait;
@ -93,9 +96,30 @@ class DiscussionPolicyTest extends TestCase
$this->assertTrue($user->can('rename', $discussion));
$this->assertFalse($user->can('rename', $discussionWithReply));
$this->app()->getContainer()->make(Dispatcher::class)->dispatch(
new PostReply(1, User::findOrFail(1), ['attributes' => ['content' => 'test']], null)
);
/** @var JsonApi $api */
$api = $this->app()->getContainer()->make(JsonApi::class);
$api
->forResource(PostResource::class)
->forEndpoint(Create::class)
->execute(
body: [
'data' => [
'attributes' => [
'content' => 'test'
],
'relationships' => [
'discussion' => [
'data' => [
'type' => 'discussions',
'id' => '1'
],
],
],
],
],
options: ['actor' => User::findOrFail(1)]
);
// Date further into the future
Carbon::setTestNow('2025-01-01 13:00:00');