From 385356aad0dd7eac0695bb1597ba1e52b5f17b40 Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Tue, 24 Dec 2019 20:45:46 +0300 Subject: [PATCH 01/18] fix oauth scopes for AdminApi#reports_update --- .../web/admin_api/admin_api_controller.ex | 2 +- .../admin_api/admin_api_controller_test.exs | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index c8abeff06..ddae139c6 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -66,7 +66,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do plug( OAuthScopesPlug, %{scopes: ["write:reports"], admin: true} - when action in [:report_update_state, :report_respond] + when action in [:reports_update] ) plug( diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 49ff005b6..4156ef50d 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -1418,6 +1418,30 @@ test "returns 404 when report id is invalid", %{conn: conn} do } end + test "requires write:reports scope", %{conn: conn, id: id, admin: admin} do + read_token = insert(:oauth_token, user: admin, scopes: ["read"]) + write_token = insert(:oauth_token, user: admin, scopes: ["write:reports"]) + + response = + conn + |> assign(:token, read_token) + |> patch("/api/pleroma/admin/reports", %{ + "reports" => [%{"state" => "resolved", "id" => id}] + }) + |> json_response(403) + + assert response == %{ + "error" => "Insufficient permissions: admin:write:reports | write:reports." + } + + conn + |> assign(:token, write_token) + |> patch("/api/pleroma/admin/reports", %{ + "reports" => [%{"state" => "resolved", "id" => id}] + }) + |> json_response(:no_content) + end + test "mark report as resolved", %{conn: conn, id: id, admin: admin} do conn |> patch("/api/pleroma/admin/reports", %{ From 2ee6754095f5b81807efe97c73ada42e2c990ede Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 14 Jan 2020 17:24:26 +0100 Subject: [PATCH 02/18] Mix Tasks: Add pleroma.benchmarks.tags --- benchmarks/load_testing/generator.ex | 44 +++++++++++++- lib/mix/tasks/pleroma/benchmarks/tags.ex | 57 +++++++++++++++++++ .../controllers/timeline_controller.ex | 14 +++-- 3 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 lib/mix/tasks/pleroma/benchmarks/tags.ex diff --git a/benchmarks/load_testing/generator.ex b/benchmarks/load_testing/generator.ex index a957e0ffb..3f88fefd7 100644 --- a/benchmarks/load_testing/generator.ex +++ b/benchmarks/load_testing/generator.ex @@ -9,7 +9,7 @@ def generate_like_activities(user, posts) do {time, _} = :timer.tc(fn -> Task.async_stream( - Enum.take_random(posts, count_likes), + Enum.take_random(posts, count_likes), fn post -> {:ok, _, _} = CommonAPI.favorite(post.id, user) end, max_concurrency: 10, timeout: 30_000 @@ -142,6 +142,48 @@ defp do_generate_activity(users) do CommonAPI.post(Enum.random(users), post) end + def generate_power_intervals(opts \\ []) do + count = Keyword.get(opts, :count, 20) + power = Keyword.get(opts, :power, 2) + IO.puts("Generating #{count} intervals for a power #{power} series...") + counts = Enum.map(1..count, fn n -> :math.pow(n, power) end) + sum = Enum.sum(counts) + + densities = + Enum.map(counts, fn c -> + c / sum + end) + + densities + |> Enum.reduce(0, fn density, acc -> + if acc == 0 do + [{0, density}] + else + [{_, lower} | _] = acc + [{lower, lower + density} | acc] + end + end) + |> Enum.reverse() + end + + def generate_tagged_activities(opts \\ []) do + tag_count = Keyword.get(opts, :tag_count, 20) + users = Keyword.get(opts, :users, Repo.all(User)) + activity_count = Keyword.get(opts, :count, 200_000) + + intervals = generate_power_intervals(count: tag_count) + + IO.puts( + "Generating #{activity_count} activities using #{tag_count} different tags of format `tag_n`, starting at tag_0" + ) + + Enum.each(1..activity_count, fn _ -> + random = :rand.uniform() + i = Enum.find_index(intervals, fn {lower, upper} -> lower <= random && upper > random end) + CommonAPI.post(Enum.random(users), %{"status" => "a post with the tag #tag_#{i}"}) + end) + end + defp do_generate_activity_with_mention(user, users) do mentions_cnt = Enum.random([2, 3, 4, 5]) with_user = Enum.random([true, false]) diff --git a/lib/mix/tasks/pleroma/benchmarks/tags.ex b/lib/mix/tasks/pleroma/benchmarks/tags.ex new file mode 100644 index 000000000..73796b5f9 --- /dev/null +++ b/lib/mix/tasks/pleroma/benchmarks/tags.ex @@ -0,0 +1,57 @@ +defmodule Mix.Tasks.Pleroma.Benchmarks.Tags do + use Mix.Task + alias Pleroma.Repo + alias Pleroma.LoadTesting.Generator + import Ecto.Query + + def run(_args) do + Mix.Pleroma.start_pleroma() + activities_count = Repo.aggregate(from(a in Pleroma.Activity), :count, :id) + + if activities_count == 0 do + IO.puts("Did not find any activities, cleaning and generating") + clean_tables() + Generator.generate_users(users_max: 10) + Generator.generate_tagged_activities() + else + IO.puts("Found #{activities_count} activities, won't generate new ones") + end + + tags = Enum.map(0..20, fn i -> {"For #tag_#{i}", "tag_#{i}"} end) + + Enum.each(tags, fn {_, tag} -> + query = + from(o in Pleroma.Object, + where: fragment("(?)->'tag' \\? (?)", o.data, ^tag) + ) + + count = Repo.aggregate(query, :count, :id) + IO.puts("Database contains #{count} posts tagged with #{tag}") + end) + + user = Repo.all(Pleroma.User) |> List.first() + + Benchee.run( + %{ + "Hashtag fetching" => fn tag -> + Pleroma.Web.MastodonAPI.TimelineController.hashtag_fetching( + %{ + "tag" => tag + }, + user, + false + ) + end + }, + inputs: tags, + time: 5 + ) + end + + defp clean_tables do + IO.puts("Deleting old data...\n") + Ecto.Adapters.SQL.query!(Repo, "TRUNCATE users CASCADE;") + Ecto.Adapters.SQL.query!(Repo, "TRUNCATE activities CASCADE;") + Ecto.Adapters.SQL.query!(Repo, "TRUNCATE objects CASCADE;") + end +end diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 384159336..29964a1d4 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -77,10 +77,7 @@ def public(%{assigns: %{user: user}} = conn, params) do |> render("index.json", activities: activities, for: user, as: :activity) end - # GET /api/v1/timelines/tag/:tag - def hashtag(%{assigns: %{user: user}} = conn, params) do - local_only = truthy_param?(params["local"]) - + def hashtag_fetching(params, user, local_only) do tags = [params["tag"], params["any"]] |> List.flatten() @@ -98,7 +95,7 @@ def hashtag(%{assigns: %{user: user}} = conn, params) do |> Map.get("none", []) |> Enum.map(&String.downcase(&1)) - activities = + _activities = params |> Map.put("type", "Create") |> Map.put("local_only", local_only) @@ -109,6 +106,13 @@ def hashtag(%{assigns: %{user: user}} = conn, params) do |> Map.put("tag_all", tag_all) |> Map.put("tag_reject", tag_reject) |> ActivityPub.fetch_public_activities() + end + + # GET /api/v1/timelines/tag/:tag + def hashtag(%{assigns: %{user: user}} = conn, params) do + local_only = truthy_param?(params["local"]) + + activities = hashtag_fetching(params, user, local_only) conn |> add_link_headers(activities, %{"local" => local_only}) From 167e9c45eccf5ddb89077c979b1d587318f78cc0 Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 15 Jan 2020 12:37:50 +0100 Subject: [PATCH 03/18] Benchmarks: Move to correct folder --- {lib => benchmarks}/mix/tasks/pleroma/benchmarks/tags.ex | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {lib => benchmarks}/mix/tasks/pleroma/benchmarks/tags.ex (100%) diff --git a/lib/mix/tasks/pleroma/benchmarks/tags.ex b/benchmarks/mix/tasks/pleroma/benchmarks/tags.ex similarity index 100% rename from lib/mix/tasks/pleroma/benchmarks/tags.ex rename to benchmarks/mix/tasks/pleroma/benchmarks/tags.ex From cc4c3255fe052b69e1f0115bfd5d2936c669a385 Mon Sep 17 00:00:00 2001 From: jp Date: Wed, 15 Jan 2020 16:16:29 -0500 Subject: [PATCH 04/18] Fix odd spacing --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2b2601082..3dff6237a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -286,7 +286,7 @@ docker: allow_failure: true script: - docker pull $IMAGE_TAG_SLUG || true - - docker build --cache-from $IMAGE_TAG_SLUG --build-arg VCS_REF=$CI_VCS_REF --build-arg BUILD_DATE=$CI_JOB_TIMESTAMP -t $IMAGE_TAG -t $IMAGE_TAG_SLUG -t $IMAGE_TAG_LATEST . + - docker build --cache-from $IMAGE_TAG_SLUG --build-arg VCS_REF=$CI_VCS_REF --build-arg BUILD_DATE=$CI_JOB_TIMESTAMP -t $IMAGE_TAG -t $IMAGE_TAG_SLUG -t $IMAGE_TAG_LATEST . - docker push $IMAGE_TAG - docker push $IMAGE_TAG_SLUG - docker push $IMAGE_TAG_LATEST @@ -305,7 +305,7 @@ docker-stable: allow_failure: true script: - docker pull $IMAGE_TAG_SLUG || true - - docker build --cache-from $IMAGE_TAG_SLUG --build-arg VCS_REF=$CI_VCS_REF --build-arg BUILD_DATE=$CI_JOB_TIMESTAMP -t $IMAGE_TAG -t $IMAGE_TAG_SLUG -t $IMAGE_TAG_LATEST_STABLE . + - docker build --cache-from $IMAGE_TAG_SLUG --build-arg VCS_REF=$CI_VCS_REF --build-arg BUILD_DATE=$CI_JOB_TIMESTAMP -t $IMAGE_TAG -t $IMAGE_TAG_SLUG -t $IMAGE_TAG_LATEST_STABLE . - docker push $IMAGE_TAG - docker push $IMAGE_TAG_SLUG - docker push $IMAGE_TAG_LATEST_STABLE From 4d19f38753353ab804406bc8a8c11fc57de74c66 Mon Sep 17 00:00:00 2001 From: jp Date: Wed, 15 Jan 2020 16:23:43 -0500 Subject: [PATCH 05/18] update `only:` for release/ branch regex matching. and to avoid running image builds on unnecessary forks --- .gitlab-ci.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3dff6237a..25e002980 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -293,7 +293,9 @@ docker: tags: - dind only: - - develop + - develop@pleroma/pleroma + - /^maint/.*$/@pleroma/pleroma + - /^ci\/.*$/@jp/pleroma docker-stable: stage: docker @@ -312,4 +314,5 @@ docker-stable: tags: - dind only: - - stable + - stable@pleroma/pleroma + - /^release/.*$/@pleroma/pleroma From 0c9499bf25ba096c7491074b4ad4c5a516303f40 Mon Sep 17 00:00:00 2001 From: jp Date: Wed, 15 Jan 2020 16:41:41 -0500 Subject: [PATCH 06/18] Fix only: on docker develop/latest job --- .gitlab-ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 25e002980..44477ea99 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -294,8 +294,6 @@ docker: - dind only: - develop@pleroma/pleroma - - /^maint/.*$/@pleroma/pleroma - - /^ci\/.*$/@jp/pleroma docker-stable: stage: docker From ffab411327cff96e547038f694b3d7d30f45e521 Mon Sep 17 00:00:00 2001 From: jp Date: Wed, 15 Jan 2020 16:54:53 -0500 Subject: [PATCH 07/18] Update docker jobs for release branches and stable branches --- .gitlab-ci.yml | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 44477ea99..318dca87e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -281,11 +281,11 @@ docker: IMAGE_TAG_LATEST_STABLE: $CI_REGISTRY_IMAGE:latest-stable before_script: &before-docker - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY + - docker pull $IMAGE_TAG_SLUG || true - export CI_JOB_TIMESTAMP=$(date --utc -Iseconds) - export CI_VCS_REF=$CI_COMMIT_SHORT_SHA allow_failure: true script: - - docker pull $IMAGE_TAG_SLUG || true - docker build --cache-from $IMAGE_TAG_SLUG --build-arg VCS_REF=$CI_VCS_REF --build-arg BUILD_DATE=$CI_JOB_TIMESTAMP -t $IMAGE_TAG -t $IMAGE_TAG_SLUG -t $IMAGE_TAG_LATEST . - docker push $IMAGE_TAG - docker push $IMAGE_TAG_SLUG @@ -304,7 +304,6 @@ docker-stable: before_script: *before-docker allow_failure: true script: - - docker pull $IMAGE_TAG_SLUG || true - docker build --cache-from $IMAGE_TAG_SLUG --build-arg VCS_REF=$CI_VCS_REF --build-arg BUILD_DATE=$CI_JOB_TIMESTAMP -t $IMAGE_TAG -t $IMAGE_TAG_SLUG -t $IMAGE_TAG_LATEST_STABLE . - docker push $IMAGE_TAG - docker push $IMAGE_TAG_SLUG @@ -313,4 +312,21 @@ docker-stable: - dind only: - stable@pleroma/pleroma + +docker-release: + stage: docker + image: docker:latest + cache: {} + dependencies: [] + variables: *docker-variables + before_script: *before-docker + allow_failure: true + script: + - docker build --cache-from $IMAGE_TAG_SLUG --build-arg VCS_REF=$CI_VCS_REF --build-arg BUILD_DATE=$CI_JOB_TIMESTAMP -t $IMAGE_TAG -t $IMAGE_TAG_SLUG . + - docker push $IMAGE_TAG + - docker push $IMAGE_TAG_SLUG + tags: + - dind + only: - /^release/.*$/@pleroma/pleroma + - /^ci\/.*$/@jp/pleroma From 1e17ad2275d102bd7e3a255989f22cfbbd065ada Mon Sep 17 00:00:00 2001 From: jp Date: Wed, 15 Jan 2020 17:25:33 -0500 Subject: [PATCH 08/18] Update docker release / stable jobs --- .gitlab-ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 318dca87e..5d0d3316a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -329,4 +329,3 @@ docker-release: - dind only: - /^release/.*$/@pleroma/pleroma - - /^ci\/.*$/@jp/pleroma From 6cf3b19a38d08b37a686cd1059604816d11708f1 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Sun, 19 Jan 2020 07:02:16 +0100 Subject: [PATCH 09/18] transmogrifier.ex: simplify handle_incoming of actors --- lib/pleroma/web/activity_pub/transmogrifier.ex | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 3fa789d53..2b8bfc3bd 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -658,24 +658,8 @@ def handle_incoming( with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object) - locked = new_user_data[:locked] || false - attachment = get_in(new_user_data, [:source_data, "attachment"]) || [] - invisible = new_user_data[:invisible] || false - - fields = - attachment - |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end) - |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end) - - update_data = - new_user_data - |> Map.take([:avatar, :banner, :bio, :name, :also_known_as]) - |> Map.put(:fields, fields) - |> Map.put(:locked, locked) - |> Map.put(:invisible, invisible) - actor - |> User.upgrade_changeset(update_data, true) + |> User.upgrade_changeset(new_user_data, true) |> User.update_and_set_cache() ActivityPub.update(%{ From 205e73058d076385e234dafe011a7cd3fad68ef8 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sun, 19 Jan 2020 13:33:46 +0300 Subject: [PATCH 10/18] mix.lock: update mock to 0.3.4 This version adds Mock.passthrough/1 which I need to use in a test --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index ff22791a4..2b449329c 100644 --- a/mix.lock +++ b/mix.lock @@ -63,7 +63,7 @@ "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"}, "mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm"}, - "mock": {:hex, :mock, "0.3.3", "42a433794b1291a9cf1525c6d26b38e039e0d3a360732b5e467bfc77ef26c914", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, + "mock": {:hex, :mock, "0.3.4", "c5862eb3b8c64237f45f586cf00c9d892ba07bb48305a43319d428ce3c2897dd", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, "mogrify": {:hex, :mogrify, "0.6.1", "de1b527514f2d95a7bbe9642eb556061afb337e220cf97adbf3a4e6438ed70af", [:mix], [], "hexpm"}, "mox": {:hex, :mox, "0.5.1", "f86bb36026aac1e6f924a4b6d024b05e9adbed5c63e8daa069bd66fb3292165b", [:mix], [], "hexpm"}, "myhtmlex": {:git, "https://git.pleroma.social/pleroma/myhtmlex.git", "ad0097e2f61d4953bfef20fb6abddf23b87111e6", [ref: "ad0097e2f61d4953bfef20fb6abddf23b87111e6", submodules: true]}, From dc0498ab2b7cee930f426e37ed4f167c2dc83262 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Thu, 16 Jan 2020 19:01:31 +0300 Subject: [PATCH 11/18] Check for unapplied migrations on startup Closes #1328 --- CHANGELOG.md | 1 + lib/pleroma/application.ex | 1 + lib/pleroma/repo.ex | 35 +++++++++++++++++++++++++++++++ test/repo_test.exs | 43 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 182f5e579..af497dcba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Breaking**: MDII uploader ### Changed +- **Breaking:** Pleroma won't start if it detects unapplied migrations - **Breaking:** attachments are removed along with statuses when there are no other references to it - **Breaking:** Elixir >=1.8 is now required (was >= 1.7) - **Breaking:** attachment links (`config :pleroma, :instance, no_attachment_links` and `config :pleroma, Pleroma.Upload, link_name`) disabled by default diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 2ae052069..e17068876 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -33,6 +33,7 @@ def user_agent do def start(_type, _args) do Pleroma.HTML.compile_scrubbers() Pleroma.Config.DeprecationWarnings.warn() + Pleroma.Repo.check_migrations_applied!() setup_instrumenters() load_custom_modules() diff --git a/lib/pleroma/repo.ex b/lib/pleroma/repo.ex index f57e088bc..cb0b6653c 100644 --- a/lib/pleroma/repo.ex +++ b/lib/pleroma/repo.ex @@ -8,6 +8,8 @@ defmodule Pleroma.Repo do adapter: Ecto.Adapters.Postgres, migration_timestamps: [type: :naive_datetime_usec] + require Logger + defmodule Instrumenter do use Prometheus.EctoInstrumenter end @@ -47,4 +49,37 @@ def get_assoc(resource, association) do _ -> {:error, :not_found} end end + + def check_migrations_applied!() do + unless Pleroma.Config.get( + [:i_am_aware_this_may_cause_data_loss, :disable_migration_check], + false + ) do + Ecto.Migrator.with_repo(__MODULE__, fn repo -> + down_migrations = + Ecto.Migrator.migrations(repo) + |> Enum.reject(fn + {:up, _, _} -> true + {:down, _, _} -> false + end) + + if length(down_migrations) > 0 do + down_migrations_text = + Enum.map(down_migrations, fn {:down, id, name} -> "- #{name} (#{id})\n" end) + + Logger.error( + "The following migrations were not applied:\n#{down_migrations_text}If you want to start Pleroma anyway, set\nconfig :pleroma, :i_am_aware_this_may_cause_data_loss, disable_migration_check: true" + ) + + raise Pleroma.Repo.UnappliedMigrationsError + end + end) + else + :ok + end + end +end + +defmodule Pleroma.Repo.UnappliedMigrationsError do + defexception message: "Unapplied Migrations detected" end diff --git a/test/repo_test.exs b/test/repo_test.exs index 85b64d4d1..5526b0327 100644 --- a/test/repo_test.exs +++ b/test/repo_test.exs @@ -4,7 +4,10 @@ defmodule Pleroma.RepoTest do use Pleroma.DataCase + import ExUnit.CaptureLog import Pleroma.Factory + import Mock + alias Pleroma.User describe "find_resource/1" do @@ -46,4 +49,44 @@ test "return error if has not assoc " do assert Repo.get_assoc(token, :user) == {:error, :not_found} end end + + describe "check_migrations_applied!" do + setup_with_mocks([ + {Ecto.Migrator, [], + [ + with_repo: fn repo, fun -> passthrough([repo, fun]) end, + migrations: fn Pleroma.Repo -> + [ + {:up, 20_191_128_153_944, "fix_missing_following_count"}, + {:up, 20_191_203_043_610, "create_report_notes"}, + {:down, 20_191_220_174_645, "add_scopes_to_pleroma_feo_auth_records"} + ] + end + ]} + ]) do + :ok + end + + test "raises if it detects unapplied migrations" do + assert_raise Pleroma.Repo.UnappliedMigrationsError, fn -> + capture_log(&Repo.check_migrations_applied!/0) + end + end + + test "doesn't do anything if disabled" do + disable_migration_check = + Pleroma.Config.get([:i_am_aware_this_may_cause_data_loss, :disable_migration_check]) + + Pleroma.Config.put([:i_am_aware_this_may_cause_data_loss, :disable_migration_check], true) + + on_exit(fn -> + Pleroma.Config.put( + [:i_am_aware_this_may_cause_data_loss, :disable_migration_check], + disable_migration_check + ) + end) + + assert :ok == Repo.check_migrations_applied!() + end + end end From 5c533e10e73d92b753d9ac20b2d7ab14f42649b2 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Mon, 20 Jan 2020 11:53:14 +0100 Subject: [PATCH 12/18] Bump credo to 1.1.5 --- lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex | 2 +- lib/pleroma/web/admin_api/admin_api_controller.ex | 2 +- .../mastodon_api/controllers/subscription_controller.ex | 2 +- lib/pleroma/web/oauth/oauth_controller.ex | 2 +- mix.exs | 2 +- mix.lock | 2 +- test/notification_test.exs | 4 ++-- .../activity_pub/transmogrifier/follow_handling_test.exs | 2 +- test/web/admin_api/admin_api_controller_test.exs | 2 +- test/web/common_api/common_api_utils_test.exs | 8 ++++---- test/web/twitter_api/password_controller_test.exs | 2 +- 11 files changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex b/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex index 4eaea00d8..c184c3b66 100644 --- a/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/vocabulary_policy.ex @@ -20,7 +20,7 @@ def filter(%{"type" => message_type} = message) do with accepted_vocabulary <- Pleroma.Config.get([:mrf_vocabulary, :accept]), rejected_vocabulary <- Pleroma.Config.get([:mrf_vocabulary, :reject]), true <- - length(accepted_vocabulary) == 0 || Enum.member?(accepted_vocabulary, message_type), + Enum.empty?(accepted_vocabulary) || Enum.member?(accepted_vocabulary, message_type), false <- length(rejected_vocabulary) > 0 && Enum.member?(rejected_vocabulary, message_type), {:ok, _} <- filter(message["object"]) do diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 529169c1b..78b77a97a 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -639,7 +639,7 @@ def get_password_reset(conn, %{"nickname" => nickname}) do def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do users = nicknames |> Enum.map(&User.get_cached_by_nickname/1) - Enum.map(users, &User.force_password_reset_async/1) + Enum.each(users, &User.force_password_reset_async/1) ModerationLog.insert_log(%{ actor: admin, diff --git a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex index fc7d52824..11f7b85d3 100644 --- a/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/subscription_controller.ex @@ -6,9 +6,9 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do @moduledoc "The module represents functions to manage user subscriptions." use Pleroma.Web, :controller + alias Pleroma.Web.MastodonAPI.PushSubscriptionView, as: View alias Pleroma.Web.Push alias Pleroma.Web.Push.Subscription - alias Pleroma.Web.MastodonAPI.PushSubscriptionView, as: View action_fallback(:errors) diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index d31a3d91c..5292aedf2 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -14,10 +14,10 @@ defmodule Pleroma.Web.OAuth.OAuthController do alias Pleroma.Web.ControllerHelper alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.Authorization + alias Pleroma.Web.OAuth.Scopes alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken - alias Pleroma.Web.OAuth.Scopes require Logger diff --git a/mix.exs b/mix.exs index c2e0b940f..0aa7c862f 100644 --- a/mix.exs +++ b/mix.exs @@ -124,7 +124,7 @@ defp deps do {:earmark, "~> 1.3"}, {:bbcode, "~> 0.1.1"}, {:ex_machina, "~> 2.3", only: :test}, - {:credo, "~> 0.9.3", only: [:dev, :test]}, + {:credo, "~> 1.1.0", only: [:dev, :test], runtime: false}, {:mock, "~> 0.3.3", only: :test}, {:crypt, git: "https://github.com/msantos/crypt", ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"}, diff --git a/mix.lock b/mix.lock index ff22791a4..9bf7804b3 100644 --- a/mix.lock +++ b/mix.lock @@ -16,7 +16,7 @@ "cors_plug": {:hex, :cors_plug, "1.5.2", "72df63c87e4f94112f458ce9d25800900cc88608c1078f0e4faddf20933eda6e", [:mix], [{:plug, "~> 1.3 or ~> 1.4 or ~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, "cowlib": {:hex, :cowlib, "2.8.0", "fd0ff1787db84ac415b8211573e9a30a3ebe71b5cbff7f720089972b2319c8a4", [:rebar3], [], "hexpm"}, - "credo": {:hex, :credo, "0.9.3", "76fa3e9e497ab282e0cf64b98a624aa11da702854c52c82db1bf24e54ab7c97a", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, + "credo": {:hex, :credo, "1.1.5", "caec7a3cadd2e58609d7ee25b3931b129e739e070539ad1a0cd7efeeb47014f4", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, "crontab": {:hex, :crontab, "1.1.8", "2ce0e74777dfcadb28a1debbea707e58b879e6aa0ffbf9c9bb540887bce43617", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "crypt": {:git, "https://github.com/msantos/crypt", "1f2b58927ab57e72910191a7ebaeff984382a1d3", [ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"]}, "custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm"}, diff --git a/test/notification_test.exs b/test/notification_test.exs index f5f23bb5a..9a1c2f2b5 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -745,7 +745,7 @@ test "it doesn't return notifications from a blocked user when with_muted is set {:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"}) - assert length(Notification.for_user(user, %{with_muted: true})) == 0 + assert Enum.empty?(Notification.for_user(user, %{with_muted: true})) end test "it doesn't return notifications from a domain-blocked user when with_muted is set" do @@ -755,7 +755,7 @@ test "it doesn't return notifications from a domain-blocked user when with_muted {:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"}) - assert length(Notification.for_user(user, %{with_muted: true})) == 0 + assert Enum.empty?(Notification.for_user(user, %{with_muted: true})) end test "it returns notifications from muted threads when with_muted is set" do diff --git a/test/web/activity_pub/transmogrifier/follow_handling_test.exs b/test/web/activity_pub/transmogrifier/follow_handling_test.exs index 7d6d0814d..1c88b05c2 100644 --- a/test/web/activity_pub/transmogrifier/follow_handling_test.exs +++ b/test/web/activity_pub/transmogrifier/follow_handling_test.exs @@ -78,7 +78,7 @@ test "with locked accounts, it does not create a follow or an accept" do ) |> Repo.all() - assert length(accepts) == 0 + assert Enum.empty?(accepts) end test "it works for follow requests when you are already followed, creating a new accept activity" do diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index a3fbb6041..d70fe54b2 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -2840,7 +2840,7 @@ test "GET /instances/:instance/statuses", %{conn: conn} do response = json_response(ret_conn, 200) - assert length(response) == 0 + assert Enum.empty?(response) end end diff --git a/test/web/common_api/common_api_utils_test.exs b/test/web/common_api/common_api_utils_test.exs index 2588898d0..4b761e039 100644 --- a/test/web/common_api/common_api_utils_test.exs +++ b/test/web/common_api/common_api_utils_test.exs @@ -307,7 +307,7 @@ test "for private posts, not a reply" do {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "private", nil) assert length(to) == 2 - assert length(cc) == 0 + assert Enum.empty?(cc) assert mentioned_user.ap_id in to assert user.follower_address in to @@ -323,7 +323,7 @@ test "for private posts, a reply" do {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "private", nil) assert length(to) == 3 - assert length(cc) == 0 + assert Enum.empty?(cc) assert mentioned_user.ap_id in to assert third_user.ap_id in to @@ -338,7 +338,7 @@ test "for direct posts, not a reply" do {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "direct", nil) assert length(to) == 1 - assert length(cc) == 0 + assert Enum.empty?(cc) assert mentioned_user.ap_id in to end @@ -353,7 +353,7 @@ test "for direct posts, a reply" do {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "direct", nil) assert length(to) == 2 - assert length(cc) == 0 + assert Enum.empty?(cc) assert mentioned_user.ap_id in to assert third_user.ap_id in to diff --git a/test/web/twitter_api/password_controller_test.exs b/test/web/twitter_api/password_controller_test.exs index 840c84a05..29ba7d265 100644 --- a/test/web/twitter_api/password_controller_test.exs +++ b/test/web/twitter_api/password_controller_test.exs @@ -55,7 +55,7 @@ test "it returns HTTP 200", %{conn: conn} do user = refresh_record(user) assert Comeonin.Pbkdf2.checkpw("test", user.password_hash) - assert length(Token.get_user_tokens(user)) == 0 + assert Enum.empty?(Token.get_user_tokens(user)) end test "it sets password_reset_pending to false", %{conn: conn} do From 271afcd940b743b84fae2ee40245d1e0c53cc714 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 20 Jan 2020 12:19:15 +0100 Subject: [PATCH 13/18] Add benchmark of any/all tag options --- .../mix/tasks/pleroma/benchmarks/tags.ex | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/benchmarks/mix/tasks/pleroma/benchmarks/tags.ex b/benchmarks/mix/tasks/pleroma/benchmarks/tags.ex index 73796b5f9..fd1506907 100644 --- a/benchmarks/mix/tasks/pleroma/benchmarks/tags.ex +++ b/benchmarks/mix/tasks/pleroma/benchmarks/tags.ex @@ -31,6 +31,36 @@ def run(_args) do user = Repo.all(Pleroma.User) |> List.first() + Benchee.run( + %{ + "Hashtag fetching, any" => fn tags -> + Pleroma.Web.MastodonAPI.TimelineController.hashtag_fetching( + %{ + "any" => tags + }, + user, + false + ) + end, + # Will always return zero results because no overlapping hashtags are generated. + "Hashtag fetching, all" => fn tags -> + Pleroma.Web.MastodonAPI.TimelineController.hashtag_fetching( + %{ + "all" => tags + }, + user, + false + ) + end + }, + inputs: + tags + |> Enum.map(fn {_, v} -> v end) + |> Enum.chunk_every(2) + |> Enum.map(fn tags -> {"For #{inspect(tags)}", tags} end), + time: 5 + ) + Benchee.run( %{ "Hashtag fetching" => fn tag -> From 5fddf988ea45382d8e2307f11205afcc4b468d45 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 20 Jan 2020 13:05:35 +0100 Subject: [PATCH 14/18] Pleroma API: `emoji_reactions_by` does not need authorization --- .../web/pleroma_api/controllers/pleroma_api_controller.ex | 2 +- .../pleroma_api/controllers/pleroma_api_controller_test.exs | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex index 772c535a4..3285dc11b 100644 --- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex @@ -23,7 +23,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do plug( OAuthScopesPlug, %{scopes: ["read:statuses"]} - when action in [:conversation, :conversation_statuses, :emoji_reactions_by] + when action in [:conversation, :conversation_statuses] ) plug( diff --git a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs index 3f7ef13bc..fb7500134 100644 --- a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs +++ b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs @@ -57,11 +57,6 @@ test "GET /api/v1/pleroma/statuses/:id/emoji_reactions_by", %{conn: conn} do {:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"}) - conn = - conn - |> assign(:user, user) - |> assign(:token, insert(:oauth_token, user: user, scopes: ["read:statuses"])) - result = conn |> get("/api/v1/pleroma/statuses/#{activity.id}/emoji_reactions_by") From 510776ba31f0bb01217fd6585848657f5bceb5f7 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 20 Jan 2020 14:27:59 +0100 Subject: [PATCH 15/18] CommonAPI: Don't error out on double favs/repeats --- lib/pleroma/web/common_api/common_api.ex | 16 ++++++++++++---- test/web/common_api/common_api_test.exs | 12 ++++++------ .../controllers/status_controller_test.exs | 7 +++++++ 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 2f3bcfc3c..c05a6c544 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -85,9 +85,13 @@ def delete(activity_id, user) do def repeat(id_or_ap_id, user, params \\ %{}) do with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), object <- Object.normalize(activity), - nil <- Utils.get_existing_announce(user.ap_id, object), + announce_activity <- Utils.get_existing_announce(user.ap_id, object), public <- public_announce?(object, params) do - ActivityPub.announce(user, object, nil, true, public) + if announce_activity do + {:ok, announce_activity, object} + else + ActivityPub.announce(user, object, nil, true, public) + end else _ -> {:error, dgettext("errors", "Could not repeat")} end @@ -105,8 +109,12 @@ def unrepeat(id_or_ap_id, user) do def favorite(id_or_ap_id, user) do with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), object <- Object.normalize(activity), - nil <- Utils.get_existing_like(user.ap_id, object) do - ActivityPub.like(user, object) + like_activity <- Utils.get_existing_like(user.ap_id, object) do + if like_activity do + {:ok, like_activity, object} + else + ActivityPub.like(user, object) + end else _ -> {:error, dgettext("errors", "Could not favorite")} end diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index b5d6d4055..f8963e42e 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -284,22 +284,22 @@ test "favoriting a status" do {:ok, %Activity{}, _} = CommonAPI.favorite(activity.id, user) end - test "retweeting a status twice returns an error" do + test "retweeting a status twice returns the status" do user = insert(:user) other_user = insert(:user) {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"}) - {:ok, %Activity{}, _object} = CommonAPI.repeat(activity.id, user) - {:error, _} = CommonAPI.repeat(activity.id, user) + {:ok, %Activity{} = activity, object} = CommonAPI.repeat(activity.id, user) + {:ok, ^activity, ^object} = CommonAPI.repeat(activity.id, user) end - test "favoriting a status twice returns an error" do + test "favoriting a status twice returns the status" do user = insert(:user) other_user = insert(:user) {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"}) - {:ok, %Activity{}, _object} = CommonAPI.favorite(activity.id, user) - {:error, _} = CommonAPI.favorite(activity.id, user) + {:ok, %Activity{} = activity, object} = CommonAPI.favorite(activity.id, user) + {:ok, ^activity, ^object} = CommonAPI.favorite(activity.id, user) end end diff --git a/test/web/mastodon_api/controllers/status_controller_test.exs b/test/web/mastodon_api/controllers/status_controller_test.exs index 307221c5d..0b75fcc91 100644 --- a/test/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/web/mastodon_api/controllers/status_controller_test.exs @@ -638,6 +638,13 @@ test "favs a status and returns it", %{conn: conn} do assert to_string(activity.id) == id end + test "favoriting twice will just return 200", %{conn: conn} do + activity = insert(:note_activity) + + post(conn, "/api/v1/statuses/#{activity.id}/favourite") + assert post(conn, "/api/v2/statuses/#{activity.id}/favourite") |> json_response(200) + end + test "returns 400 error for a wrong id", %{conn: conn} do conn = post(conn, "/api/v1/statuses/1/favourite") From 2199d63ddcbbfabb4d0daa70cbebfeac2d50717b Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 20 Jan 2020 15:31:14 +0100 Subject: [PATCH 16/18] StatusControllerTest: Fix typo --- test/web/mastodon_api/controllers/status_controller_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/web/mastodon_api/controllers/status_controller_test.exs b/test/web/mastodon_api/controllers/status_controller_test.exs index 0b75fcc91..b03b4b344 100644 --- a/test/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/web/mastodon_api/controllers/status_controller_test.exs @@ -642,7 +642,7 @@ test "favoriting twice will just return 200", %{conn: conn} do activity = insert(:note_activity) post(conn, "/api/v1/statuses/#{activity.id}/favourite") - assert post(conn, "/api/v2/statuses/#{activity.id}/favourite") |> json_response(200) + assert post(conn, "/api/v1/statuses/#{activity.id}/favourite") |> json_response(200) end test "returns 400 error for a wrong id", %{conn: conn} do From 7518f3877f9fb7c62c05292246e4ba82927cb09a Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 20 Jan 2020 16:31:12 +0100 Subject: [PATCH 17/18] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index af497dcba..6e6b1609e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API, streaming: Add `pleroma.direct_conversation_id` to the `conversation` stream event payload. - Admin API: Render whole status in grouped reports - Mastodon API: User timelines will now respect blocks, unless you are getting the user timeline of somebody you blocked (which would be empty otherwise). +- Mastodon API: Favoriting / Repeating a post multiple times will now return the identical response every time. Before, executing that action twice would return an error ("already favorited") on the second try. ### Added From ca3b4de2c9ed2c34d2d789e4b5e2c721c0d99f71 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Mon, 20 Jan 2020 20:04:25 +0300 Subject: [PATCH 18/18] [#1521] AdminApiControllerTest: fixed create report test (OAuth). --- test/web/admin_api/admin_api_controller_test.exs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index d11b26207..c8f8ba310 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -1363,9 +1363,9 @@ test "returns 404 when report id is invalid", %{conn: conn} do } end - test "requires write:reports scope", %{conn: conn, id: id, admin: admin} do - read_token = insert(:oauth_token, user: admin, scopes: ["read"]) - write_token = insert(:oauth_token, user: admin, scopes: ["write:reports"]) + test "requires admin:write:reports scope", %{conn: conn, id: id, admin: admin} do + read_token = insert(:oauth_token, user: admin, scopes: ["admin:read"]) + write_token = insert(:oauth_token, user: admin, scopes: ["admin:write:reports"]) response = conn @@ -1376,7 +1376,7 @@ test "requires write:reports scope", %{conn: conn, id: id, admin: admin} do |> json_response(403) assert response == %{ - "error" => "Insufficient permissions: admin:write:reports | write:reports." + "error" => "Insufficient permissions: admin:write:reports." } conn