Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into feature/expire...
[akkoma] / test / pleroma / web / mastodon_api / controllers / notification_controller_test.exs
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do
6 use Pleroma.Web.ConnCase
7
8 alias Pleroma.Notification
9 alias Pleroma.Repo
10 alias Pleroma.User
11 alias Pleroma.Web.CommonAPI
12
13 import Pleroma.Factory
14
15 test "does NOT render account/pleroma/relationship by default" do
16 %{user: user, conn: conn} = oauth_access(["read:notifications"])
17 other_user = insert(:user)
18
19 {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
20 {:ok, [_notification]} = Notification.create_notifications(activity)
21
22 response =
23 conn
24 |> assign(:user, user)
25 |> get("/api/v1/notifications")
26 |> json_response_and_validate_schema(200)
27
28 assert Enum.all?(response, fn n ->
29 get_in(n, ["account", "pleroma", "relationship"]) == %{}
30 end)
31 end
32
33 test "list of notifications" do
34 %{user: user, conn: conn} = oauth_access(["read:notifications"])
35 other_user = insert(:user)
36
37 {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
38
39 {:ok, [_notification]} = Notification.create_notifications(activity)
40
41 conn =
42 conn
43 |> assign(:user, user)
44 |> get("/api/v1/notifications")
45
46 expected_response =
47 "hi <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{user.id}\" href=\"#{
48 user.ap_id
49 }\" rel=\"ugc\">@<span>#{user.nickname}</span></a></span>"
50
51 assert [%{"status" => %{"content" => response}} | _rest] =
52 json_response_and_validate_schema(conn, 200)
53
54 assert response == expected_response
55 end
56
57 test "by default, does not contain pleroma:chat_mention" do
58 %{user: user, conn: conn} = oauth_access(["read:notifications"])
59 other_user = insert(:user)
60
61 {:ok, _activity} = CommonAPI.post_chat_message(other_user, user, "hey")
62
63 result =
64 conn
65 |> get("/api/v1/notifications")
66 |> json_response_and_validate_schema(200)
67
68 assert [] == result
69
70 result =
71 conn
72 |> get("/api/v1/notifications?include_types[]=pleroma:chat_mention")
73 |> json_response_and_validate_schema(200)
74
75 assert [_] = result
76 end
77
78 test "getting a single notification" do
79 %{user: user, conn: conn} = oauth_access(["read:notifications"])
80 other_user = insert(:user)
81
82 {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
83
84 {:ok, [notification]} = Notification.create_notifications(activity)
85
86 conn = get(conn, "/api/v1/notifications/#{notification.id}")
87
88 expected_response =
89 "hi <span class=\"h-card\"><a class=\"u-url mention\" data-user=\"#{user.id}\" href=\"#{
90 user.ap_id
91 }\" rel=\"ugc\">@<span>#{user.nickname}</span></a></span>"
92
93 assert %{"status" => %{"content" => response}} = json_response_and_validate_schema(conn, 200)
94 assert response == expected_response
95 end
96
97 test "dismissing a single notification (deprecated endpoint)" do
98 %{user: user, conn: conn} = oauth_access(["write:notifications"])
99 other_user = insert(:user)
100
101 {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
102
103 {:ok, [notification]} = Notification.create_notifications(activity)
104
105 conn =
106 conn
107 |> assign(:user, user)
108 |> put_req_header("content-type", "application/json")
109 |> post("/api/v1/notifications/dismiss", %{"id" => to_string(notification.id)})
110
111 assert %{} = json_response_and_validate_schema(conn, 200)
112 end
113
114 test "dismissing a single notification" do
115 %{user: user, conn: conn} = oauth_access(["write:notifications"])
116 other_user = insert(:user)
117
118 {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
119
120 {:ok, [notification]} = Notification.create_notifications(activity)
121
122 conn =
123 conn
124 |> assign(:user, user)
125 |> post("/api/v1/notifications/#{notification.id}/dismiss")
126
127 assert %{} = json_response_and_validate_schema(conn, 200)
128 end
129
130 test "clearing all notifications" do
131 %{user: user, conn: conn} = oauth_access(["write:notifications", "read:notifications"])
132 other_user = insert(:user)
133
134 {:ok, activity} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
135
136 {:ok, [_notification]} = Notification.create_notifications(activity)
137
138 ret_conn = post(conn, "/api/v1/notifications/clear")
139
140 assert %{} = json_response_and_validate_schema(ret_conn, 200)
141
142 ret_conn = get(conn, "/api/v1/notifications")
143
144 assert all = json_response_and_validate_schema(ret_conn, 200)
145 assert all == []
146 end
147
148 test "paginates notifications using min_id, since_id, max_id, and limit" do
149 %{user: user, conn: conn} = oauth_access(["read:notifications"])
150 other_user = insert(:user)
151
152 {:ok, activity1} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
153 {:ok, activity2} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
154 {:ok, activity3} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
155 {:ok, activity4} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
156
157 notification1_id = get_notification_id_by_activity(activity1)
158 notification2_id = get_notification_id_by_activity(activity2)
159 notification3_id = get_notification_id_by_activity(activity3)
160 notification4_id = get_notification_id_by_activity(activity4)
161
162 conn = assign(conn, :user, user)
163
164 # min_id
165 result =
166 conn
167 |> get("/api/v1/notifications?limit=2&min_id=#{notification1_id}")
168 |> json_response_and_validate_schema(:ok)
169
170 assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
171
172 # since_id
173 result =
174 conn
175 |> get("/api/v1/notifications?limit=2&since_id=#{notification1_id}")
176 |> json_response_and_validate_schema(:ok)
177
178 assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
179
180 # max_id
181 result =
182 conn
183 |> get("/api/v1/notifications?limit=2&max_id=#{notification4_id}")
184 |> json_response_and_validate_schema(:ok)
185
186 assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
187 end
188
189 describe "exclude_visibilities" do
190 test "filters notifications for mentions" do
191 %{user: user, conn: conn} = oauth_access(["read:notifications"])
192 other_user = insert(:user)
193
194 {:ok, public_activity} =
195 CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "public"})
196
197 {:ok, direct_activity} =
198 CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "direct"})
199
200 {:ok, unlisted_activity} =
201 CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "unlisted"})
202
203 {:ok, private_activity} =
204 CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "private"})
205
206 query = params_to_query(%{exclude_visibilities: ["public", "unlisted", "private"]})
207 conn_res = get(conn, "/api/v1/notifications?" <> query)
208
209 assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200)
210 assert id == direct_activity.id
211
212 query = params_to_query(%{exclude_visibilities: ["public", "unlisted", "direct"]})
213 conn_res = get(conn, "/api/v1/notifications?" <> query)
214
215 assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200)
216 assert id == private_activity.id
217
218 query = params_to_query(%{exclude_visibilities: ["public", "private", "direct"]})
219 conn_res = get(conn, "/api/v1/notifications?" <> query)
220
221 assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200)
222 assert id == unlisted_activity.id
223
224 query = params_to_query(%{exclude_visibilities: ["unlisted", "private", "direct"]})
225 conn_res = get(conn, "/api/v1/notifications?" <> query)
226
227 assert [%{"status" => %{"id" => id}}] = json_response_and_validate_schema(conn_res, 200)
228 assert id == public_activity.id
229 end
230
231 test "filters notifications for Like activities" do
232 user = insert(:user)
233 %{user: other_user, conn: conn} = oauth_access(["read:notifications"])
234
235 {:ok, public_activity} = CommonAPI.post(other_user, %{status: ".", visibility: "public"})
236
237 {:ok, direct_activity} =
238 CommonAPI.post(other_user, %{status: "@#{user.nickname}", visibility: "direct"})
239
240 {:ok, unlisted_activity} =
241 CommonAPI.post(other_user, %{status: ".", visibility: "unlisted"})
242
243 {:ok, private_activity} = CommonAPI.post(other_user, %{status: ".", visibility: "private"})
244
245 {:ok, _} = CommonAPI.favorite(user, public_activity.id)
246 {:ok, _} = CommonAPI.favorite(user, direct_activity.id)
247 {:ok, _} = CommonAPI.favorite(user, unlisted_activity.id)
248 {:ok, _} = CommonAPI.favorite(user, private_activity.id)
249
250 activity_ids =
251 conn
252 |> get("/api/v1/notifications?exclude_visibilities[]=direct")
253 |> json_response_and_validate_schema(200)
254 |> Enum.map(& &1["status"]["id"])
255
256 assert public_activity.id in activity_ids
257 assert unlisted_activity.id in activity_ids
258 assert private_activity.id in activity_ids
259 refute direct_activity.id in activity_ids
260
261 activity_ids =
262 conn
263 |> get("/api/v1/notifications?exclude_visibilities[]=unlisted")
264 |> json_response_and_validate_schema(200)
265 |> Enum.map(& &1["status"]["id"])
266
267 assert public_activity.id in activity_ids
268 refute unlisted_activity.id in activity_ids
269 assert private_activity.id in activity_ids
270 assert direct_activity.id in activity_ids
271
272 activity_ids =
273 conn
274 |> get("/api/v1/notifications?exclude_visibilities[]=private")
275 |> json_response_and_validate_schema(200)
276 |> Enum.map(& &1["status"]["id"])
277
278 assert public_activity.id in activity_ids
279 assert unlisted_activity.id in activity_ids
280 refute private_activity.id in activity_ids
281 assert direct_activity.id in activity_ids
282
283 activity_ids =
284 conn
285 |> get("/api/v1/notifications?exclude_visibilities[]=public")
286 |> json_response_and_validate_schema(200)
287 |> Enum.map(& &1["status"]["id"])
288
289 refute public_activity.id in activity_ids
290 assert unlisted_activity.id in activity_ids
291 assert private_activity.id in activity_ids
292 assert direct_activity.id in activity_ids
293 end
294
295 test "filters notifications for Announce activities" do
296 user = insert(:user)
297 %{user: other_user, conn: conn} = oauth_access(["read:notifications"])
298
299 {:ok, public_activity} = CommonAPI.post(other_user, %{status: ".", visibility: "public"})
300
301 {:ok, unlisted_activity} =
302 CommonAPI.post(other_user, %{status: ".", visibility: "unlisted"})
303
304 {:ok, _} = CommonAPI.repeat(public_activity.id, user)
305 {:ok, _} = CommonAPI.repeat(unlisted_activity.id, user)
306
307 activity_ids =
308 conn
309 |> get("/api/v1/notifications?exclude_visibilities[]=unlisted")
310 |> json_response_and_validate_schema(200)
311 |> Enum.map(& &1["status"]["id"])
312
313 assert public_activity.id in activity_ids
314 refute unlisted_activity.id in activity_ids
315 end
316
317 test "doesn't return less than the requested amount of records when the user's reply is liked" do
318 user = insert(:user)
319 %{user: other_user, conn: conn} = oauth_access(["read:notifications"])
320
321 {:ok, mention} =
322 CommonAPI.post(user, %{status: "@#{other_user.nickname}", visibility: "public"})
323
324 {:ok, activity} = CommonAPI.post(user, %{status: ".", visibility: "public"})
325
326 {:ok, reply} =
327 CommonAPI.post(other_user, %{
328 status: ".",
329 visibility: "public",
330 in_reply_to_status_id: activity.id
331 })
332
333 {:ok, _favorite} = CommonAPI.favorite(user, reply.id)
334
335 activity_ids =
336 conn
337 |> get("/api/v1/notifications?exclude_visibilities[]=direct&limit=2")
338 |> json_response_and_validate_schema(200)
339 |> Enum.map(& &1["status"]["id"])
340
341 assert [reply.id, mention.id] == activity_ids
342 end
343 end
344
345 test "filters notifications using exclude_types" do
346 %{user: user, conn: conn} = oauth_access(["read:notifications"])
347 other_user = insert(:user)
348
349 {:ok, mention_activity} = CommonAPI.post(other_user, %{status: "hey @#{user.nickname}"})
350 {:ok, create_activity} = CommonAPI.post(user, %{status: "hey"})
351 {:ok, favorite_activity} = CommonAPI.favorite(other_user, create_activity.id)
352 {:ok, reblog_activity} = CommonAPI.repeat(create_activity.id, other_user)
353 {:ok, _, _, follow_activity} = CommonAPI.follow(other_user, user)
354
355 mention_notification_id = get_notification_id_by_activity(mention_activity)
356 favorite_notification_id = get_notification_id_by_activity(favorite_activity)
357 reblog_notification_id = get_notification_id_by_activity(reblog_activity)
358 follow_notification_id = get_notification_id_by_activity(follow_activity)
359
360 query = params_to_query(%{exclude_types: ["mention", "favourite", "reblog"]})
361 conn_res = get(conn, "/api/v1/notifications?" <> query)
362
363 assert [%{"id" => ^follow_notification_id}] = json_response_and_validate_schema(conn_res, 200)
364
365 query = params_to_query(%{exclude_types: ["favourite", "reblog", "follow"]})
366 conn_res = get(conn, "/api/v1/notifications?" <> query)
367
368 assert [%{"id" => ^mention_notification_id}] =
369 json_response_and_validate_schema(conn_res, 200)
370
371 query = params_to_query(%{exclude_types: ["reblog", "follow", "mention"]})
372 conn_res = get(conn, "/api/v1/notifications?" <> query)
373
374 assert [%{"id" => ^favorite_notification_id}] =
375 json_response_and_validate_schema(conn_res, 200)
376
377 query = params_to_query(%{exclude_types: ["follow", "mention", "favourite"]})
378 conn_res = get(conn, "/api/v1/notifications?" <> query)
379
380 assert [%{"id" => ^reblog_notification_id}] = json_response_and_validate_schema(conn_res, 200)
381 end
382
383 test "filters notifications using include_types" do
384 %{user: user, conn: conn} = oauth_access(["read:notifications"])
385 other_user = insert(:user)
386
387 {:ok, mention_activity} = CommonAPI.post(other_user, %{status: "hey @#{user.nickname}"})
388 {:ok, create_activity} = CommonAPI.post(user, %{status: "hey"})
389 {:ok, favorite_activity} = CommonAPI.favorite(other_user, create_activity.id)
390 {:ok, reblog_activity} = CommonAPI.repeat(create_activity.id, other_user)
391 {:ok, _, _, follow_activity} = CommonAPI.follow(other_user, user)
392
393 mention_notification_id = get_notification_id_by_activity(mention_activity)
394 favorite_notification_id = get_notification_id_by_activity(favorite_activity)
395 reblog_notification_id = get_notification_id_by_activity(reblog_activity)
396 follow_notification_id = get_notification_id_by_activity(follow_activity)
397
398 conn_res = get(conn, "/api/v1/notifications?include_types[]=follow")
399
400 assert [%{"id" => ^follow_notification_id}] = json_response_and_validate_schema(conn_res, 200)
401
402 conn_res = get(conn, "/api/v1/notifications?include_types[]=mention")
403
404 assert [%{"id" => ^mention_notification_id}] =
405 json_response_and_validate_schema(conn_res, 200)
406
407 conn_res = get(conn, "/api/v1/notifications?include_types[]=favourite")
408
409 assert [%{"id" => ^favorite_notification_id}] =
410 json_response_and_validate_schema(conn_res, 200)
411
412 conn_res = get(conn, "/api/v1/notifications?include_types[]=reblog")
413
414 assert [%{"id" => ^reblog_notification_id}] = json_response_and_validate_schema(conn_res, 200)
415
416 result = conn |> get("/api/v1/notifications") |> json_response_and_validate_schema(200)
417
418 assert length(result) == 4
419
420 query = params_to_query(%{include_types: ["follow", "mention", "favourite", "reblog"]})
421
422 result =
423 conn
424 |> get("/api/v1/notifications?" <> query)
425 |> json_response_and_validate_schema(200)
426
427 assert length(result) == 4
428 end
429
430 test "destroy multiple" do
431 %{user: user, conn: conn} = oauth_access(["read:notifications", "write:notifications"])
432 other_user = insert(:user)
433
434 {:ok, activity1} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
435 {:ok, activity2} = CommonAPI.post(other_user, %{status: "hi @#{user.nickname}"})
436 {:ok, activity3} = CommonAPI.post(user, %{status: "hi @#{other_user.nickname}"})
437 {:ok, activity4} = CommonAPI.post(user, %{status: "hi @#{other_user.nickname}"})
438
439 notification1_id = get_notification_id_by_activity(activity1)
440 notification2_id = get_notification_id_by_activity(activity2)
441 notification3_id = get_notification_id_by_activity(activity3)
442 notification4_id = get_notification_id_by_activity(activity4)
443
444 result =
445 conn
446 |> get("/api/v1/notifications")
447 |> json_response_and_validate_schema(:ok)
448
449 assert [%{"id" => ^notification2_id}, %{"id" => ^notification1_id}] = result
450
451 conn2 =
452 conn
453 |> assign(:user, other_user)
454 |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:notifications"]))
455
456 result =
457 conn2
458 |> get("/api/v1/notifications")
459 |> json_response_and_validate_schema(:ok)
460
461 assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
462
463 query = params_to_query(%{ids: [notification1_id, notification2_id]})
464 conn_destroy = delete(conn, "/api/v1/notifications/destroy_multiple?" <> query)
465
466 assert json_response_and_validate_schema(conn_destroy, 200) == %{}
467
468 result =
469 conn2
470 |> get("/api/v1/notifications")
471 |> json_response_and_validate_schema(:ok)
472
473 assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result
474 end
475
476 test "doesn't see notifications after muting user with notifications" do
477 %{user: user, conn: conn} = oauth_access(["read:notifications"])
478 user2 = insert(:user)
479
480 {:ok, _, _, _} = CommonAPI.follow(user, user2)
481 {:ok, _} = CommonAPI.post(user2, %{status: "hey @#{user.nickname}"})
482
483 ret_conn = get(conn, "/api/v1/notifications")
484
485 assert length(json_response_and_validate_schema(ret_conn, 200)) == 1
486
487 {:ok, _user_relationships} = User.mute(user, user2)
488
489 conn = get(conn, "/api/v1/notifications")
490
491 assert json_response_and_validate_schema(conn, 200) == []
492 end
493
494 test "see notifications after muting user without notifications" do
495 %{user: user, conn: conn} = oauth_access(["read:notifications"])
496 user2 = insert(:user)
497
498 {:ok, _, _, _} = CommonAPI.follow(user, user2)
499 {:ok, _} = CommonAPI.post(user2, %{status: "hey @#{user.nickname}"})
500
501 ret_conn = get(conn, "/api/v1/notifications")
502
503 assert length(json_response_and_validate_schema(ret_conn, 200)) == 1
504
505 {:ok, _user_relationships} = User.mute(user, user2, %{notifications: false})
506
507 conn = get(conn, "/api/v1/notifications")
508
509 assert length(json_response_and_validate_schema(conn, 200)) == 1
510 end
511
512 test "see notifications after muting user with notifications and with_muted parameter" do
513 %{user: user, conn: conn} = oauth_access(["read:notifications"])
514 user2 = insert(:user)
515
516 {:ok, _, _, _} = CommonAPI.follow(user, user2)
517 {:ok, _} = CommonAPI.post(user2, %{status: "hey @#{user.nickname}"})
518
519 ret_conn = get(conn, "/api/v1/notifications")
520
521 assert length(json_response_and_validate_schema(ret_conn, 200)) == 1
522
523 {:ok, _user_relationships} = User.mute(user, user2)
524
525 conn = get(conn, "/api/v1/notifications?with_muted=true")
526
527 assert length(json_response_and_validate_schema(conn, 200)) == 1
528 end
529
530 @tag capture_log: true
531 test "see move notifications" do
532 old_user = insert(:user)
533 new_user = insert(:user, also_known_as: [old_user.ap_id])
534 %{user: follower, conn: conn} = oauth_access(["read:notifications"])
535
536 old_user_url = old_user.ap_id
537
538 body =
539 File.read!("test/fixtures/users_mock/localhost.json")
540 |> String.replace("{{nickname}}", old_user.nickname)
541 |> Jason.encode!()
542
543 Tesla.Mock.mock(fn
544 %{method: :get, url: ^old_user_url} ->
545 %Tesla.Env{status: 200, body: body}
546 end)
547
548 User.follow(follower, old_user)
549 Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user)
550 Pleroma.Tests.ObanHelpers.perform_all()
551
552 conn = get(conn, "/api/v1/notifications")
553
554 assert length(json_response_and_validate_schema(conn, 200)) == 1
555 end
556
557 describe "link headers" do
558 test "preserves parameters in link headers" do
559 %{user: user, conn: conn} = oauth_access(["read:notifications"])
560 other_user = insert(:user)
561
562 {:ok, activity1} =
563 CommonAPI.post(other_user, %{
564 status: "hi @#{user.nickname}",
565 visibility: "public"
566 })
567
568 {:ok, activity2} =
569 CommonAPI.post(other_user, %{
570 status: "hi @#{user.nickname}",
571 visibility: "public"
572 })
573
574 notification1 = Repo.get_by(Notification, activity_id: activity1.id)
575 notification2 = Repo.get_by(Notification, activity_id: activity2.id)
576
577 conn =
578 conn
579 |> assign(:user, user)
580 |> get("/api/v1/notifications?limit=5")
581
582 assert [link_header] = get_resp_header(conn, "link")
583 assert link_header =~ ~r/limit=5/
584 assert link_header =~ ~r/min_id=#{notification2.id}/
585 assert link_header =~ ~r/max_id=#{notification1.id}/
586 end
587 end
588
589 describe "from specified user" do
590 test "account_id" do
591 %{user: user, conn: conn} = oauth_access(["read:notifications"])
592
593 %{id: account_id} = other_user1 = insert(:user)
594 other_user2 = insert(:user)
595
596 {:ok, _activity} = CommonAPI.post(other_user1, %{status: "hi @#{user.nickname}"})
597 {:ok, _activity} = CommonAPI.post(other_user2, %{status: "bye @#{user.nickname}"})
598
599 assert [%{"account" => %{"id" => ^account_id}}] =
600 conn
601 |> assign(:user, user)
602 |> get("/api/v1/notifications?account_id=#{account_id}")
603 |> json_response_and_validate_schema(200)
604
605 assert %{"error" => "Account is not found"} =
606 conn
607 |> assign(:user, user)
608 |> get("/api/v1/notifications?account_id=cofe")
609 |> json_response_and_validate_schema(404)
610 end
611 end
612
613 defp get_notification_id_by_activity(%{id: id}) do
614 Notification
615 |> Repo.get_by(activity_id: id)
616 |> Map.get(:id)
617 |> to_string()
618 end
619
620 defp params_to_query(%{} = params) do
621 Enum.map_join(params, "&", fn
622 {k, v} when is_list(v) -> Enum.map_join(v, "&", &"#{k}[]=#{&1}")
623 {k, v} -> k <> "=" <> v
624 end)
625 end
626 end