From 48ae3c4347f68e20db7e3e67da32be2e70599fb3 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 5 Dec 2019 20:18:25 +0700 Subject: [PATCH 01/34] Add support for custom modules --- CHANGELOG.md | 1 + config/config.exs | 1 + docs/configuration/cheatsheet.md | 2 ++ lib/pleroma/application.ex | 24 ++++++++++++++++++++++++ 4 files changed, 28 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a06ea211e..6564cf40a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mix task to list all users (`mix pleroma.user list`) - Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache). - MRF: New module which handles incoming posts based on their age. By default, all incoming posts that are older than 2 days will be unlisted and not shown to their followers. +- Support for custom Elixir modules (such as MRF policies)
API Changes diff --git a/config/config.exs b/config/config.exs index b60ffef7d..e1358eda0 100644 --- a/config/config.exs +++ b/config/config.exs @@ -249,6 +249,7 @@ quarantined_instances: [], managed_config: true, static_dir: "instance/static/", + custom_modules_dir: "instance/modules/", allowed_post_formats: [ "text/plain", "text/html", diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index dc2f55229..f73d368c1 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -68,6 +68,8 @@ You shouldn't edit the base config directly to avoid breakages and merge conflic * `account_field_name_length`: An account field name maximum length (default: `512`). * `account_field_value_length`: An account field value maximum length (default: `2048`). * `external_user_synchronization`: Enabling following/followers counters synchronization for external users. +* `custom_modules_dir`: A path to custom Elixir modules (such as MRF policies). + !!! danger This is a Work In Progress, not usable just yet diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 9dbd1e26b..5b6e233a6 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -32,6 +32,7 @@ def user_agent do def start(_type, _args) do Pleroma.Config.DeprecationWarnings.warn() setup_instrumenters() + load_custom_modules() # Define workers and child supervisors to be supervised children = @@ -67,6 +68,29 @@ def start(_type, _args) do Supervisor.start_link(children, opts) end + def load_custom_modules() do + dir = Pleroma.Config.get([:instance, :custom_modules_dir]) + + if dir && File.exists?(dir) do + dir + |> File.ls!() + |> Enum.map(&Path.join(dir, &1)) + |> Kernel.ParallelCompiler.compile() + |> case do + {:error, _errors, _warnings} -> + raise "Invalid custom modules" + + {:ok, modules, _warnings} -> + Enum.each(modules, fn mod -> + name = mod |> Atom.to_string() |> String.trim_leading("Elixir.") + IO.puts("Custom module loaded: #{name}") + end) + + :ok + end + end + end + defp setup_instrumenters do require Prometheus.Registry From 1216b546c6bc0540e266fe0f05829f4f683b1ce9 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 5 Dec 2019 20:29:17 +0700 Subject: [PATCH 02/34] Fix credo warning --- lib/pleroma/application.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 5b6e233a6..73364f141 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -68,7 +68,7 @@ def start(_type, _args) do Supervisor.start_link(children, opts) end - def load_custom_modules() do + def load_custom_modules do dir = Pleroma.Config.get([:instance, :custom_modules_dir]) if dir && File.exists?(dir) do From 157bceeda9124cea7ba69eaf6639ca52b3fac7c6 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Fri, 6 Dec 2019 15:04:46 +0700 Subject: [PATCH 03/34] Move runtime configuration from `:instance` to `:modules` --- config/config.exs | 3 ++- config/releases.exs | 1 + docs/configuration/cheatsheet.md | 12 ++++++++++-- lib/pleroma/application.ex | 2 +- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/config/config.exs b/config/config.exs index e1358eda0..64e33c82f 100644 --- a/config/config.exs +++ b/config/config.exs @@ -249,7 +249,6 @@ quarantined_instances: [], managed_config: true, static_dir: "instance/static/", - custom_modules_dir: "instance/modules/", allowed_post_formats: [ "text/plain", "text/html", @@ -618,6 +617,8 @@ activity_pub: nil, activity_pub_question: 30_000 +config :pleroma, :modules, runtime_dir: "instance/modules" + config :swarm, node_blacklist: [~r/myhtml_.*$/] # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. diff --git a/config/releases.exs b/config/releases.exs index 98c5ceccd..b224960db 100644 --- a/config/releases.exs +++ b/config/releases.exs @@ -2,6 +2,7 @@ config :pleroma, :instance, static_dir: "/var/lib/pleroma/static" config :pleroma, Pleroma.Uploaders.Local, uploads: "/var/lib/pleroma/uploads" +config :pleroma, :modules, runtime_dir: "/var/lib/pleroma/modules" config_path = System.get_env("PLEROMA_CONFIG_PATH") || "/etc/pleroma/config.exs" diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index f73d368c1..413a668c6 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -68,8 +68,6 @@ You shouldn't edit the base config directly to avoid breakages and merge conflic * `account_field_name_length`: An account field name maximum length (default: `512`). * `account_field_value_length`: An account field value maximum length (default: `2048`). * `external_user_synchronization`: Enabling following/followers counters synchronization for external users. -* `custom_modules_dir`: A path to custom Elixir modules (such as MRF policies). - !!! danger This is a Work In Progress, not usable just yet @@ -831,3 +829,13 @@ config :auto_linker, rel: "ugc" ] ``` + +## Custom Runtime Modules (`:modules`) + +* `runtime_dir`: A path to custom Elixir modules (such as MRF policies). + +Example: + +```elixir +config :pleroma, :modules, runtime_dir: "/var/lib/pleroma/modules" +``` diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 73364f141..9d2f3f320 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -69,7 +69,7 @@ def start(_type, _args) do end def load_custom_modules do - dir = Pleroma.Config.get([:instance, :custom_modules_dir]) + dir = Pleroma.Config.get([:modules, :runtime_dir]) if dir && File.exists?(dir) do dir From e4292cbfad47e59c76461fa201bab3e5f791962b Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Fri, 6 Dec 2019 15:16:39 +0700 Subject: [PATCH 04/34] Use Kernel.inspect/2 to print loaded custom modules --- lib/pleroma/application.ex | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 9d2f3f320..17f6b9c80 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -82,8 +82,7 @@ def load_custom_modules do {:ok, modules, _warnings} -> Enum.each(modules, fn mod -> - name = mod |> Atom.to_string() |> String.trim_leading("Elixir.") - IO.puts("Custom module loaded: #{name}") + IO.puts("Custom module loaded: #{inspect(mod)}") end) :ok From a75d4a41e03979b4d1b9af5205e457d714ff76df Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Fri, 6 Dec 2019 17:05:09 +0700 Subject: [PATCH 05/34] Add a test for custom runtime modules --- config/test.exs | 2 ++ lib/pleroma/application.ex | 8 +++++--- test/fixtures/modules/runtime_module.ex | 9 +++++++++ test/runtime_test.exs | 11 +++++++++++ 4 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 test/fixtures/modules/runtime_module.ex create mode 100644 test/runtime_test.exs diff --git a/config/test.exs b/config/test.exs index 9b737d4d7..8b9bf5c77 100644 --- a/config/test.exs +++ b/config/test.exs @@ -93,6 +93,8 @@ config :pleroma, Pleroma.ReverseProxy.Client, Pleroma.ReverseProxy.ClientMock +config :pleroma, :modules, runtime_dir: "test/fixtures/modules" + if File.exists?("./config/test.secret.exs") do import_config "test.secret.exs" else diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 17f6b9c80..82a005700 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -81,9 +81,11 @@ def load_custom_modules do raise "Invalid custom modules" {:ok, modules, _warnings} -> - Enum.each(modules, fn mod -> - IO.puts("Custom module loaded: #{inspect(mod)}") - end) + if @env != :test do + Enum.each(modules, fn mod -> + IO.puts("Custom module loaded: #{inspect(mod)}") + end) + end :ok end diff --git a/test/fixtures/modules/runtime_module.ex b/test/fixtures/modules/runtime_module.ex new file mode 100644 index 000000000..4711c3532 --- /dev/null +++ b/test/fixtures/modules/runtime_module.ex @@ -0,0 +1,9 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule RuntimeModule do + @moduledoc """ + This is a dummy module to test custom runtime modules. + """ +end diff --git a/test/runtime_test.exs b/test/runtime_test.exs new file mode 100644 index 000000000..f7b6f23d4 --- /dev/null +++ b/test/runtime_test.exs @@ -0,0 +1,11 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.RuntimeTest do + use ExUnit.Case, async: true + + test "it loads custom runtime modules" do + assert Code.ensure_compiled?(RuntimeModule) + end +end From 84f891ea3e31c936bc990a3c2310d539df62fc44 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Mon, 9 Dec 2019 18:23:07 +0700 Subject: [PATCH 06/34] Add Pleroma.Utils.compile_dir/1 --- lib/pleroma/application.ex | 4 +--- lib/pleroma/utils.ex | 12 ++++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 lib/pleroma/utils.ex diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 82a005700..104620b37 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -73,9 +73,7 @@ def load_custom_modules do if dir && File.exists?(dir) do dir - |> File.ls!() - |> Enum.map(&Path.join(dir, &1)) - |> Kernel.ParallelCompiler.compile() + |> Pleroma.Utils.compile_dir() |> case do {:error, _errors, _warnings} -> raise "Invalid custom modules" diff --git a/lib/pleroma/utils.ex b/lib/pleroma/utils.ex new file mode 100644 index 000000000..8d36a0001 --- /dev/null +++ b/lib/pleroma/utils.ex @@ -0,0 +1,12 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Utils do + def compile_dir(dir) when is_binary(dir) do + dir + |> File.ls!() + |> Enum.map(&Path.join(dir, &1)) + |> Kernel.ParallelCompiler.compile() + end +end From ed92784e7cfe60756733f518efce14253b1c78d6 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Mon, 9 Dec 2019 19:11:54 +0700 Subject: [PATCH 07/34] Set Logger level to :info in prod --- config/prod.exs | 4 ++-- lib/pleroma/application.ex | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/config/prod.exs b/config/prod.exs index 25873f360..adbce5606 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -20,8 +20,8 @@ config :phoenix, serve_endpoints: true # Do not print debug messages in production -config :logger, :console, level: :warn -config :logger, :ex_syslogger, level: :warn +config :logger, :console, level: :info +config :logger, :ex_syslogger, level: :info # ## SSL Support # diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 104620b37..f47cb0ce9 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Application do import Cachex.Spec use Application + require Logger @name Mix.Project.config()[:name] @version Mix.Project.config()[:version] @@ -81,7 +82,7 @@ def load_custom_modules do {:ok, modules, _warnings} -> if @env != :test do Enum.each(modules, fn mod -> - IO.puts("Custom module loaded: #{inspect(mod)}") + Logger.info("Custom module loaded: #{inspect(mod)}") end) end From 78299ab18205b0bbaf521640e188a862ca27aa61 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Mon, 9 Dec 2019 19:12:24 +0700 Subject: [PATCH 08/34] Set Plug.Logger to log at `:debug` level --- lib/pleroma/web/endpoint.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 49735b5c2..5fcce7ca2 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -59,7 +59,7 @@ defmodule Pleroma.Web.Endpoint do plug(Pleroma.Plugs.TrailingFormatPlug) plug(Plug.RequestId) - plug(Plug.Logger) + plug(Plug.Logger, log: :debug) plug( Plug.Parsers, From b0505b2cc764678501500c242232c6c1afbb3c60 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Mon, 9 Dec 2019 20:07:43 +0300 Subject: [PATCH 09/34] docs: fix incorrect display in digest task docs --- docs/administration/CLI_tasks/digest.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/administration/CLI_tasks/digest.md b/docs/administration/CLI_tasks/digest.md index a70f24c06..1badda8c3 100644 --- a/docs/administration/CLI_tasks/digest.md +++ b/docs/administration/CLI_tasks/digest.md @@ -14,8 +14,9 @@ mix pleroma.digest test [] Example: + ```sh tab="OTP" - ./bin/pleroma_ctl digest test donaldtheduck 2019-05-20 +./bin/pleroma_ctl digest test donaldtheduck 2019-05-20 ``` ```sh tab="From Source" From 0a8b32a661cb470af41c21aa46e55f32c1a762cf Mon Sep 17 00:00:00 2001 From: rinpatch Date: Mon, 9 Dec 2019 20:08:31 +0300 Subject: [PATCH 10/34] docs: remove overoptimistic OTP benefits and use tabs in migration from source --- .../migrating_from_source_otp_en.md | 51 +++++++------------ 1 file changed, 18 insertions(+), 33 deletions(-) diff --git a/docs/installation/migrating_from_source_otp_en.md b/docs/installation/migrating_from_source_otp_en.md index 87568faad..31c2f1294 100644 --- a/docs/installation/migrating_from_source_otp_en.md +++ b/docs/installation/migrating_from_source_otp_en.md @@ -1,42 +1,28 @@ # Switching a from-source install to OTP releases + ## What are OTP releases? OTP releases are as close as you can get to binary releases with Erlang/Elixir. The release is self-contained, and provides everything needed to boot it, it is easily administered via the provided shell script to open up a remote console, start/stop/restart the release, start in the background, send remote commands, and more. -### Can I still run the develop branch if I decide to use them? -Yes, we produce builds for every commit in `develop`. However `develop` is considered unstable, please don't use it in production because of faster access to new features, unless you need them as an app developer. -## Why would one want to switch? -Benefits of OTP releases over from-source installs include: -* **Less space used.** OTP releases come without source code, build tools, have docs and debug symbols stripped from the compiled bytecode and do not cointain tests, docs, revision history. -* **Minimal system dependencies.** Excluding the database and reverse proxy, only `curl`, `unzip` and `ncurses` are needed to download and run the release. Because Erlang runtime and Elixir are shipped with Pleroma, one can use the latest BEAM optimizations and Pleroma features, without having to worry about outdated system repos or a missing `erlang-*` package. -* **Potentially less bugs and better performance.** This extends on the previous point, because we have control over exactly what gets shipped, we can tweak the VM arguments and forget about weird bugs due to Erlang/Elixir version mismatches. -* **Faster and less bug-prone mix tasks.** On a from-source install one has to wait untill a new Pleroma node is started for each mix task and they execute outside of the instance context (for example if a user was deleted via a mix task, the instance will have no knowledge of that and continue to display status count and follows before the cache expires). Mix tasks in OTP releases are executed by calling into a running instance via RPC, which solves both of these problems. -### Sounds great, how do I switch? -Currently we support Linux machines with GNU (e.g. Debian, Ubuntu) or musl (e.g. Alpine) libc and `x86_64`, `aarch64` or `armv7l` CPUs. If you are unsure, check the [Detecting flavour](otp_en.md#detecting-flavour) section in OTP install guide. If your platform is supported, proceed with the guide, if not check the [My platform is not supported](#my-platform-is-not-supported) section. -### I don't think it is worth the effort, can I stay on a from-source install? -Yes, currently there are no plans to deprecate them. - -### My platform is not supported -If you think your platform is a popular choice for running Pleroma instances, or has the potential to become one, you can [file an issue on our Gitlab](https://git.pleroma.social/pleroma/pleroma/issues/new). If not, guides on how to build and update releases by yourself will be available soon. ## Pre-requisites You will be running commands as root. If you aren't root already, please elevate your priviledges by executing `sudo su`/`su`. The system needs to have `curl` and `unzip` installed for downloading and unpacking release builds. -Debian/Ubuntu: -```sh +```sh tab="Alpine" +apk add curl unzip +``` + +```sh tab="Debian/Ubuntu" apt install curl unzip ``` -Alpine: -``` -apk add curl unzip -``` ## Moving content out of the application directory When using OTP releases the application directory changes with every version so it would be a bother to keep content there (and also dangerous unless `--no-rm` option is used when updating). Fortunately almost all paths in Pleroma are configurable, so it is possible to move them out of there. Pleroma should be stopped before proceeding. ### Moving uploads/custom public files directory + ```sh # Create uploads directory and set proper permissions (skip if using a remote uploader) # Note: It does not have to be `/var/lib/pleroma/uploads`, you can configure it to be something else later @@ -92,8 +78,8 @@ Before proceeding, get the flavour from [Detecting flavour](otp_en.md#detecting- rm -r ~pleroma/* # Set the flavour environment variable to the string you got in Detecting flavour section. -# For example if the flavour is `arm64-musl` the command will be -export FLAVOUR="arm64-musl" +# For example if the flavour is `amd64-musl` the command will be +export FLAVOUR="amd64-musl" # Clone the release build into a temporary directory and unpack it # Replace `stable` with `unstable` if you want to run the unstable branch @@ -124,8 +110,15 @@ OTP releases have different service files than from-source installs so they need **Warning:** The service files assume pleroma user's home directory is `/opt/pleroma`, please make sure all paths fit your installation. -Debian/Ubuntu: -```sh +```sh tab="Alpine" +# Copy the service into a proper directory +cp -f ~pleroma/installation/init.d/pleroma /etc/init.d/pleroma + +# Start pleroma +rc-service pleroma start +``` + +```sh tab="Debian/Ubuntu" # Copy the service into a proper directory cp ~pleroma/installation/pleroma.service /etc/systemd/system/pleroma.service @@ -139,14 +132,6 @@ systemctl reenable pleroma systemctl start pleroma ``` -Alpine: -```sh -# Copy the service into a proper directory -cp -f ~pleroma/installation/init.d/pleroma /etc/init.d/pleroma - -# Start pleroma -rc-service pleroma start -``` ## Running mix tasks Refer to [Running mix tasks](otp_en.md#running-mix-tasks) section from OTP release installation guide. ## Updating From 8dbe2dfde1b606ec7dd5461cee89ad1b4e7ca39c Mon Sep 17 00:00:00 2001 From: rinpatch Date: Mon, 9 Dec 2019 20:09:47 +0300 Subject: [PATCH 11/34] docs: use tabs and improve grammar in OTP install guide --- docs/installation/otp_en.md | 154 ++++++++++++++++++------------------ 1 file changed, 79 insertions(+), 75 deletions(-) diff --git a/docs/installation/otp_en.md b/docs/installation/otp_en.md index 965e30e2a..93230806c 100644 --- a/docs/installation/otp_en.md +++ b/docs/installation/otp_en.md @@ -6,7 +6,7 @@ You will be running commands as root. If you aren't root already, please elevate your priviledges by executing `sudo su`/`su`. -While in theory OTP releases are possbile to install on any compatible machine, for the sake of simplicity this guide focuses only on Debian/Ubuntu/Alpine. +While in theory OTP releases are possbile to install on any compatible machine, for the sake of simplicity this guide focuses only on Debian/Ubuntu and Alpine. ### Detecting flavour @@ -20,6 +20,7 @@ If your platform is supported the output will contain the flavour string, you wi ### Installing the required packages Other than things bundled in the OTP release Pleroma depends on: + * curl (to download the release build) * unzip (needed to unpack release builds) * ncurses (ERTS won't run without it) @@ -27,18 +28,16 @@ Other than things bundled in the OTP release Pleroma depends on: * nginx (could be swapped with another reverse proxy but this guide covers only it) * certbot (for Let's Encrypt certificates, could be swapped with another ACME client, but this guide covers only it) -Debian/Ubuntu: -```sh -apt install curl unzip libncurses5 postgresql postgresql-contrib nginx certbot -``` -Alpine: - -```sh +```sh tab="Alpine" echo "http://nl.alpinelinux.org/alpine/latest-stable/community" >> /etc/apk/repositories apk update apk add curl unzip ncurses postgresql postgresql-contrib nginx certbot ``` +```sh tab="Debian/Ubuntu" +apt install curl unzip libncurses5 postgresql postgresql-contrib nginx certbot +``` + ## Setup ### Configuring PostgreSQL #### (Optional) Installing RUM indexes @@ -48,12 +47,7 @@ apk add curl unzip ncurses postgresql postgresql-contrib nginx certbot RUM indexes are an alternative indexing scheme that is not included in PostgreSQL by default. You can read more about them on the [Configuration page](../configuration/cheatsheet.md#rum-indexing-for-full-text-search). They are completely optional and most of the time are not worth it, especially if you are running a single user instance (unless you absolutely need ordered search results). -Debian/Ubuntu (available only on Buster/19.04): -```sh -apt install postgresql-11-rum -``` -Alpine: -```sh +```sh tab="Alpine" apk add git build-base postgresql-dev git clone https://github.com/postgrespro/rum /tmp/rum cd /tmp/rum @@ -62,25 +56,31 @@ make USE_PGXS=1 install cd rm -r /tmp/rum ``` + +```sh tab="Debian/Ubuntu" +# Available only on Buster/19.04 +apt install postgresql-11-rum +``` + #### (Optional) Performance configuration For optimal performance, you may use [PGTune](https://pgtune.leopard.in.ua), don't forget to restart postgresql after editing the configuration -Debian/Ubuntu: -```sh -systemctl restart postgresql -``` -Alpine: -```sh +```sh tab="Alpine" rc-service postgresql restart ``` + +```sh tab="Debian/Ubuntu" +systemctl restart postgresql +``` + ### Installing Pleroma ```sh -# Create the Pleroma user +# Create a Pleroma user adduser --system --shell /bin/false --home /opt/pleroma pleroma # Set the flavour environment variable to the string you got in Detecting flavour section. -# For example if the flavour is `arm64-musl` the command will be -export FLAVOUR="arm64-musl" +# For example if the flavour is `amd64-musl` the command will be +export FLAVOUR="amd64-musl" # Clone the release build into a temporary directory and unpack it su pleroma -s $SHELL -lc " @@ -133,49 +133,52 @@ su pleroma -s $SHELL -lc "./bin/pleroma stop" ### Setting up nginx and getting Let's Encrypt SSL certificaties +#### Get a Let's Encrypt certificate ```sh -# Get a Let's Encrypt certificate certbot certonly --standalone --preferred-challenges http -d yourinstance.tld +``` -# Copy the Pleroma nginx configuration to the nginx folder -# The location of nginx configs is dependent on the distro +#### Copy Pleroma nginx configuration to the nginx folder -# For Debian/Ubuntu: +The location of nginx configs is dependent on the distro + +```sh tab="Alpine" +cp /opt/pleroma/installation/pleroma.nginx /etc/nginx/conf.d/pleroma.conf +``` + +```sh tab="Debian/Ubuntu" cp /opt/pleroma/installation/pleroma.nginx /etc/nginx/sites-available/pleroma.nginx ln -s /etc/nginx/sites-available/pleroma.nginx /etc/nginx/sites-enabled/pleroma.nginx -# For Alpine: -cp /opt/pleroma/installation/pleroma.nginx /etc/nginx/conf.d/pleroma.conf -# If your distro does not have either of those you can append -# `include /etc/nginx/pleroma.conf` to the end of the http section in /etc/nginx/nginx.conf and -cp /opt/pleroma/installation/pleroma.nginx /etc/nginx/pleroma.conf +``` -# Edit the nginx config replacing example.tld with your (sub)domain +If your distro does not have either of those you can append `include /etc/nginx/pleroma.conf` to the end of the http section in /etc/nginx/nginx.conf and +```sh +cp /opt/pleroma/installation/pleroma.nginx /etc/nginx/pleroma.conf +``` + +#### Edit the nginx config +```sh +# Replace example.tld with your (sub)domain $EDITOR path-to-nginx-config # Verify that the config is valid nginx -t +``` +#### Start nginx -# Start nginx -# For Debian/Ubuntu: -systemctl start nginx -# For Alpine: +```sh tab="Alpine" rc-service nginx start ``` -At this point if you open your (sub)domain in a browser you should see a 502 error, that's because pleroma is not started yet. +```sh tab="Debian/Ubuntu" +systemctl start nginx +``` + +At this point if you open your (sub)domain in a browser you should see a 502 error, that's because Pleroma is not started yet. ### Setting up a system service -Debian/Ubuntu: -```sh -# Copy the service into a proper directory -cp /opt/pleroma/installation/pleroma.service /etc/systemd/system/pleroma.service -# Start pleroma and enable it on boot -systemctl start pleroma -systemctl enable pleroma -``` -Alpine: -```sh +```sh tab="Alpine" # Copy the service into a proper directory cp /opt/pleroma/installation/init.d/pleroma /etc/init.d/pleroma @@ -184,13 +187,22 @@ rc-service pleroma start rc-update add pleroma ``` +```sh tab="Debian/Ubuntu" +# Copy the service into a proper directory +cp /opt/pleroma/installation/pleroma.service /etc/systemd/system/pleroma.service + +# Start pleroma and enable it on boot +systemctl start pleroma +systemctl enable pleroma +``` + If everything worked, you should see Pleroma-FE when visiting your domain. If that didn't happen, try reviewing the installation steps, starting Pleroma in the foreground and seeing if there are any errrors. -Still doesn't work? Feel free to contact us on [#pleroma on freenode](https://webchat.freenode.net/?channels=%23pleroma) or via matrix at , you can also [file an issue on our Gitlab](https://git.pleroma.social/pleroma/pleroma/issues/new) +Still doesn't work? Feel free to contact us on [#pleroma on freenode](https://irc.pleroma.social) or via matrix at , you can also [file an issue on our Gitlab](https://git.pleroma.social/pleroma/pleroma-support/issues/new) ## Post installation -### Setting up auto-renew Let's Encrypt certificate +### Setting up auto-renew of the Let's Encrypt certificate ```sh # Create the directory for webroot challenges mkdir -p /var/lib/letsencrypt @@ -201,25 +213,8 @@ $EDITOR path-to-nginx-config # Verify that the config is valid nginx -t ``` -Debian/Ubuntu: -```sh -# Restart nginx -systemctl restart nginx -# Ensure the webroot menthod and post hook is working -certbot renew --cert-name yourinstance.tld --webroot -w /var/lib/letsencrypt/ --dry-run --post-hook 'systemctl nginx reload' - -# Add it to the daily cron -echo '#!/bin/sh -certbot renew --cert-name yourinstance.tld --webroot -w /var/lib/letsencrypt/ --post-hook "systemctl reload nginx" -' > /etc/cron.daily/renew-pleroma-cert -chmod +x /etc/cron.daily/renew-pleroma-cert - -# If everything worked the output should contain /etc/cron.daily/renew-pleroma-cert -run-parts --test /etc/cron.daily -``` -Alpine: -```sh +```sh tab="Alpine" # Restart nginx rc-service nginx restart @@ -236,15 +231,25 @@ certbot renew --cert-name yourinstance.tld --webroot -w /var/lib/letsencrypt/ -- ' > /etc/periodic/daily/renew-pleroma-cert chmod +x /etc/periodic/daily/renew-pleroma-cert -# If everything worked this should output /etc/periodic/daily/renew-pleroma-cert +# If everything worked the output should contain /etc/cron.daily/renew-pleroma-cert run-parts --test /etc/periodic/daily ``` -### Running mix tasks -Throughout the wiki and guides there is a lot of references to mix tasks. Since `mix` is a build tool, you can't just call `mix pleroma.task`, instead you should call `pleroma_ctl` stripping pleroma/ecto namespace. -So for example, if the task is `mix pleroma.user set admin --admin`, you should run it like this: -```sh -su pleroma -s $SHELL -lc "./bin/pleroma_ctl user set admin --admin" +```sh tab="Debian/Ubuntu" +# Restart nginx +systemctl restart nginx + +# Ensure the webroot menthod and post hook is working +certbot renew --cert-name yourinstance.tld --webroot -w /var/lib/letsencrypt/ --dry-run --post-hook 'systemctl reload nginx' + +# Add it to the daily cron +echo '#!/bin/sh +certbot renew --cert-name yourinstance.tld --webroot -w /var/lib/letsencrypt/ --post-hook "systemctl reload nginx" +' > /etc/cron.daily/renew-pleroma-cert +chmod +x /etc/cron.daily/renew-pleroma-cert + +# If everything worked the output should contain /etc/cron.daily/renew-pleroma-cert +run-parts --test /etc/cron.daily ``` ## Create your first user and set as admin @@ -270,4 +275,3 @@ But you should **always check the release notes/changelog** in case there are co * [Backup your instance](../administration/backup.md) * [Hardening your instance](../configuration/hardening.md) * [How to activate mediaproxy](../configuration/howto_mediaproxy.md) -* [Updating your instance](../administration/updating.md) From d237e9b11d9deb92145fc5ce7a3dc81135fc91e9 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Mon, 9 Dec 2019 20:11:39 +0300 Subject: [PATCH 12/34] docs: drop CentOS 7 install guide --- docs/installation/centos7_en.md | 274 -------------------------------- 1 file changed, 274 deletions(-) delete mode 100644 docs/installation/centos7_en.md diff --git a/docs/installation/centos7_en.md b/docs/installation/centos7_en.md deleted file mode 100644 index ad4f58dc1..000000000 --- a/docs/installation/centos7_en.md +++ /dev/null @@ -1,274 +0,0 @@ -# Installing on CentOS 7 -## Installation - -This guide is a step-by-step installation guide for CentOS 7. It also assumes that you have administrative rights, either as root or a user with [sudo permissions](https://www.digitalocean.com/community/tutorials/how-to-create-a-sudo-user-on-centos-quickstart). If you want to run this guide with root, ignore the `sudo` at the beginning of the lines, unless it calls a user like `sudo -Hu pleroma`; in this case, use `su -s $SHELL -c 'command'` instead. - -### Required packages - -* `postgresql` (9,6+, CentOS 7 comes with 9.2, we will install version 11 in this guide) -* `elixir` (1.5+) -* `erlang` -* `erlang-parsetools` -* `erlang-xmerl` -* `git` -* Development Tools - -#### Optional packages used in this guide - -* `nginx` (preferred, example configs for other reverse proxies can be found in the repo) -* `certbot` (or any other ACME client for Let’s Encrypt certificates) - -### Prepare the system - -* First update the system, if not already done: - -```shell -sudo yum update -``` - -* Install some of the above mentioned programs: - -```shell -sudo yum install wget git unzip -``` - -* Install development tools: - -```shell -sudo yum group install "Development Tools" -``` - -### Install Elixir and Erlang - -* Add the EPEL repo: - -```shell -sudo yum install epel-release -sudo yum -y update -``` - -* Install Erlang repository: - -```shell -wget -P /tmp/ https://packages.erlang-solutions.com/erlang-solutions-1.0-1.noarch.rpm -sudo rpm -Uvh erlang-solutions-1.0-1.noarch.rpm -``` - -* Install Erlang: - -```shell -sudo yum install erlang erlang-parsetools erlang-xmerl -``` - -* Download [latest Elixir release from Github](https://github.com/elixir-lang/elixir/releases/tag/v1.8.1) (Example for the newest version at the time when this manual was written) - -```shell -wget -P /tmp/ https://github.com/elixir-lang/elixir/releases/download/v1.8.1/Precompiled.zip -``` - -* Create folder where you want to install Elixir, we’ll use: - -```shell -sudo mkdir -p /opt/elixir -``` - -* Unzip downloaded file there: - -```shell -sudo unzip /tmp/Precompiled.zip -d /opt/elixir -``` - -* Create symlinks for the pre-compiled binaries: - -```shell -for e in elixir elixirc iex mix; do sudo ln -s /opt/elixir/bin/${e} /usr/local/bin/${e}; done -``` - -### Install PostgreSQL - -* Add the Postgresql repository: - -```shell -sudo yum install https://download.postgresql.org/pub/repos/yum/11/redhat/rhel-7-x86_64/pgdg-centos11-11-2.noarch.rpm -``` - -* Install the Postgresql server: - -```shell -sudo yum install postgresql11-server postgresql11-contrib -``` - -* Initialize database: - -```shell -sudo /usr/pgsql-11/bin/postgresql-11-setup initdb -``` - -* Open configuration file `/var/lib/pgsql/11/data/pg_hba.conf` and change the following lines from: - -```plain -# IPv4 local connections: -host all all 127.0.0.1/32 ident -# IPv6 local connections: -host all all ::1/128 ident -``` - -to - -```plain -# IPv4 local connections: -host all all 127.0.0.1/32 md5 -# IPv6 local connections: -host all all ::1/128 md5 -``` - -* Enable and start postgresql server: - -```shell -sudo systemctl enable --now postgresql-11.service -``` - -### Install PleromaBE - -* Add a new system user for the Pleroma service: - -```shell -sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma -``` - -**Note**: To execute a single command as the Pleroma system user, use `sudo -Hu pleroma command`. You can also switch to a shell by using `sudo -Hu pleroma $SHELL`. If you don’t have and want `sudo` on your system, you can use `su` as root user (UID 0) for a single command by using `su -l pleroma -s $SHELL -c 'command'` and `su -l pleroma -s $SHELL` for starting a shell. - -* Git clone the PleromaBE repository and make the Pleroma user the owner of the directory: - -```shell -sudo mkdir -p /opt/pleroma -sudo chown -R pleroma:pleroma /opt/pleroma -sudo -Hu pleroma git clone -b stable https://git.pleroma.social/pleroma/pleroma /opt/pleroma -``` - -* Change to the new directory: - -```shell -cd /opt/pleroma -``` - -* Install the dependencies for Pleroma and answer with `yes` if it asks you to install `Hex`: - -```shell -sudo -Hu pleroma mix deps.get -``` - -* Generate the configuration: `sudo -Hu pleroma mix pleroma.instance gen` - * Answer with `yes` if it asks you to install `rebar3`. - * This may take some time, because parts of pleroma get compiled first. - * After that it will ask you a few questions about your instance and generates a configuration file in `config/generated_config.exs`. - -* Check the configuration and if all looks right, rename it, so Pleroma will load it (`prod.secret.exs` for productive instance, `dev.secret.exs` for development instances): - -```shell -mv config/{generated_config.exs,prod.secret.exs} -``` - -* The previous command creates also the file `config/setup_db.psql`, with which you can create the database: - -```shell -sudo -Hu postgres psql -f config/setup_db.psql -``` - -* Now run the database migration: - -```shell -sudo -Hu pleroma MIX_ENV=prod mix ecto.migrate -``` - -* Now you can start Pleroma already - -```shell -sudo -Hu pleroma MIX_ENV=prod mix phx.server -``` - -### Finalize installation - -If you want to open your newly installed instance to the world, you should run nginx or some other webserver/proxy in front of Pleroma and you should consider to create a systemd service file for Pleroma. - -#### Nginx - -* Install nginx, if not already done: - -```shell -sudo yum install nginx -``` - -* Setup your SSL cert, using your method of choice or certbot. If using certbot, first install it: - -```shell -sudo yum install certbot-nginx -``` - -and then set it up: - -```shell -sudo mkdir -p /var/lib/letsencrypt/ -sudo certbot certonly --email -d --standalone -``` - -If that doesn’t work, make sure, that nginx is not already running. If it still doesn’t work, try setting up nginx first (change ssl “on” to “off” and try again). - ---- - -* Copy the example nginx configuration to the nginx folder - -```shell -sudo cp /opt/pleroma/installation/pleroma.nginx /etc/nginx/conf.d/pleroma.conf -``` - -* Before starting nginx edit the configuration and change it to your needs (e.g. change servername, change cert paths) -* Enable and start nginx: - -```shell -sudo systemctl enable --now nginx -``` - -If you need to renew the certificate in the future, uncomment the relevant location block in the nginx config and run: - -```shell -sudo certbot certonly --email -d --webroot -w /var/lib/letsencrypt/ -``` - -#### Other webserver/proxies - -You can find example configurations for them in `/opt/pleroma/installation/`. - -#### Systemd service - -* Copy example service file - -```shell -sudo cp /opt/pleroma/installation/pleroma.service /etc/systemd/system/pleroma.service -``` - -* Edit the service file and make sure that all paths fit your installation -* Enable and start `pleroma.service`: - -```shell -sudo systemctl enable --now pleroma.service -``` - -#### Create your first user - -If your instance is up and running, you can create your first user with administrative rights with the following task: - -```shell -sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new --admin -``` - -#### Further reading - -* [Backup your instance](../administration/backup.md) -* [Hardening your instance](../configuration/hardening.md) -* [How to activate mediaproxy](../configuration/howto_mediaproxy.md) -* [Updating your instance](../administration/updating.md) - -## Questions - -Questions about the installation or didn’t it work as it should be, ask in [#pleroma:matrix.org](https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org) or IRC Channel **#pleroma** on **Freenode**. From b7a57d8e388a03d7d92248aa8c583365bde9d0b1 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 10 Dec 2019 00:38:01 +0700 Subject: [PATCH 13/34] Use Pleroma.Utils.compile_dir/1 in Pleroma.HTML.compile_scrubbers/0 --- lib/pleroma/html.ex | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex index 2cae29f35..11513106e 100644 --- a/lib/pleroma/html.ex +++ b/lib/pleroma/html.ex @@ -10,9 +10,7 @@ def compile_scrubbers do dir = Path.join(:code.priv_dir(:pleroma), "scrubbers") dir - |> File.ls!() - |> Enum.map(&Path.join(dir, &1)) - |> Kernel.ParallelCompiler.compile() + |> Pleroma.Utils.compile_dir() |> case do {:error, _errors, _warnings} -> raise "Compiling scrubbers failed" From a37bd5c25587528b9f7a8ac1d148f6a4eb171769 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Tue, 10 Dec 2019 15:08:57 +0700 Subject: [PATCH 14/34] Change log level --- lib/pleroma/object/fetcher.ex | 2 +- lib/pleroma/web/activity_pub/publisher.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index 4d71c91a8..a1bde90f1 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -154,7 +154,7 @@ defp maybe_date_fetch(headers, date) do end def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do - Logger.info("Fetching object #{id} via AP") + Logger.debug("Fetching object #{id} via AP") date = Pleroma.Signature.signed_date() diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index 4ea37fc7b..e834f43ad 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -47,7 +47,7 @@ def is_representable?(%Activity{} = activity) do * `id`: the ActivityStreams URI of the message """ def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do - Logger.info("Federating #{id} to #{inbox}") + Logger.debug("Federating #{id} to #{inbox}") %{host: host, path: path} = URI.parse(inbox) digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64()) From ee6805850c8a86105b7f16d0510cf8465ba24452 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Wed, 11 Dec 2019 17:46:07 +0700 Subject: [PATCH 15/34] Set log level to debug for not important messages --- lib/pleroma/web/activity_pub/activity_pub_controller.ex | 6 +++--- lib/pleroma/web/activity_pub/mrf/drop_policy.ex | 2 +- .../web/activity_pub/mrf/mediaproxy_warming_policy.ex | 2 +- lib/pleroma/web/activity_pub/publisher.ex | 2 +- lib/pleroma/web/federator/federator.ex | 8 ++++---- lib/pleroma/web/federator/publisher.ex | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index dec5da0d3..5059e3984 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -257,7 +257,7 @@ def inbox(%{assigns: %{valid_signature: true}} = conn, params) do # only accept relayed Creates def inbox(conn, %{"type" => "Create"} = params) do - Logger.info( + Logger.debug( "Signature missing or not from author, relayed Create message, fetching object from source" ) @@ -270,11 +270,11 @@ def inbox(conn, params) do headers = Enum.into(conn.req_headers, %{}) if String.contains?(headers["signature"], params["actor"]) do - Logger.info( + Logger.debug( "Signature validation error for: #{params["actor"]}, make sure you are forwarding the HTTP Host header!" ) - Logger.info(inspect(conn.req_headers)) + Logger.debug(inspect(conn.req_headers)) end json(conn, dgettext("errors", "error")) diff --git a/lib/pleroma/web/activity_pub/mrf/drop_policy.ex b/lib/pleroma/web/activity_pub/mrf/drop_policy.ex index f7831bc3e..4a5709974 100644 --- a/lib/pleroma/web/activity_pub/mrf/drop_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/drop_policy.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.DropPolicy do @impl true def filter(object) do - Logger.info("REJECTING #{inspect(object)}") + Logger.debug("REJECTING #{inspect(object)}") {:reject, object} end diff --git a/lib/pleroma/web/activity_pub/mrf/mediaproxy_warming_policy.ex b/lib/pleroma/web/activity_pub/mrf/mediaproxy_warming_policy.ex index 26b8539fe..df774b0f7 100644 --- a/lib/pleroma/web/activity_pub/mrf/mediaproxy_warming_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/mediaproxy_warming_policy.ex @@ -18,7 +18,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do ] def perform(:prefetch, url) do - Logger.info("Prefetching #{inspect(url)}") + Logger.debug("Prefetching #{inspect(url)}") url |> MediaProxy.url() diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index e834f43ad..aeaddff64 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -223,7 +223,7 @@ def publish(%User{} = actor, %Activity{} = activity) do public = is_public?(activity) if public && Config.get([:instance, :allow_relay]) do - Logger.info(fn -> "Relaying #{activity.data["id"]} out" end) + Logger.debug(fn -> "Relaying #{activity.data["id"]} out" end) Relay.publish(activity) end diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex index e8a56ebd7..f506a7d24 100644 --- a/lib/pleroma/web/federator/federator.ex +++ b/lib/pleroma/web/federator/federator.ex @@ -58,7 +58,7 @@ def perform(:publish, activity) do end def perform(:incoming_ap_doc, params) do - Logger.info("Handling incoming AP activity") + Logger.debug("Handling incoming AP activity") params = Utils.normalize_params(params) @@ -71,13 +71,13 @@ def perform(:incoming_ap_doc, params) do {:ok, activity} else %Activity{} -> - Logger.info("Already had #{params["id"]}") + Logger.debug("Already had #{params["id"]}") :error _e -> # Just drop those for now - Logger.info("Unhandled activity") - Logger.info(Jason.encode!(params, pretty: true)) + Logger.debug("Unhandled activity") + Logger.debug(Jason.encode!(params, pretty: true)) :error end end diff --git a/lib/pleroma/web/federator/publisher.ex b/lib/pleroma/web/federator/publisher.ex index fb9b26649..1d045c644 100644 --- a/lib/pleroma/web/federator/publisher.ex +++ b/lib/pleroma/web/federator/publisher.ex @@ -47,7 +47,7 @@ def publish(%User{} = user, %Activity{} = activity) do Config.get([:instance, :federation_publisher_modules]) |> Enum.each(fn module -> if module.is_representable?(activity) do - Logger.info("Publishing #{activity.data["id"]} using #{inspect(module)}") + Logger.debug("Publishing #{activity.data["id"]} using #{inspect(module)}") module.publish(user, activity) end end) From 1a6e30d32ef17e5791de404e5cebb37844264dcb Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Wed, 11 Dec 2019 22:32:53 +0700 Subject: [PATCH 16/34] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20b8de887..e2249f897 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Deprecated `User.Info` embedded schema (fields moved to `User`) - Store status data inside Flag activity - Deprecated (reorganized as `UserRelationship` entity) User fields with user AP IDs (`blocks`, `mutes`, `muted_reblogs`, `muted_notifications`, `subscribers`). +- Logger: default log level changed from `warn` to `info`.
API Changes From c6f2735ffa1db7871bcb56c00b6d19e4de346d18 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 12 Dec 2019 14:37:57 +0700 Subject: [PATCH 17/34] Remove runtime modules config example --- docs/configuration/cheatsheet.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index 743c188bb..b3a13833c 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -834,9 +834,3 @@ config :auto_linker, ## Custom Runtime Modules (`:modules`) * `runtime_dir`: A path to custom Elixir modules (such as MRF policies). - -Example: - -```elixir -config :pleroma, :modules, runtime_dir: "/var/lib/pleroma/modules" -``` From 7973cbdb9fa9120306cb5a265a477eeccd315ee6 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Sun, 15 Dec 2019 22:32:42 +0300 Subject: [PATCH 18/34] OAuthScopesPlug: disallowed nil token (unless with :fallback option). WIP: controller tests modification: OAuth scopes usage. --- lib/pleroma/plugs/oauth_scopes_plug.ex | 9 +- lib/pleroma/user.ex | 6 +- .../controllers/emoji_api_controller.ex | 2 +- .../controllers/pleroma_api_controller.ex | 9 +- .../controllers/util_controller.ex | 57 +- test/notification_test.exs | 2 +- test/plugs/oauth_scopes_plug_test.exs | 157 ++--- test/support/conn_case.ex | 20 + test/support/factory.ex | 32 +- .../admin_api/admin_api_controller_test.exs | 566 +++++++----------- .../controllers/status_controller_test.exs | 392 +++++------- test/web/oauth/oauth_controller_test.exs | 10 +- .../pleroma_api_controller_test.exs | 40 +- test/web/twitter_api/util_controller_test.exs | 248 ++++---- 14 files changed, 638 insertions(+), 912 deletions(-) diff --git a/lib/pleroma/plugs/oauth_scopes_plug.ex b/lib/pleroma/plugs/oauth_scopes_plug.ex index 174a8389c..07c0f7fdb 100644 --- a/lib/pleroma/plugs/oauth_scopes_plug.ex +++ b/lib/pleroma/plugs/oauth_scopes_plug.ex @@ -18,16 +18,13 @@ def call(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do token = assigns[:token] scopes = transform_scopes(scopes, options) - matched_scopes = token && filter_descendants(scopes, token.scopes) + matched_scopes = (token && filter_descendants(scopes, token.scopes)) || [] cond do - is_nil(token) -> - maybe_perform_instance_privacy_check(conn, options) - - op == :| && Enum.any?(matched_scopes) -> + token && op == :| && Enum.any?(matched_scopes) -> conn - op == :& && matched_scopes == scopes -> + token && op == :& && matched_scopes == scopes -> conn options[:fallback] == :proceed_unauthenticated -> diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 706aee2ff..021a542b3 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1855,9 +1855,9 @@ def admin_api_update(user, params) do ]) with {:ok, updated_user} <- update_and_set_cache(changeset) do - if user.is_admin && !updated_user.is_admin do - # Tokens & authorizations containing any admin scopes must be revoked (revoking all). - # This is an extra safety measure (tokens' admin scopes won't be accepted for non-admins). + if user.is_admin != updated_user.is_admin do + # Admin status change results in change of accessible OAuth scopes, and instead of changing + # already issued tokens we revoke them, requiring user to sign in again global_sign_out(user) end diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex index 69dfa92e3..0bbf84fd3 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex @@ -52,7 +52,7 @@ def list_from(conn, %{"instance_address" => address}) do @doc """ Lists the packs available on the instance as JSON. - The information is public and does not require authentification. The format is + The information is public and does not require authentication. The format is a map of "pack directory name" to pack.json contents. """ def list_packs(conn, _params) do 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 8fed3f5bb..772c535a4 100644 --- a/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex @@ -22,7 +22,14 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do plug( OAuthScopesPlug, - %{scopes: ["read:statuses"]} when action in [:conversation, :conversation_statuses] + %{scopes: ["read:statuses"]} + when action in [:conversation, :conversation_statuses, :emoji_reactions_by] + ) + + plug( + OAuthScopesPlug, + %{scopes: ["write:statuses"]} + when action in [:react_with_emoji, :unreact_with_emoji] ) plug( diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 2305bb413..849783d4a 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -22,7 +22,14 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do plug( OAuthScopesPlug, %{scopes: ["follow", "write:follows"]} - when action in [:do_remote_follow, :follow_import] + when action == :follow_import + ) + + # Note: follower can submit the form (with password auth) not being signed in (having no token) + plug( + OAuthScopesPlug, + %{fallback: :proceed_unauthenticated, scopes: ["follow", "write:follows"]} + when action == :do_remote_follow ) plug(OAuthScopesPlug, %{scopes: ["follow", "write:blocks"]} when action == :blocks_import) @@ -112,6 +119,28 @@ defp is_status?(acct) do end end + def do_remote_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id}}) + when not is_nil(user) do + with {:fetch_user, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)}, + {:ok, _follower, _followee, _activity} <- CommonAPI.follow(user, followee) do + conn + |> render("followed.html", %{error: false}) + else + # Was already following user + {:error, "Could not follow user:" <> _rest} -> + render(conn, "followed.html", %{error: "Error following account"}) + + {:fetch_user, error} -> + Logger.debug("Remote follow failed with error #{inspect(error)}") + render(conn, "followed.html", %{error: "Could not find user"}) + + e -> + Logger.debug("Remote follow failed with error #{inspect(e)}") + render(conn, "followed.html", %{error: "Something went wrong."}) + end + end + + # Note: "id" is the id of followee user, disregard incorrect placing under "authorization" def do_remote_follow(conn, %{ "authorization" => %{"name" => username, "password" => password, "id" => id} }) do @@ -145,24 +174,12 @@ def do_remote_follow(conn, %{ end end - def do_remote_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id}}) do - with {:fetch_user, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)}, - {:ok, _follower, _followee, _activity} <- CommonAPI.follow(user, followee) do - conn - |> render("followed.html", %{error: false}) - else - # Was already following user - {:error, "Could not follow user:" <> _rest} -> - render(conn, "followed.html", %{error: "Error following account"}) + def do_remote_follow(%{assigns: %{user: nil}} = conn, _) do + render(conn, "followed.html", %{error: "Insufficient permissions: follow | write:follows."}) + end - {:fetch_user, error} -> - Logger.debug("Remote follow failed with error #{inspect(error)}") - render(conn, "followed.html", %{error: "Could not find user"}) - - e -> - Logger.debug("Remote follow failed with error #{inspect(e)}") - render(conn, "followed.html", %{error: "Something went wrong."}) - end + def do_remote_follow(conn, _) do + render(conn, "followed.html", %{error: "Something went wrong."}) end def notifications_read(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do @@ -345,7 +362,9 @@ def change_email(%{assigns: %{user: user}} = conn, params) do end def delete_account(%{assigns: %{user: user}} = conn, params) do - case CommonAPI.Utils.confirm_current_password(user, params["password"]) do + password = params["password"] || "" + + case CommonAPI.Utils.confirm_current_password(user, password) do {:ok, user} -> User.delete(user) json(conn, %{status: "success"}) diff --git a/test/notification_test.exs b/test/notification_test.exs index ffa3d4b8c..f5f23bb5a 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -98,7 +98,7 @@ test "it creates a notification for user if the user blocks the activity author" assert Notification.create_notification(activity, user) end - test "it creates a notificatin for the user if the user mutes the activity author" do + test "it creates a notification for the user if the user mutes the activity author" do muter = insert(:user) muted = insert(:user) {:ok, _} = User.mute(muter, muted) diff --git a/test/plugs/oauth_scopes_plug_test.exs b/test/plugs/oauth_scopes_plug_test.exs index 89f32f43a..ce426677b 100644 --- a/test/plugs/oauth_scopes_plug_test.exs +++ b/test/plugs/oauth_scopes_plug_test.exs @@ -16,34 +16,6 @@ defmodule Pleroma.Plugs.OAuthScopesPlugTest do :ok end - describe "when `assigns[:token]` is nil, " do - test "with :skip_instance_privacy_check option, proceeds with no op", %{conn: conn} do - conn = - conn - |> assign(:user, insert(:user)) - |> OAuthScopesPlug.call(%{scopes: ["read"], skip_instance_privacy_check: true}) - - refute conn.halted - assert conn.assigns[:user] - - refute called(EnsurePublicOrAuthenticatedPlug.call(conn, :_)) - end - - test "without :skip_instance_privacy_check option, calls EnsurePublicOrAuthenticatedPlug", %{ - conn: conn - } do - conn = - conn - |> assign(:user, insert(:user)) - |> OAuthScopesPlug.call(%{scopes: ["read"]}) - - refute conn.halted - assert conn.assigns[:user] - - assert called(EnsurePublicOrAuthenticatedPlug.call(conn, :_)) - end - end - test "if `token.scopes` fulfills specified 'any of' conditions, " <> "proceeds with no op", %{conn: conn} do @@ -75,64 +47,56 @@ test "if `token.scopes` fulfills specified 'all of' conditions, " <> end describe "with `fallback: :proceed_unauthenticated` option, " do - test "if `token.scopes` doesn't fulfill specified 'any of' conditions, " <> - "clears `assigns[:user]` and calls EnsurePublicOrAuthenticatedPlug", + test "if `token.scopes` doesn't fulfill specified conditions, " <> + "clears :user and :token assigns and calls EnsurePublicOrAuthenticatedPlug", %{conn: conn} do - token = insert(:oauth_token, scopes: ["read", "write"]) |> Repo.preload(:user) + user = insert(:user) + token1 = insert(:oauth_token, scopes: ["read", "write"], user: user) - conn = - conn - |> assign(:user, token.user) - |> assign(:token, token) - |> OAuthScopesPlug.call(%{scopes: ["follow"], fallback: :proceed_unauthenticated}) + for token <- [token1, nil], op <- [:|, :&] do + ret_conn = + conn + |> assign(:user, user) + |> assign(:token, token) + |> OAuthScopesPlug.call(%{ + scopes: ["follow"], + op: op, + fallback: :proceed_unauthenticated + }) - refute conn.halted - refute conn.assigns[:user] + refute ret_conn.halted + refute ret_conn.assigns[:user] + refute ret_conn.assigns[:token] - assert called(EnsurePublicOrAuthenticatedPlug.call(conn, :_)) - end - - test "if `token.scopes` doesn't fulfill specified 'all of' conditions, " <> - "clears `assigns[:user] and calls EnsurePublicOrAuthenticatedPlug", - %{conn: conn} do - token = insert(:oauth_token, scopes: ["read", "write"]) |> Repo.preload(:user) - - conn = - conn - |> assign(:user, token.user) - |> assign(:token, token) - |> OAuthScopesPlug.call(%{ - scopes: ["read", "follow"], - op: :&, - fallback: :proceed_unauthenticated - }) - - refute conn.halted - refute conn.assigns[:user] - - assert called(EnsurePublicOrAuthenticatedPlug.call(conn, :_)) + assert called(EnsurePublicOrAuthenticatedPlug.call(ret_conn, :_)) + end end test "with :skip_instance_privacy_check option, " <> "if `token.scopes` doesn't fulfill specified conditions, " <> - "clears `assigns[:user]` and does not call EnsurePublicOrAuthenticatedPlug", + "clears :user and :token assigns and does NOT call EnsurePublicOrAuthenticatedPlug", %{conn: conn} do - token = insert(:oauth_token, scopes: ["read:statuses", "write"]) |> Repo.preload(:user) + user = insert(:user) + token1 = insert(:oauth_token, scopes: ["read:statuses", "write"], user: user) - conn = - conn - |> assign(:user, token.user) - |> assign(:token, token) - |> OAuthScopesPlug.call(%{ - scopes: ["read"], - fallback: :proceed_unauthenticated, - skip_instance_privacy_check: true - }) + for token <- [token1, nil], op <- [:|, :&] do + ret_conn = + conn + |> assign(:user, user) + |> assign(:token, token) + |> OAuthScopesPlug.call(%{ + scopes: ["read"], + op: op, + fallback: :proceed_unauthenticated, + skip_instance_privacy_check: true + }) - refute conn.halted - refute conn.assigns[:user] + refute ret_conn.halted + refute ret_conn.assigns[:user] + refute ret_conn.assigns[:token] - refute called(EnsurePublicOrAuthenticatedPlug.call(conn, :_)) + refute called(EnsurePublicOrAuthenticatedPlug.call(ret_conn, :_)) + end end end @@ -140,39 +104,42 @@ test "with :skip_instance_privacy_check option, " <> test "if `token.scopes` does not fulfill specified 'any of' conditions, " <> "returns 403 and halts", %{conn: conn} do - token = insert(:oauth_token, scopes: ["read", "write"]) - any_of_scopes = ["follow"] + for token <- [insert(:oauth_token, scopes: ["read", "write"]), nil] do + any_of_scopes = ["follow", "push"] - conn = - conn - |> assign(:token, token) - |> OAuthScopesPlug.call(%{scopes: any_of_scopes}) + ret_conn = + conn + |> assign(:token, token) + |> OAuthScopesPlug.call(%{scopes: any_of_scopes}) - assert conn.halted - assert 403 == conn.status + assert ret_conn.halted + assert 403 == ret_conn.status - expected_error = "Insufficient permissions: #{Enum.join(any_of_scopes, ", ")}." - assert Jason.encode!(%{error: expected_error}) == conn.resp_body + expected_error = "Insufficient permissions: #{Enum.join(any_of_scopes, " | ")}." + assert Jason.encode!(%{error: expected_error}) == ret_conn.resp_body + end end test "if `token.scopes` does not fulfill specified 'all of' conditions, " <> "returns 403 and halts", %{conn: conn} do - token = insert(:oauth_token, scopes: ["read", "write"]) - all_of_scopes = ["write", "follow"] + for token <- [insert(:oauth_token, scopes: ["read", "write"]), nil] do + token_scopes = (token && token.scopes) || [] + all_of_scopes = ["write", "follow"] - conn = - conn - |> assign(:token, token) - |> OAuthScopesPlug.call(%{scopes: all_of_scopes, op: :&}) + conn = + conn + |> assign(:token, token) + |> OAuthScopesPlug.call(%{scopes: all_of_scopes, op: :&}) - assert conn.halted - assert 403 == conn.status + assert conn.halted + assert 403 == conn.status - expected_error = - "Insufficient permissions: #{Enum.join(all_of_scopes -- token.scopes, ", ")}." + expected_error = + "Insufficient permissions: #{Enum.join(all_of_scopes -- token_scopes, " & ")}." - assert Jason.encode!(%{error: expected_error}) == conn.resp_body + assert Jason.encode!(%{error: expected_error}) == conn.resp_body + end end end diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 9897f72ce..95bc2492a 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -28,6 +28,26 @@ defmodule Pleroma.Web.ConnCase do # The default endpoint for testing @endpoint Pleroma.Web.Endpoint + + # Sets up OAuth access with specified scopes + defp oauth_access(scopes, opts \\ %{}) do + user = + Map.get_lazy(opts, :user, fn -> + Pleroma.Factory.insert(:user) + end) + + token = + Map.get_lazy(opts, :oauth_token, fn -> + Pleroma.Factory.insert(:oauth_token, user: user, scopes: scopes) + end) + + conn = + build_conn() + |> assign(:user, user) + |> assign(:token, token) + + %{user: user, token: token, conn: conn} + end end end diff --git a/test/support/factory.ex b/test/support/factory.ex index 314f26ec9..100864055 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -296,7 +296,7 @@ def oauth_app_factory do %Pleroma.Web.OAuth.App{ client_name: "Some client", redirect_uris: "https://example.com/callback", - scopes: ["read", "write", "follow", "push"], + scopes: ["read", "write", "follow", "push", "admin"], website: "https://example.com", client_id: Ecto.UUID.generate(), client_secret: "aaa;/&bbb" @@ -310,19 +310,37 @@ def instance_factory do } end - def oauth_token_factory do - oauth_app = insert(:oauth_app) + def oauth_token_factory(attrs \\ %{}) do + scopes = Map.get(attrs, :scopes, ["read"]) + oauth_app = Map.get_lazy(attrs, :app, fn -> insert(:oauth_app, scopes: scopes) end) + user = Map.get_lazy(attrs, :user, fn -> build(:user) end) + + valid_until = + Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10)) %Pleroma.Web.OAuth.Token{ token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(), - scopes: ["read"], refresh_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(), - user: build(:user), - app_id: oauth_app.id, - valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10) + scopes: scopes, + user: user, + app: oauth_app, + valid_until: valid_until } end + def oauth_admin_token_factory(attrs \\ %{}) do + user = Map.get_lazy(attrs, :user, fn -> build(:user, is_admin: true) end) + + scopes = + attrs + |> Map.get(:scopes, ["admin"]) + |> Kernel.++(["admin"]) + |> Enum.uniq() + + attrs = Map.merge(attrs, %{user: user, scopes: scopes}) + oauth_token_factory(attrs) + end + def oauth_authorization_factory do %Pleroma.Web.OAuth.Authorization{ token: :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false), diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 49ff005b6..a3fbb6041 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -26,8 +26,16 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do :ok end - clear_config([:auth, :enforce_oauth_admin_scope_usage]) do - Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false) + setup do + admin = insert(:user, is_admin: true) + token = insert(:oauth_admin_token, user: admin) + + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, token) + + {:ok, %{admin: admin, token: token, conn: conn}} end describe "with [:auth, :enforce_oauth_admin_scope_usage]," do @@ -35,9 +43,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], true) end - test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or broader scope" do + test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or broader scope", + %{admin: admin} do user = insert(:user) - admin = insert(:user, is_admin: true) url = "/api/pleroma/admin/users/#{user.nickname}" good_token1 = insert(:oauth_token, user: admin, scopes: ["admin"]) @@ -80,14 +88,67 @@ test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or bro end end + describe "unless [:auth, :enforce_oauth_admin_scope_usage]," do + clear_config([:auth, :enforce_oauth_admin_scope_usage]) do + Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false) + end + + test "GET /api/pleroma/admin/users/:nickname requires " <> + "read:accounts or admin:read:accounts or broader scope", + %{admin: admin} do + user = insert(:user) + url = "/api/pleroma/admin/users/#{user.nickname}" + + good_token1 = insert(:oauth_token, user: admin, scopes: ["admin"]) + good_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read"]) + good_token3 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts"]) + good_token4 = insert(:oauth_token, user: admin, scopes: ["read:accounts"]) + good_token5 = insert(:oauth_token, user: admin, scopes: ["read"]) + + good_tokens = [good_token1, good_token2, good_token3, good_token4, good_token5] + + bad_token1 = insert(:oauth_token, user: admin, scopes: ["read:accounts:partial"]) + bad_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts:partial"]) + bad_token3 = nil + + for good_token <- good_tokens do + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, good_token) + |> get(url) + + assert json_response(conn, 200) + end + + for good_token <- good_tokens do + conn = + build_conn() + |> assign(:user, nil) + |> assign(:token, good_token) + |> get(url) + + assert json_response(conn, :forbidden) + end + + for bad_token <- [bad_token1, bad_token2, bad_token3] do + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, bad_token) + |> get(url) + + assert json_response(conn, :forbidden) + end + end + end + describe "DELETE /api/pleroma/admin/users" do - test "single user" do - admin = insert(:user, is_admin: true) + test "single user", %{admin: admin, conn: conn} do user = insert(:user) conn = - build_conn() - |> assign(:user, admin) + conn |> put_req_header("accept", "application/json") |> delete("/api/pleroma/admin/users?nickname=#{user.nickname}") @@ -99,14 +160,12 @@ test "single user" do assert json_response(conn, 200) == user.nickname end - test "multiple users" do - admin = insert(:user, is_admin: true) + test "multiple users", %{admin: admin, conn: conn} do user_one = insert(:user) user_two = insert(:user) conn = - build_conn() - |> assign(:user, admin) + conn |> put_req_header("accept", "application/json") |> delete("/api/pleroma/admin/users", %{ nicknames: [user_one.nickname, user_two.nickname] @@ -123,12 +182,9 @@ test "multiple users" do end describe "/api/pleroma/admin/users" do - test "Create" do - admin = insert(:user, is_admin: true) - + test "Create", %{conn: conn} do conn = - build_conn() - |> assign(:user, admin) + conn |> put_req_header("accept", "application/json") |> post("/api/pleroma/admin/users", %{ "users" => [ @@ -153,13 +209,11 @@ test "Create" do assert ["lain", "lain2"] -- Enum.map(log_entry.data["subjects"], & &1["nickname"]) == [] end - test "Cannot create user with existing email" do - admin = insert(:user, is_admin: true) + test "Cannot create user with existing email", %{conn: conn} do user = insert(:user) conn = - build_conn() - |> assign(:user, admin) + conn |> put_req_header("accept", "application/json") |> post("/api/pleroma/admin/users", %{ "users" => [ @@ -184,13 +238,11 @@ test "Cannot create user with existing email" do ] end - test "Cannot create user with existing nickname" do - admin = insert(:user, is_admin: true) + test "Cannot create user with existing nickname", %{conn: conn} do user = insert(:user) conn = - build_conn() - |> assign(:user, admin) + conn |> put_req_header("accept", "application/json") |> post("/api/pleroma/admin/users", %{ "users" => [ @@ -215,13 +267,11 @@ test "Cannot create user with existing nickname" do ] end - test "Multiple user creation works in transaction" do - admin = insert(:user, is_admin: true) + test "Multiple user creation works in transaction", %{conn: conn} do user = insert(:user) conn = - build_conn() - |> assign(:user, admin) + conn |> put_req_header("accept", "application/json") |> post("/api/pleroma/admin/users", %{ "users" => [ @@ -265,13 +315,9 @@ test "Multiple user creation works in transaction" do describe "/api/pleroma/admin/users/:nickname" do test "Show", %{conn: conn} do - admin = insert(:user, is_admin: true) user = insert(:user) - conn = - conn - |> assign(:user, admin) - |> get("/api/pleroma/admin/users/#{user.nickname}") + conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}") expected = %{ "deactivated" => false, @@ -289,26 +335,20 @@ test "Show", %{conn: conn} do end test "when the user doesn't exist", %{conn: conn} do - admin = insert(:user, is_admin: true) user = build(:user) - conn = - conn - |> assign(:user, admin) - |> get("/api/pleroma/admin/users/#{user.nickname}") + conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}") assert "Not found" == json_response(conn, 404) end end describe "/api/pleroma/admin/users/follow" do - test "allows to force-follow another user" do - admin = insert(:user, is_admin: true) + test "allows to force-follow another user", %{admin: admin, conn: conn} do user = insert(:user) follower = insert(:user) - build_conn() - |> assign(:user, admin) + conn |> put_req_header("accept", "application/json") |> post("/api/pleroma/admin/users/follow", %{ "follower" => follower.nickname, @@ -328,15 +368,13 @@ test "allows to force-follow another user" do end describe "/api/pleroma/admin/users/unfollow" do - test "allows to force-unfollow another user" do - admin = insert(:user, is_admin: true) + test "allows to force-unfollow another user", %{admin: admin, conn: conn} do user = insert(:user) follower = insert(:user) User.follow(follower, user) - build_conn() - |> assign(:user, admin) + conn |> put_req_header("accept", "application/json") |> post("/api/pleroma/admin/users/unfollow", %{ "follower" => follower.nickname, @@ -356,23 +394,20 @@ test "allows to force-unfollow another user" do end describe "PUT /api/pleroma/admin/users/tag" do - setup do - admin = insert(:user, is_admin: true) + setup %{conn: conn} do user1 = insert(:user, %{tags: ["x"]}) user2 = insert(:user, %{tags: ["y"]}) user3 = insert(:user, %{tags: ["unchanged"]}) conn = - build_conn() - |> assign(:user, admin) + conn |> put_req_header("accept", "application/json") |> put( - "/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=#{ - user2.nickname - }&tags[]=foo&tags[]=bar" + "/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=" <> + "#{user2.nickname}&tags[]=foo&tags[]=bar" ) - %{conn: conn, admin: admin, user1: user1, user2: user2, user3: user3} + %{conn: conn, user1: user1, user2: user2, user3: user3} end test "it appends specified tags to users with specified nicknames", %{ @@ -405,23 +440,20 @@ test "it does not modify tags of not specified users", %{conn: conn, user3: user end describe "DELETE /api/pleroma/admin/users/tag" do - setup do - admin = insert(:user, is_admin: true) + setup %{conn: conn} do user1 = insert(:user, %{tags: ["x"]}) user2 = insert(:user, %{tags: ["y", "z"]}) user3 = insert(:user, %{tags: ["unchanged"]}) conn = - build_conn() - |> assign(:user, admin) + conn |> put_req_header("accept", "application/json") |> delete( - "/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=#{ - user2.nickname - }&tags[]=x&tags[]=z" + "/api/pleroma/admin/users/tag?nicknames[]=#{user1.nickname}&nicknames[]=" <> + "#{user2.nickname}&tags[]=x&tags[]=z" ) - %{conn: conn, admin: admin, user1: user1, user2: user2, user3: user3} + %{conn: conn, user1: user1, user2: user2, user3: user3} end test "it removes specified tags from users with specified nicknames", %{ @@ -454,12 +486,9 @@ test "it does not modify tags of not specified users", %{conn: conn, user3: user end describe "/api/pleroma/admin/users/:nickname/permission_group" do - test "GET is giving user_info" do - admin = insert(:user, is_admin: true) - + test "GET is giving user_info", %{admin: admin, conn: conn} do conn = - build_conn() - |> assign(:user, admin) + conn |> put_req_header("accept", "application/json") |> get("/api/pleroma/admin/users/#{admin.nickname}/permission_group/") @@ -469,13 +498,11 @@ test "GET is giving user_info" do } end - test "/:right POST, can add to a permission group" do - admin = insert(:user, is_admin: true) + test "/:right POST, can add to a permission group", %{admin: admin, conn: conn} do user = insert(:user) conn = - build_conn() - |> assign(:user, admin) + conn |> put_req_header("accept", "application/json") |> post("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin") @@ -489,22 +516,18 @@ test "/:right POST, can add to a permission group" do "@#{admin.nickname} made @#{user.nickname} admin" end - test "/:right POST, can add to a permission group (multiple)" do - admin = insert(:user, is_admin: true) + test "/:right POST, can add to a permission group (multiple)", %{admin: admin, conn: conn} do user_one = insert(:user) user_two = insert(:user) conn = - build_conn() - |> assign(:user, admin) + conn |> put_req_header("accept", "application/json") |> post("/api/pleroma/admin/users/permission_group/admin", %{ nicknames: [user_one.nickname, user_two.nickname] }) - assert json_response(conn, 200) == %{ - "is_admin" => true - } + assert json_response(conn, 200) == %{"is_admin" => true} log_entry = Repo.one(ModerationLog) @@ -512,19 +535,15 @@ test "/:right POST, can add to a permission group (multiple)" do "@#{admin.nickname} made @#{user_one.nickname}, @#{user_two.nickname} admin" end - test "/:right DELETE, can remove from a permission group" do - admin = insert(:user, is_admin: true) + test "/:right DELETE, can remove from a permission group", %{admin: admin, conn: conn} do user = insert(:user, is_admin: true) conn = - build_conn() - |> assign(:user, admin) + conn |> put_req_header("accept", "application/json") |> delete("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin") - assert json_response(conn, 200) == %{ - "is_admin" => false - } + assert json_response(conn, 200) == %{"is_admin" => false} log_entry = Repo.one(ModerationLog) @@ -532,22 +551,21 @@ test "/:right DELETE, can remove from a permission group" do "@#{admin.nickname} revoked admin role from @#{user.nickname}" end - test "/:right DELETE, can remove from a permission group (multiple)" do - admin = insert(:user, is_admin: true) + test "/:right DELETE, can remove from a permission group (multiple)", %{ + admin: admin, + conn: conn + } do user_one = insert(:user, is_admin: true) user_two = insert(:user, is_admin: true) conn = - build_conn() - |> assign(:user, admin) + conn |> put_req_header("accept", "application/json") |> delete("/api/pleroma/admin/users/permission_group/admin", %{ nicknames: [user_one.nickname, user_two.nickname] }) - assert json_response(conn, 200) == %{ - "is_admin" => false - } + assert json_response(conn, 200) == %{"is_admin" => false} log_entry = Repo.one(ModerationLog) @@ -559,10 +577,6 @@ test "/:right DELETE, can remove from a permission group (multiple)" do end describe "POST /api/pleroma/admin/email_invite, with valid config" do - setup do - [user: insert(:user, is_admin: true)] - end - clear_config([:instance, :registrations_open]) do Pleroma.Config.put([:instance, :registrations_open], false) end @@ -571,14 +585,13 @@ test "/:right DELETE, can remove from a permission group (multiple)" do Pleroma.Config.put([:instance, :invites_enabled], true) end - test "sends invitation and returns 204", %{conn: conn, user: user} do + test "sends invitation and returns 204", %{admin: admin, conn: conn} do recipient_email = "foo@bar.com" recipient_name = "J. D." conn = - conn - |> assign(:user, user) - |> post( + post( + conn, "/api/pleroma/admin/users/email_invite?email=#{recipient_email}&name=#{recipient_name}" ) @@ -593,7 +606,7 @@ test "sends invitation and returns 204", %{conn: conn, user: user} do email = Pleroma.Emails.UserEmail.user_invitation_email( - user, + admin, token_record, recipient_email, recipient_name @@ -606,12 +619,14 @@ test "sends invitation and returns 204", %{conn: conn, user: user} do ) end - test "it returns 403 if requested by a non-admin", %{conn: conn} do + test "it returns 403 if requested by a non-admin" do non_admin_user = insert(:user) + token = insert(:oauth_token, user: non_admin_user) conn = - conn + build_conn() |> assign(:user, non_admin_user) + |> assign(:token, token) |> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD") assert json_response(conn, :forbidden) @@ -619,45 +634,33 @@ test "it returns 403 if requested by a non-admin", %{conn: conn} do end describe "POST /api/pleroma/admin/users/email_invite, with invalid config" do - setup do - [user: insert(:user, is_admin: true)] - end - clear_config([:instance, :registrations_open]) clear_config([:instance, :invites_enabled]) - test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn, user: user} do + test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn} do Pleroma.Config.put([:instance, :registrations_open], false) Pleroma.Config.put([:instance, :invites_enabled], false) - conn = - conn - |> assign(:user, user) - |> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD") + conn = post(conn, "/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD") assert json_response(conn, :internal_server_error) end - test "it returns 500 if `registrations_open` is enabled", %{conn: conn, user: user} do + test "it returns 500 if `registrations_open` is enabled", %{conn: conn} do Pleroma.Config.put([:instance, :registrations_open], true) Pleroma.Config.put([:instance, :invites_enabled], true) - conn = - conn - |> assign(:user, user) - |> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD") + conn = post(conn, "/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD") assert json_response(conn, :internal_server_error) end end - test "/api/pleroma/admin/users/:nickname/password_reset" do - admin = insert(:user, is_admin: true) + test "/api/pleroma/admin/users/:nickname/password_reset", %{conn: conn} do user = insert(:user) conn = - build_conn() - |> assign(:user, admin) + conn |> put_req_header("accept", "application/json") |> get("/api/pleroma/admin/users/#{user.nickname}/password_reset") @@ -667,16 +670,6 @@ test "/api/pleroma/admin/users/:nickname/password_reset" do end describe "GET /api/pleroma/admin/users" do - setup do - admin = insert(:user, is_admin: true) - - conn = - build_conn() - |> assign(:user, admin) - - {:ok, conn: conn, admin: admin} - end - test "renders users array for the first page", %{conn: conn, admin: admin} do user = insert(:user, local: false, tags: ["foo", "bar"]) conn = get(conn, "/api/pleroma/admin/users?page=1") @@ -898,6 +891,7 @@ test "regular search with page size", %{conn: conn} do test "only local users" do admin = insert(:user, is_admin: true, nickname: "john") + token = insert(:oauth_admin_token, user: admin) user = insert(:user, nickname: "bob") insert(:user, nickname: "bobb", local: false) @@ -905,6 +899,7 @@ test "only local users" do conn = build_conn() |> assign(:user, admin) + |> assign(:token, token) |> get("/api/pleroma/admin/users?query=bo&filters=local") assert json_response(conn, 200) == %{ @@ -926,16 +921,13 @@ test "only local users" do } end - test "only local users with no query", %{admin: old_admin} do + test "only local users with no query", %{conn: conn, admin: old_admin} do admin = insert(:user, is_admin: true, nickname: "john") user = insert(:user, nickname: "bob") insert(:user, nickname: "bobb", local: false) - conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/users?filters=local") + conn = get(conn, "/api/pleroma/admin/users?filters=local") users = [ @@ -1093,6 +1085,7 @@ test "load users with tags list", %{conn: conn} do test "it works with multiple filters" do admin = insert(:user, nickname: "john", is_admin: true) + token = insert(:oauth_admin_token, user: admin) user = insert(:user, nickname: "bob", local: false, deactivated: true) insert(:user, nickname: "ken", local: true, deactivated: true) @@ -1101,6 +1094,7 @@ test "it works with multiple filters" do conn = build_conn() |> assign(:user, admin) + |> assign(:token, token) |> get("/api/pleroma/admin/users?filters=deactivated,external") assert json_response(conn, 200) == %{ @@ -1122,13 +1116,10 @@ test "it works with multiple filters" do } end - test "it omits relay user", %{admin: admin} do + test "it omits relay user", %{admin: admin, conn: conn} do assert %User{} = Relay.get_actor() - conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/users") + conn = get(conn, "/api/pleroma/admin/users") assert json_response(conn, 200) == %{ "count" => 1, @@ -1150,15 +1141,13 @@ test "it omits relay user", %{admin: admin} do end end - test "PATCH /api/pleroma/admin/users/activate" do - admin = insert(:user, is_admin: true) + test "PATCH /api/pleroma/admin/users/activate", %{admin: admin, conn: conn} do user_one = insert(:user, deactivated: true) user_two = insert(:user, deactivated: true) conn = - build_conn() - |> assign(:user, admin) - |> patch( + patch( + conn, "/api/pleroma/admin/users/activate", %{nicknames: [user_one.nickname, user_two.nickname]} ) @@ -1172,15 +1161,13 @@ test "PATCH /api/pleroma/admin/users/activate" do "@#{admin.nickname} activated users: @#{user_one.nickname}, @#{user_two.nickname}" end - test "PATCH /api/pleroma/admin/users/deactivate" do - admin = insert(:user, is_admin: true) + test "PATCH /api/pleroma/admin/users/deactivate", %{admin: admin, conn: conn} do user_one = insert(:user, deactivated: false) user_two = insert(:user, deactivated: false) conn = - build_conn() - |> assign(:user, admin) - |> patch( + patch( + conn, "/api/pleroma/admin/users/deactivate", %{nicknames: [user_one.nickname, user_two.nickname]} ) @@ -1194,14 +1181,10 @@ test "PATCH /api/pleroma/admin/users/deactivate" do "@#{admin.nickname} deactivated users: @#{user_one.nickname}, @#{user_two.nickname}" end - test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation" do - admin = insert(:user, is_admin: true) + test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation", %{admin: admin, conn: conn} do user = insert(:user) - conn = - build_conn() - |> assign(:user, admin) - |> patch("/api/pleroma/admin/users/#{user.nickname}/toggle_activation") + conn = patch(conn, "/api/pleroma/admin/users/#{user.nickname}/toggle_activation") assert json_response(conn, 200) == %{ @@ -1223,16 +1206,6 @@ test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation" do end describe "POST /api/pleroma/admin/users/invite_token" do - setup do - admin = insert(:user, is_admin: true) - - conn = - build_conn() - |> assign(:user, admin) - - {:ok, conn: conn} - end - test "without options", %{conn: conn} do conn = post(conn, "/api/pleroma/admin/users/invite_token") @@ -1287,16 +1260,6 @@ test "with max use and expires_at", %{conn: conn} do end describe "GET /api/pleroma/admin/users/invites" do - setup do - admin = insert(:user, is_admin: true) - - conn = - build_conn() - |> assign(:user, admin) - - {:ok, conn: conn} - end - test "no invites", %{conn: conn} do conn = get(conn, "/api/pleroma/admin/users/invites") @@ -1325,14 +1288,10 @@ test "with invite", %{conn: conn} do end describe "POST /api/pleroma/admin/users/revoke_invite" do - test "with token" do - admin = insert(:user, is_admin: true) + test "with token", %{conn: conn} do {:ok, invite} = UserInviteToken.create_invite() - conn = - build_conn() - |> assign(:user, admin) - |> post("/api/pleroma/admin/users/revoke_invite", %{"token" => invite.token}) + conn = post(conn, "/api/pleroma/admin/users/revoke_invite", %{"token" => invite.token}) assert json_response(conn, 200) == %{ "expires_at" => nil, @@ -1345,25 +1304,14 @@ test "with token" do } end - test "with invalid token" do - admin = insert(:user, is_admin: true) - - conn = - build_conn() - |> assign(:user, admin) - |> post("/api/pleroma/admin/users/revoke_invite", %{"token" => "foo"}) + test "with invalid token", %{conn: conn} do + conn = post(conn, "/api/pleroma/admin/users/revoke_invite", %{"token" => "foo"}) assert json_response(conn, :not_found) == "Not found" end end describe "GET /api/pleroma/admin/reports/:id" do - setup %{conn: conn} do - admin = insert(:user, is_admin: true) - - %{conn: assign(conn, :user, admin)} - end - test "returns report by its id", %{conn: conn} do [reporter, target_user] = insert_pair(:user) activity = insert(:note_activity, user: target_user) @@ -1391,8 +1339,7 @@ test "returns 404 when report id is invalid", %{conn: conn} do end describe "PATCH /api/pleroma/admin/reports" do - setup %{conn: conn} do - admin = insert(:user, is_admin: true) + setup do [reporter, target_user] = insert_pair(:user) activity = insert(:note_activity, user: target_user) @@ -1411,9 +1358,7 @@ test "returns 404 when report id is invalid", %{conn: conn} do }) %{ - conn: assign(conn, :user, admin), id: report_id, - admin: admin, second_report_id: second_report_id } end @@ -1509,12 +1454,6 @@ test "updates state of multiple reports", %{ end describe "GET /api/pleroma/admin/reports" do - setup %{conn: conn} do - admin = insert(:user, is_admin: true) - - %{conn: assign(conn, :user, admin)} - end - test "returns empty response when no reports created", %{conn: conn} do response = conn @@ -1609,10 +1548,12 @@ test "returns reports with specified state", %{conn: conn} do test "returns 403 when requested by a non-admin" do user = insert(:user) + token = insert(:oauth_token, user: user) conn = build_conn() |> assign(:user, user) + |> assign(:token, token) |> get("/api/pleroma/admin/reports") assert json_response(conn, :forbidden) == @@ -1620,17 +1561,14 @@ test "returns 403 when requested by a non-admin" do end test "returns 403 when requested by anonymous" do - conn = - build_conn() - |> get("/api/pleroma/admin/reports") + conn = get(build_conn(), "/api/pleroma/admin/reports") assert json_response(conn, :forbidden) == %{"error" => "Invalid credentials."} end end describe "GET /api/pleroma/admin/grouped_reports" do - setup %{conn: conn} do - admin = insert(:user, is_admin: true) + setup do [reporter, target_user] = insert_pair(:user) date1 = (DateTime.to_unix(DateTime.utc_now()) + 1000) |> DateTime.from_unix!() @@ -1665,7 +1603,6 @@ test "returns 403 when requested by anonymous" do }) %{ - conn: assign(conn, :user, admin), first_status: Activity.get_by_ap_id_with_object(first_status.data["id"]), second_status: Activity.get_by_ap_id_with_object(second_status.data["id"]), third_status: Activity.get_by_ap_id_with_object(third_status.data["id"]), @@ -1833,11 +1770,10 @@ test "account not empty if status was deleted", %{ end describe "PUT /api/pleroma/admin/statuses/:id" do - setup %{conn: conn} do - admin = insert(:user, is_admin: true) + setup do activity = insert(:note_activity) - %{conn: assign(conn, :user, admin), id: activity.id, admin: admin} + %{id: activity.id} end test "toggle sensitive flag", %{conn: conn, id: id, admin: admin} do @@ -1890,20 +1826,17 @@ test "change visibility flag", %{conn: conn, id: id, admin: admin} do end test "returns 400 when visibility is unknown", %{conn: conn, id: id} do - conn = - conn - |> put("/api/pleroma/admin/statuses/#{id}", %{"visibility" => "test"}) + conn = put(conn, "/api/pleroma/admin/statuses/#{id}", %{"visibility" => "test"}) assert json_response(conn, :bad_request) == "Unsupported visibility" end end describe "DELETE /api/pleroma/admin/statuses/:id" do - setup %{conn: conn} do - admin = insert(:user, is_admin: true) + setup do activity = insert(:note_activity) - %{conn: assign(conn, :user, admin), id: activity.id, admin: admin} + %{id: activity.id} end test "deletes status", %{conn: conn, id: id, admin: admin} do @@ -1920,21 +1853,13 @@ test "deletes status", %{conn: conn, id: id, admin: admin} do end test "returns error when status is not exist", %{conn: conn} do - conn = - conn - |> delete("/api/pleroma/admin/statuses/test") + conn = delete(conn, "/api/pleroma/admin/statuses/test") assert json_response(conn, :bad_request) == "Could not delete" end end describe "GET /api/pleroma/admin/config" do - setup %{conn: conn} do - admin = insert(:user, is_admin: true) - - %{conn: assign(conn, :user, admin)} - end - test "without any settings in db", %{conn: conn} do conn = get(conn, "/api/pleroma/admin/config") @@ -1966,9 +1891,7 @@ test "with settings in db", %{conn: conn} do end describe "POST /api/pleroma/admin/config" do - setup %{conn: conn} do - admin = insert(:user, is_admin: true) - + setup do temp_file = "config/test.exported_from_db.secret.exs" on_exit(fn -> @@ -1982,8 +1905,6 @@ test "with settings in db", %{conn: conn} do Application.delete_env(:pleroma, Pleroma.Captcha.NotReal) :ok = File.rm(temp_file) end) - - %{conn: assign(conn, :user, admin)} end clear_config([:instance, :dynamic_configuration]) do @@ -2535,9 +2456,7 @@ test "delete part of settings by atom subkeys", %{conn: conn} do end describe "config mix tasks run" do - setup %{conn: conn} do - admin = insert(:user, is_admin: true) - + setup do temp_file = "config/test.exported_from_db.secret.exs" Mix.shell(Mix.Shell.Quiet) @@ -2547,7 +2466,7 @@ test "delete part of settings by atom subkeys", %{conn: conn} do :ok = File.rm(temp_file) end) - %{conn: assign(conn, :user, admin), admin: admin} + :ok end clear_config([:instance, :dynamic_configuration]) do @@ -2558,25 +2477,21 @@ test "delete part of settings by atom subkeys", %{conn: conn} do Pleroma.Config.put([:feed, :post_title], %{max_length: 100, omission: "…"}) end - test "transfer settings to DB and to file", %{conn: conn, admin: admin} do + test "transfer settings to DB and to file", %{conn: conn} do assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) == [] - conn = get(conn, "/api/pleroma/admin/config/migrate_to_db") - assert json_response(conn, 200) == %{} + ret_conn = get(conn, "/api/pleroma/admin/config/migrate_to_db") + assert json_response(ret_conn, 200) == %{} assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) > 0 - conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/config/migrate_from_db") + ret_conn = get(conn, "/api/pleroma/admin/config/migrate_from_db") - assert json_response(conn, 200) == %{} + assert json_response(ret_conn, 200) == %{} assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) == [] end end describe "GET /api/pleroma/admin/users/:nickname/statuses" do setup do - admin = insert(:user, is_admin: true) user = insert(:user) date1 = (DateTime.to_unix(DateTime.utc_now()) + 2000) |> DateTime.from_unix!() @@ -2587,11 +2502,7 @@ test "transfer settings to DB and to file", %{conn: conn, admin: admin} do insert(:note_activity, user: user, published: date2) insert(:note_activity, user: user, published: date3) - conn = - build_conn() - |> assign(:user, admin) - - {:ok, conn: conn, user: user} + %{user: user} end test "renders user's statuses", %{conn: conn, user: user} do @@ -2632,11 +2543,10 @@ test "returns private statuses with godmode on", %{conn: conn, user: user} do end describe "GET /api/pleroma/admin/moderation_log" do - setup %{conn: conn} do - admin = insert(:user, is_admin: true) + setup do moderator = insert(:user, is_moderator: true) - %{conn: assign(conn, :user, admin), admin: admin, moderator: moderator} + %{moderator: moderator} end test "returns the log", %{conn: conn, admin: admin} do @@ -2841,20 +2751,12 @@ test "returns log filtered by search", %{conn: conn, moderator: moderator} do end describe "PATCH /users/:nickname/force_password_reset" do - setup %{conn: conn} do - admin = insert(:user, is_admin: true) + test "sets password_reset_pending to true", %{conn: conn} do user = insert(:user) - - %{conn: assign(conn, :user, admin), admin: admin, user: user} - end - - test "sets password_reset_pending to true", %{admin: admin, user: user} do assert user.password_reset_pending == false conn = - build_conn() - |> assign(:user, admin) - |> patch("/api/pleroma/admin/users/force_password_reset", %{nicknames: [user.nickname]}) + patch(conn, "/api/pleroma/admin/users/force_password_reset", %{nicknames: [user.nickname]}) assert json_response(conn, 204) == "" @@ -2865,17 +2767,9 @@ test "sets password_reset_pending to true", %{admin: admin, user: user} do end describe "relays" do - setup %{conn: conn} do - admin = insert(:user, is_admin: true) - - %{conn: assign(conn, :user, admin), admin: admin} - end - - test "POST /relay", %{admin: admin} do + test "POST /relay", %{conn: conn, admin: admin} do conn = - build_conn() - |> assign(:user, admin) - |> post("/api/pleroma/admin/relay", %{ + post(conn, "/api/pleroma/admin/relay", %{ relay_url: "http://mastodon.example.org/users/admin" }) @@ -2887,7 +2781,7 @@ test "POST /relay", %{admin: admin} do "@#{admin.nickname} followed relay: http://mastodon.example.org/users/admin" end - test "GET /relay", %{admin: admin} do + test "GET /relay", %{conn: conn} do relay_user = Pleroma.Web.ActivityPub.Relay.get_actor() ["http://mastodon.example.org/users/admin", "https://mstdn.io/users/mayuutann"] @@ -2896,25 +2790,18 @@ test "GET /relay", %{admin: admin} do User.follow(relay_user, user) end) - conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/relay") + conn = get(conn, "/api/pleroma/admin/relay") assert json_response(conn, 200)["relays"] -- ["mastodon.example.org", "mstdn.io"] == [] end - test "DELETE /relay", %{admin: admin} do - build_conn() - |> assign(:user, admin) - |> post("/api/pleroma/admin/relay", %{ + test "DELETE /relay", %{conn: conn, admin: admin} do + post(conn, "/api/pleroma/admin/relay", %{ relay_url: "http://mastodon.example.org/users/admin" }) conn = - build_conn() - |> assign(:user, admin) - |> delete("/api/pleroma/admin/relay", %{ + delete(conn, "/api/pleroma/admin/relay", %{ relay_url: "http://mastodon.example.org/users/admin" }) @@ -2931,63 +2818,48 @@ test "DELETE /relay", %{admin: admin} do end describe "instances" do - test "GET /instances/:instance/statuses" do - admin = insert(:user, is_admin: true) + test "GET /instances/:instance/statuses", %{conn: conn} do user = insert(:user, local: false, nickname: "archaeme@archae.me") user2 = insert(:user, local: false, nickname: "test@test.com") insert_pair(:note_activity, user: user) insert(:note_activity, user: user2) - conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/instances/archae.me/statuses") + ret_conn = get(conn, "/api/pleroma/admin/instances/archae.me/statuses") - response = json_response(conn, 200) + response = json_response(ret_conn, 200) assert length(response) == 2 - conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/instances/test.com/statuses") + ret_conn = get(conn, "/api/pleroma/admin/instances/test.com/statuses") - response = json_response(conn, 200) + response = json_response(ret_conn, 200) assert length(response) == 1 - conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/instances/nonexistent.com/statuses") + ret_conn = get(conn, "/api/pleroma/admin/instances/nonexistent.com/statuses") - response = json_response(conn, 200) + response = json_response(ret_conn, 200) assert length(response) == 0 end end describe "PATCH /confirm_email" do - setup %{conn: conn} do - admin = insert(:user, is_admin: true) - - %{conn: assign(conn, :user, admin), admin: admin} - end - - test "it confirms emails of two users", %{admin: admin} do + test "it confirms emails of two users", %{conn: conn, admin: admin} do [first_user, second_user] = insert_pair(:user, confirmation_pending: true) assert first_user.confirmation_pending == true assert second_user.confirmation_pending == true - build_conn() - |> assign(:user, admin) - |> patch("/api/pleroma/admin/users/confirm_email", %{ - nicknames: [ - first_user.nickname, - second_user.nickname - ] - }) + ret_conn = + patch(conn, "/api/pleroma/admin/users/confirm_email", %{ + nicknames: [ + first_user.nickname, + second_user.nickname + ] + }) + + assert ret_conn.status == 200 assert first_user.confirmation_pending == true assert second_user.confirmation_pending == true @@ -3002,23 +2874,18 @@ test "it confirms emails of two users", %{admin: admin} do end describe "PATCH /resend_confirmation_email" do - setup %{conn: conn} do - admin = insert(:user, is_admin: true) - - %{conn: assign(conn, :user, admin), admin: admin} - end - - test "it resend emails for two users", %{admin: admin} do + test "it resend emails for two users", %{conn: conn, admin: admin} do [first_user, second_user] = insert_pair(:user, confirmation_pending: true) - build_conn() - |> assign(:user, admin) - |> patch("/api/pleroma/admin/users/resend_confirmation_email", %{ - nicknames: [ - first_user.nickname, - second_user.nickname - ] - }) + ret_conn = + patch(conn, "/api/pleroma/admin/users/resend_confirmation_email", %{ + nicknames: [ + first_user.nickname, + second_user.nickname + ] + }) + + assert ret_conn.status == 200 log_entry = Repo.one(ModerationLog) @@ -3030,8 +2897,7 @@ test "it resend emails for two users", %{admin: admin} do end describe "POST /reports/:id/notes" do - setup do - admin = insert(:user, is_admin: true) + setup %{conn: conn, admin: admin} do [reporter, target_user] = insert_pair(:user) activity = insert(:note_activity, user: target_user) @@ -3042,22 +2908,17 @@ test "it resend emails for two users", %{admin: admin} do "status_ids" => [activity.id] }) - build_conn() - |> assign(:user, admin) - |> post("/api/pleroma/admin/reports/#{report_id}/notes", %{ + post(conn, "/api/pleroma/admin/reports/#{report_id}/notes", %{ content: "this is disgusting!" }) - build_conn() - |> assign(:user, admin) - |> post("/api/pleroma/admin/reports/#{report_id}/notes", %{ + post(conn, "/api/pleroma/admin/reports/#{report_id}/notes", %{ content: "this is disgusting2!" }) %{ admin_id: admin.id, - report_id: report_id, - admin: admin + report_id: report_id } end @@ -3071,11 +2932,8 @@ test "it creates report note", %{admin_id: admin_id, report_id: report_id} do } = note end - test "it returns reports with notes", %{admin: admin} do - conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/reports") + test "it returns reports with notes", %{conn: conn, admin: admin} do + conn = get(conn, "/api/pleroma/admin/reports") response = json_response(conn, 200) notes = hd(response["reports"])["notes"] @@ -3087,14 +2945,12 @@ test "it returns reports with notes", %{admin: admin} do assert response["total"] == 1 end - test "it deletes the note", %{admin: admin, report_id: report_id} do + test "it deletes the note", %{conn: conn, report_id: report_id} do assert ReportNote |> Repo.all() |> length() == 2 [note, _] = Repo.all(ReportNote) - build_conn() - |> assign(:user, admin) - |> delete("/api/pleroma/admin/reports/#{report_id}/notes/#{note.id}") + delete(conn, "/api/pleroma/admin/reports/#{report_id}/notes/#{note.id}") assert ReportNote |> Repo.all() |> length() == 1 end diff --git a/test/web/mastodon_api/controllers/status_controller_test.exs b/test/web/mastodon_api/controllers/status_controller_test.exs index 5fbe947ba..307221c5d 100644 --- a/test/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/web/mastodon_api/controllers/status_controller_test.exs @@ -23,24 +23,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do clear_config([:instance, :allow_relay]) describe "posting statuses" do - setup do - user = insert(:user) - - conn = - build_conn() - |> assign(:user, user) - - [conn: conn] - end + setup do: oauth_access(["write:statuses"]) test "posting a status does not increment reblog_count when relaying", %{conn: conn} do Pleroma.Config.put([:instance, :federating], true) Pleroma.Config.get([:instance, :allow_relay], true) - user = insert(:user) response = conn - |> assign(:user, user) |> post("api/v1/statuses", %{ "content_type" => "text/plain", "source" => "Pleroma FE", @@ -54,7 +44,6 @@ test "posting a status does not increment reblog_count when relaying", %{conn: c response = conn - |> assign(:user, user) |> get("api/v1/statuses/#{response["id"]}", %{}) |> json_response(200) @@ -132,9 +121,7 @@ test "posting a status", %{conn: conn} do NaiveDateTime.to_iso8601(expiration.scheduled_at) end - test "posting an undefined status with an attachment", %{conn: conn} do - user = insert(:user) - + test "posting an undefined status with an attachment", %{user: user, conn: conn} do file = %Plug.Upload{ content_type: "image/jpg", path: Path.absname("test/fixtures/image.jpg"), @@ -144,17 +131,14 @@ test "posting an undefined status with an attachment", %{conn: conn} do {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) conn = - conn - |> assign(:user, user) - |> post("/api/v1/statuses", %{ + post(conn, "/api/v1/statuses", %{ "media_ids" => [to_string(upload.id)] }) assert json_response(conn, 200) end - test "replying to a status", %{conn: conn} do - user = insert(:user) + test "replying to a status", %{user: user, conn: conn} do {:ok, replied_to} = CommonAPI.post(user, %{"status" => "cofe"}) conn = @@ -169,8 +153,10 @@ test "replying to a status", %{conn: conn} do assert Activity.get_in_reply_to_activity(activity).id == replied_to.id end - test "replying to a direct message with visibility other than direct", %{conn: conn} do - user = insert(:user) + test "replying to a direct message with visibility other than direct", %{ + user: user, + conn: conn + } do {:ok, replied_to} = CommonAPI.post(user, %{"status" => "suya..", "visibility" => "direct"}) Enum.each(["public", "private", "unlisted"], fn visibility -> @@ -187,18 +173,14 @@ test "replying to a direct message with visibility other than direct", %{conn: c end test "posting a status with an invalid in_reply_to_id", %{conn: conn} do - conn = - conn - |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => ""}) + conn = post(conn, "/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => ""}) assert %{"content" => "xD", "id" => id} = json_response(conn, 200) assert Activity.get_by_id(id) end test "posting a sensitive status", %{conn: conn} do - conn = - conn - |> post("/api/v1/statuses", %{"status" => "cofe", "sensitive" => true}) + conn = post(conn, "/api/v1/statuses", %{"status" => "cofe", "sensitive" => true}) assert %{"content" => "cofe", "id" => id, "sensitive" => true} = json_response(conn, 200) assert Activity.get_by_id(id) @@ -206,8 +188,7 @@ test "posting a sensitive status", %{conn: conn} do test "posting a fake status", %{conn: conn} do real_conn = - conn - |> post("/api/v1/statuses", %{ + post(conn, "/api/v1/statuses", %{ "status" => "\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it" }) @@ -226,8 +207,7 @@ test "posting a fake status", %{conn: conn} do |> Kernel.put_in(["pleroma", "conversation_id"], nil) fake_conn = - conn - |> post("/api/v1/statuses", %{ + post(conn, "/api/v1/statuses", %{ "status" => "\"Tenshi Eating a Corndog\" is a much discussed concept on /jp/. The significance of it is disputed, so I will focus on one core concept: the symbolism behind it", "preview" => true @@ -254,8 +234,7 @@ test "posting a status with OGP link preview", %{conn: conn} do Config.put([:rich_media, :enabled], true) conn = - conn - |> post("/api/v1/statuses", %{ + post(conn, "/api/v1/statuses", %{ "status" => "https://example.com/ogp" }) @@ -267,9 +246,7 @@ test "posting a direct status", %{conn: conn} do user2 = insert(:user) content = "direct cofe @#{user2.nickname}" - conn = - conn - |> post("api/v1/statuses", %{"status" => content, "visibility" => "direct"}) + conn = post(conn, "api/v1/statuses", %{"status" => content, "visibility" => "direct"}) assert %{"id" => id} = response = json_response(conn, 200) assert response["visibility"] == "direct" @@ -282,14 +259,13 @@ test "posting a direct status", %{conn: conn} do end describe "posting scheduled statuses" do + setup do: oauth_access(["write:statuses"]) + test "creates a scheduled activity", %{conn: conn} do - user = insert(:user) scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond) conn = - conn - |> assign(:user, user) - |> post("/api/v1/statuses", %{ + post(conn, "/api/v1/statuses", %{ "status" => "scheduled", "scheduled_at" => scheduled_at }) @@ -299,8 +275,7 @@ test "creates a scheduled activity", %{conn: conn} do assert [] == Repo.all(Activity) end - test "creates a scheduled activity with a media attachment", %{conn: conn} do - user = insert(:user) + test "creates a scheduled activity with a media attachment", %{user: user, conn: conn} do scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond) file = %Plug.Upload{ @@ -312,9 +287,7 @@ test "creates a scheduled activity with a media attachment", %{conn: conn} do {:ok, upload} = ActivityPub.upload(file, actor: user.ap_id) conn = - conn - |> assign(:user, user) - |> post("/api/v1/statuses", %{ + post(conn, "/api/v1/statuses", %{ "media_ids" => [to_string(upload.id)], "status" => "scheduled", "scheduled_at" => scheduled_at @@ -326,15 +299,11 @@ test "creates a scheduled activity with a media attachment", %{conn: conn} do test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now", %{conn: conn} do - user = insert(:user) - scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(5) - 1, :millisecond) conn = - conn - |> assign(:user, user) - |> post("/api/v1/statuses", %{ + post(conn, "/api/v1/statuses", %{ "status" => "not scheduled", "scheduled_at" => scheduled_at }) @@ -343,9 +312,7 @@ test "skips the scheduling and creates the activity if scheduled_at is earlier t assert [] == Repo.all(ScheduledActivity) end - test "returns error when daily user limit is exceeded", %{conn: conn} do - user = insert(:user) - + test "returns error when daily user limit is exceeded", %{user: user, conn: conn} do today = NaiveDateTime.utc_now() |> NaiveDateTime.add(:timer.minutes(6), :millisecond) @@ -355,17 +322,12 @@ test "returns error when daily user limit is exceeded", %{conn: conn} do {:ok, _} = ScheduledActivity.create(user, attrs) {:ok, _} = ScheduledActivity.create(user, attrs) - conn = - conn - |> assign(:user, user) - |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => today}) + conn = post(conn, "/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => today}) assert %{"error" => "daily limit exceeded"} == json_response(conn, 422) end - test "returns error when total user limit is exceeded", %{conn: conn} do - user = insert(:user) - + test "returns error when total user limit is exceeded", %{user: user, conn: conn} do today = NaiveDateTime.utc_now() |> NaiveDateTime.add(:timer.minutes(6), :millisecond) @@ -382,23 +344,20 @@ test "returns error when total user limit is exceeded", %{conn: conn} do {:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow}) conn = - conn - |> assign(:user, user) - |> post("/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => tomorrow}) + post(conn, "/api/v1/statuses", %{"status" => "scheduled", "scheduled_at" => tomorrow}) assert %{"error" => "total limit exceeded"} == json_response(conn, 422) end end describe "posting polls" do + setup do: oauth_access(["write:statuses"]) + test "posting a poll", %{conn: conn} do - user = insert(:user) time = NaiveDateTime.utc_now() conn = - conn - |> assign(:user, user) - |> post("/api/v1/statuses", %{ + post(conn, "/api/v1/statuses", %{ "status" => "Who is the #bestgrill?", "poll" => %{"options" => ["Rei", "Asuka", "Misato"], "expires_in" => 420} }) @@ -414,13 +373,10 @@ test "posting a poll", %{conn: conn} do end test "option limit is enforced", %{conn: conn} do - user = insert(:user) limit = Config.get([:instance, :poll_limits, :max_options]) conn = - conn - |> assign(:user, user) - |> post("/api/v1/statuses", %{ + post(conn, "/api/v1/statuses", %{ "status" => "desu~", "poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1} }) @@ -430,13 +386,10 @@ test "option limit is enforced", %{conn: conn} do end test "option character limit is enforced", %{conn: conn} do - user = insert(:user) limit = Config.get([:instance, :poll_limits, :max_option_chars]) conn = - conn - |> assign(:user, user) - |> post("/api/v1/statuses", %{ + post(conn, "/api/v1/statuses", %{ "status" => "...", "poll" => %{ "options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)], @@ -449,13 +402,10 @@ test "option character limit is enforced", %{conn: conn} do end test "minimal date limit is enforced", %{conn: conn} do - user = insert(:user) limit = Config.get([:instance, :poll_limits, :min_expiration]) conn = - conn - |> assign(:user, user) - |> post("/api/v1/statuses", %{ + post(conn, "/api/v1/statuses", %{ "status" => "imagine arbitrary limits", "poll" => %{ "options" => ["this post was made by pleroma gang"], @@ -468,13 +418,10 @@ test "minimal date limit is enforced", %{conn: conn} do end test "maximum date limit is enforced", %{conn: conn} do - user = insert(:user) limit = Config.get([:instance, :poll_limits, :max_expiration]) conn = - conn - |> assign(:user, user) - |> post("/api/v1/statuses", %{ + post(conn, "/api/v1/statuses", %{ "status" => "imagine arbitrary limits", "poll" => %{ "options" => ["this post was made by pleroma gang"], @@ -487,19 +434,18 @@ test "maximum date limit is enforced", %{conn: conn} do end end - test "get a status", %{conn: conn} do + test "get a status" do + %{conn: conn} = oauth_access(["read:statuses"]) activity = insert(:note_activity) - conn = - conn - |> get("/api/v1/statuses/#{activity.id}") + conn = get(conn, "/api/v1/statuses/#{activity.id}") assert %{"id" => id} = json_response(conn, 200) assert id == to_string(activity.id) end - test "get a direct status", %{conn: conn} do - user = insert(:user) + test "get a direct status" do + %{user: user, conn: conn} = oauth_access(["read:statuses"]) other_user = insert(:user) {:ok, activity} = @@ -516,7 +462,8 @@ test "get a direct status", %{conn: conn} do assert res["pleroma"]["direct_conversation_id"] == participation.id end - test "get statuses by IDs", %{conn: conn} do + test "get statuses by IDs" do + %{conn: conn} = oauth_access(["read:statuses"]) %{id: id1} = insert(:note_activity) %{id: id2} = insert(:note_activity) @@ -527,9 +474,9 @@ test "get statuses by IDs", %{conn: conn} do end describe "deleting a status" do - test "when you created it", %{conn: conn} do - activity = insert(:note_activity) - author = User.get_cached_by_ap_id(activity.data["actor"]) + test "when you created it" do + %{user: author, conn: conn} = oauth_access(["write:statuses"]) + activity = insert(:note_activity, user: author) conn = conn @@ -541,14 +488,11 @@ test "when you created it", %{conn: conn} do refute Activity.get_by_id(activity.id) end - test "when you didn't create it", %{conn: conn} do + test "when you didn't create it" do + %{conn: conn} = oauth_access(["write:statuses"]) activity = insert(:note_activity) - user = insert(:user) - conn = - conn - |> assign(:user, user) - |> delete("/api/v1/statuses/#{activity.id}") + conn = delete(conn, "/api/v1/statuses/#{activity.id}") assert %{"error" => _} = json_response(conn, 403) @@ -564,6 +508,7 @@ test "when you're an admin or moderator", %{conn: conn} do res_conn = conn |> assign(:user, admin) + |> assign(:token, insert(:oauth_token, user: admin, scopes: ["write:statuses"])) |> delete("/api/v1/statuses/#{activity1.id}") assert %{} = json_response(res_conn, 200) @@ -571,6 +516,7 @@ test "when you're an admin or moderator", %{conn: conn} do res_conn = conn |> assign(:user, moderator) + |> assign(:token, insert(:oauth_token, user: moderator, scopes: ["write:statuses"])) |> delete("/api/v1/statuses/#{activity2.id}") assert %{} = json_response(res_conn, 200) @@ -581,14 +527,12 @@ test "when you're an admin or moderator", %{conn: conn} do end describe "reblogging" do + setup do: oauth_access(["write:statuses"]) + test "reblogs and returns the reblogged status", %{conn: conn} do activity = insert(:note_activity) - user = insert(:user) - conn = - conn - |> assign(:user, user) - |> post("/api/v1/statuses/#{activity.id}/reblog") + conn = post(conn, "/api/v1/statuses/#{activity.id}/reblog") assert %{ "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}, @@ -600,12 +544,8 @@ test "reblogs and returns the reblogged status", %{conn: conn} do test "reblogs privately and returns the reblogged status", %{conn: conn} do activity = insert(:note_activity) - user = insert(:user) - conn = - conn - |> assign(:user, user) - |> post("/api/v1/statuses/#{activity.id}/reblog", %{"visibility" => "private"}) + conn = post(conn, "/api/v1/statuses/#{activity.id}/reblog", %{"visibility" => "private"}) assert %{ "reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}, @@ -616,7 +556,7 @@ test "reblogs privately and returns the reblogged status", %{conn: conn} do assert to_string(activity.id) == id end - test "reblogged status for another user", %{conn: conn} do + test "reblogged status for another user" do activity = insert(:note_activity) user1 = insert(:user) user2 = insert(:user) @@ -627,8 +567,9 @@ test "reblogged status for another user", %{conn: conn} do {:ok, _, _object} = CommonAPI.repeat(activity.id, user2) conn_res = - conn + build_conn() |> assign(:user, user3) + |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"])) |> get("/api/v1/statuses/#{reblog_activity1.id}") assert %{ @@ -639,8 +580,9 @@ test "reblogged status for another user", %{conn: conn} do } = json_response(conn_res, 200) conn_res = - conn + build_conn() |> assign(:user, user2) + |> assign(:token, insert(:oauth_token, user: user2, scopes: ["read:statuses"])) |> get("/api/v1/statuses/#{reblog_activity1.id}") assert %{ @@ -654,28 +596,21 @@ test "reblogged status for another user", %{conn: conn} do end test "returns 400 error when activity is not exist", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - |> post("/api/v1/statuses/foo/reblog") + conn = post(conn, "/api/v1/statuses/foo/reblog") assert json_response(conn, 400) == %{"error" => "Could not repeat"} end end describe "unreblogging" do - test "unreblogs and returns the unreblogged status", %{conn: conn} do + setup do: oauth_access(["write:statuses"]) + + test "unreblogs and returns the unreblogged status", %{user: user, conn: conn} do activity = insert(:note_activity) - user = insert(:user) {:ok, _, _} = CommonAPI.repeat(activity.id, user) - conn = - conn - |> assign(:user, user) - |> post("/api/v1/statuses/#{activity.id}/unreblog") + conn = post(conn, "/api/v1/statuses/#{activity.id}/unreblog") assert %{"id" => id, "reblogged" => false, "reblogs_count" => 0} = json_response(conn, 200) @@ -683,26 +618,19 @@ test "unreblogs and returns the unreblogged status", %{conn: conn} do end test "returns 400 error when activity is not exist", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - |> post("/api/v1/statuses/foo/unreblog") + conn = post(conn, "/api/v1/statuses/foo/unreblog") assert json_response(conn, 400) == %{"error" => "Could not unrepeat"} end end describe "favoriting" do + setup do: oauth_access(["write:favourites"]) + test "favs a status and returns it", %{conn: conn} do activity = insert(:note_activity) - user = insert(:user) - conn = - conn - |> assign(:user, user) - |> post("/api/v1/statuses/#{activity.id}/favourite") + conn = post(conn, "/api/v1/statuses/#{activity.id}/favourite") assert %{"id" => id, "favourites_count" => 1, "favourited" => true} = json_response(conn, 200) @@ -711,28 +639,21 @@ test "favs a status and returns it", %{conn: conn} do end test "returns 400 error for a wrong id", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - |> post("/api/v1/statuses/1/favourite") + conn = post(conn, "/api/v1/statuses/1/favourite") assert json_response(conn, 400) == %{"error" => "Could not favorite"} end end describe "unfavoriting" do - test "unfavorites a status and returns it", %{conn: conn} do + setup do: oauth_access(["write:favourites"]) + + test "unfavorites a status and returns it", %{user: user, conn: conn} do activity = insert(:note_activity) - user = insert(:user) {:ok, _, _} = CommonAPI.favorite(activity.id, user) - conn = - conn - |> assign(:user, user) - |> post("/api/v1/statuses/#{activity.id}/unfavourite") + conn = post(conn, "/api/v1/statuses/#{activity.id}/unfavourite") assert %{"id" => id, "favourites_count" => 0, "favourited" => false} = json_response(conn, 200) @@ -741,23 +662,19 @@ test "unfavorites a status and returns it", %{conn: conn} do end test "returns 400 error for a wrong id", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - |> post("/api/v1/statuses/1/unfavourite") + conn = post(conn, "/api/v1/statuses/1/unfavourite") assert json_response(conn, 400) == %{"error" => "Could not unfavorite"} end end describe "pinned statuses" do - setup do - user = insert(:user) + setup do: oauth_access(["write:accounts"]) + + setup %{user: user} do {:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!"}) - [user: user, activity: activity] + %{activity: activity} end clear_config([:instance, :max_pinned_statuses]) do @@ -769,13 +686,11 @@ test "pin status", %{conn: conn, user: user, activity: activity} do assert %{"id" => ^id_str, "pinned" => true} = conn - |> assign(:user, user) |> post("/api/v1/statuses/#{activity.id}/pin") |> json_response(200) assert [%{"id" => ^id_str, "pinned" => true}] = conn - |> assign(:user, user) |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true") |> json_response(200) end @@ -783,19 +698,16 @@ test "pin status", %{conn: conn, user: user, activity: activity} do test "/pin: returns 400 error when activity is not public", %{conn: conn, user: user} do {:ok, dm} = CommonAPI.post(user, %{"status" => "test", "visibility" => "direct"}) - conn = - conn - |> assign(:user, user) - |> post("/api/v1/statuses/#{dm.id}/pin") + conn = post(conn, "/api/v1/statuses/#{dm.id}/pin") assert json_response(conn, 400) == %{"error" => "Could not pin"} end test "unpin status", %{conn: conn, user: user, activity: activity} do {:ok, _} = CommonAPI.pin(activity.id, user) + user = refresh_record(user) id_str = to_string(activity.id) - user = refresh_record(user) assert %{"id" => ^id_str, "pinned" => false} = conn @@ -805,16 +717,12 @@ test "unpin status", %{conn: conn, user: user, activity: activity} do assert [] = conn - |> assign(:user, user) |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true") |> json_response(200) end - test "/unpin: returns 400 error when activity is not exist", %{conn: conn, user: user} do - conn = - conn - |> assign(:user, user) - |> post("/api/v1/statuses/1/unpin") + test "/unpin: returns 400 error when activity is not exist", %{conn: conn} do + conn = post(conn, "/api/v1/statuses/1/unpin") assert json_response(conn, 400) == %{"error" => "Could not unpin"} end @@ -826,7 +734,6 @@ test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do assert %{"id" => ^id_str_one, "pinned" => true} = conn - |> assign(:user, user) |> post("/api/v1/statuses/#{id_str_one}/pin") |> json_response(200) @@ -844,8 +751,7 @@ test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do setup do Config.put([:rich_media, :enabled], true) - user = insert(:user) - %{user: user} + oauth_access(["read:statuses"]) end test "returns rich-media card", %{conn: conn, user: user} do @@ -887,7 +793,6 @@ test "returns rich-media card", %{conn: conn, user: user} do response_two = conn - |> assign(:user, user) |> get("/api/v1/statuses/#{activity.id}/card") |> json_response(200) @@ -925,72 +830,55 @@ test "replaces missing description with an empty string", %{conn: conn, user: us end test "bookmarks" do - user = insert(:user) - for_user = insert(:user) + %{conn: conn} = oauth_access(["write:bookmarks", "read:bookmarks"]) + author = insert(:user) {:ok, activity1} = - CommonAPI.post(user, %{ + CommonAPI.post(author, %{ "status" => "heweoo?" }) {:ok, activity2} = - CommonAPI.post(user, %{ + CommonAPI.post(author, %{ "status" => "heweoo!" }) - response1 = - build_conn() - |> assign(:user, for_user) - |> post("/api/v1/statuses/#{activity1.id}/bookmark") + response1 = post(conn, "/api/v1/statuses/#{activity1.id}/bookmark") assert json_response(response1, 200)["bookmarked"] == true - response2 = - build_conn() - |> assign(:user, for_user) - |> post("/api/v1/statuses/#{activity2.id}/bookmark") + response2 = post(conn, "/api/v1/statuses/#{activity2.id}/bookmark") assert json_response(response2, 200)["bookmarked"] == true - bookmarks = - build_conn() - |> assign(:user, for_user) - |> get("/api/v1/bookmarks") + bookmarks = get(conn, "/api/v1/bookmarks") assert [json_response(response2, 200), json_response(response1, 200)] == json_response(bookmarks, 200) - response1 = - build_conn() - |> assign(:user, for_user) - |> post("/api/v1/statuses/#{activity1.id}/unbookmark") + response1 = post(conn, "/api/v1/statuses/#{activity1.id}/unbookmark") assert json_response(response1, 200)["bookmarked"] == false - bookmarks = - build_conn() - |> assign(:user, for_user) - |> get("/api/v1/bookmarks") + bookmarks = get(conn, "/api/v1/bookmarks") assert [json_response(response2, 200)] == json_response(bookmarks, 200) end describe "conversation muting" do + setup do: oauth_access(["write:mutes"]) + setup do post_user = insert(:user) - user = insert(:user) - {:ok, activity} = CommonAPI.post(post_user, %{"status" => "HIE"}) - - [user: user, activity: activity] + %{activity: activity} end - test "mute conversation", %{conn: conn, user: user, activity: activity} do + test "mute conversation", %{conn: conn, activity: activity} do id_str = to_string(activity.id) assert %{"id" => ^id_str, "muted" => true} = conn - |> assign(:user, user) |> post("/api/v1/statuses/#{activity.id}/mute") |> json_response(200) end @@ -998,10 +886,7 @@ test "mute conversation", %{conn: conn, user: user, activity: activity} do test "cannot mute already muted conversation", %{conn: conn, user: user, activity: activity} do {:ok, _} = CommonAPI.add_mute(user, activity) - conn = - conn - |> assign(:user, user) - |> post("/api/v1/statuses/#{activity.id}/mute") + conn = post(conn, "/api/v1/statuses/#{activity.id}/mute") assert json_response(conn, 400) == %{"error" => "conversation is already muted"} end @@ -1010,11 +895,10 @@ test "unmute conversation", %{conn: conn, user: user, activity: activity} do {:ok, _} = CommonAPI.add_mute(user, activity) id_str = to_string(activity.id) - user = refresh_record(user) assert %{"id" => ^id_str, "muted" => false} = conn - |> assign(:user, user) + # |> assign(:user, user) |> post("/api/v1/statuses/#{activity.id}/unmute") |> json_response(200) end @@ -1031,6 +915,7 @@ test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{c conn1 = conn |> assign(:user, user2) + |> assign(:token, insert(:oauth_token, user: user2, scopes: ["write:statuses"])) |> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id}) assert %{"content" => "xD", "id" => id} = json_response(conn1, 200) @@ -1044,6 +929,7 @@ test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{c conn2 = conn |> assign(:user, user3) + |> assign(:token, insert(:oauth_token, user: user3, scopes: ["write:statuses"])) |> post("/api/v1/statuses/#{activity.id}/reblog") assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} = @@ -1055,6 +941,7 @@ test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{c conn3 = conn |> assign(:user, user3) + |> assign(:token, insert(:oauth_token, user: user3, scopes: ["read:statuses"])) |> get("api/v1/timelines/home") [reblogged_activity] = json_response(conn3, 200) @@ -1066,15 +953,12 @@ test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{c end describe "GET /api/v1/statuses/:id/favourited_by" do - setup do - user = insert(:user) + setup do: oauth_access(["read:accounts"]) + + setup %{user: user} do {:ok, activity} = CommonAPI.post(user, %{"status" => "test"}) - conn = - build_conn() - |> assign(:user, user) - - [conn: conn, activity: activity, user: user] + %{activity: activity} end test "returns users who have favorited the status", %{conn: conn, activity: activity} do @@ -1114,20 +998,18 @@ test "does not return users who have favorited the status but are blocked", %{ response = conn - |> assign(:user, user) |> get("/api/v1/statuses/#{activity.id}/favourited_by") |> json_response(:ok) assert Enum.empty?(response) end - test "does not fail on an unauthenticated request", %{conn: conn, activity: activity} do + test "does not fail on an unauthenticated request", %{activity: activity} do other_user = insert(:user) {:ok, _, _} = CommonAPI.favorite(activity.id, other_user) response = - conn - |> assign(:user, nil) + build_conn() |> get("/api/v1/statuses/#{activity.id}/favourited_by") |> json_response(:ok) @@ -1135,7 +1017,7 @@ test "does not fail on an unauthenticated request", %{conn: conn, activity: acti assert id == other_user.id end - test "requires authentification for private posts", %{conn: conn, user: user} do + test "requires authentication for private posts", %{user: user} do other_user = insert(:user) {:ok, activity} = @@ -1146,15 +1028,25 @@ test "requires authentification for private posts", %{conn: conn, user: user} do {:ok, _, _} = CommonAPI.favorite(activity.id, other_user) + favourited_by_url = "/api/v1/statuses/#{activity.id}/favourited_by" + + build_conn() + |> get(favourited_by_url) + |> json_response(404) + + conn = + build_conn() + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"])) + conn - |> assign(:user, nil) - |> get("/api/v1/statuses/#{activity.id}/favourited_by") + |> assign(:token, nil) + |> get(favourited_by_url) |> json_response(404) response = - build_conn() - |> assign(:user, other_user) - |> get("/api/v1/statuses/#{activity.id}/favourited_by") + conn + |> get(favourited_by_url) |> json_response(200) [%{"id" => id}] = response @@ -1163,15 +1055,12 @@ test "requires authentification for private posts", %{conn: conn, user: user} do end describe "GET /api/v1/statuses/:id/reblogged_by" do - setup do - user = insert(:user) + setup do: oauth_access(["read:accounts"]) + + setup %{user: user} do {:ok, activity} = CommonAPI.post(user, %{"status" => "test"}) - conn = - build_conn() - |> assign(:user, user) - - [conn: conn, activity: activity, user: user] + %{activity: activity} end test "returns users who have reblogged the status", %{conn: conn, activity: activity} do @@ -1211,7 +1100,6 @@ test "does not return users who have reblogged the status but are blocked", %{ response = conn - |> assign(:user, user) |> get("/api/v1/statuses/#{activity.id}/reblogged_by") |> json_response(:ok) @@ -1219,7 +1107,7 @@ test "does not return users who have reblogged the status but are blocked", %{ end test "does not return users who have reblogged the status privately", %{ - conn: %{assigns: %{user: user}} = conn, + conn: conn, activity: activity } do other_user = insert(:user) @@ -1228,20 +1116,18 @@ test "does not return users who have reblogged the status privately", %{ response = conn - |> assign(:user, user) |> get("/api/v1/statuses/#{activity.id}/reblogged_by") |> json_response(:ok) assert Enum.empty?(response) end - test "does not fail on an unauthenticated request", %{conn: conn, activity: activity} do + test "does not fail on an unauthenticated request", %{activity: activity} do other_user = insert(:user) {:ok, _, _} = CommonAPI.repeat(activity.id, other_user) response = - conn - |> assign(:user, nil) + build_conn() |> get("/api/v1/statuses/#{activity.id}/reblogged_by") |> json_response(:ok) @@ -1249,7 +1135,7 @@ test "does not fail on an unauthenticated request", %{conn: conn, activity: acti assert id == other_user.id end - test "requires authentification for private posts", %{conn: conn, user: user} do + test "requires authentication for private posts", %{user: user} do other_user = insert(:user) {:ok, activity} = @@ -1258,14 +1144,14 @@ test "requires authentification for private posts", %{conn: conn, user: user} do "visibility" => "direct" }) - conn - |> assign(:user, nil) + build_conn() |> get("/api/v1/statuses/#{activity.id}/reblogged_by") |> json_response(404) response = build_conn() |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"])) |> get("/api/v1/statuses/#{activity.id}/reblogged_by") |> json_response(200) @@ -1284,7 +1170,6 @@ test "context" do response = build_conn() - |> assign(:user, nil) |> get("/api/v1/statuses/#{id3}/context") |> json_response(:ok) @@ -1294,8 +1179,8 @@ test "context" do } = response end - test "returns the favorites of a user", %{conn: conn} do - user = insert(:user) + test "returns the favorites of a user" do + %{user: user, conn: conn} = oauth_access(["read:favourites"]) other_user = insert(:user) {:ok, _} = CommonAPI.post(other_user, %{"status" => "bla"}) @@ -1303,10 +1188,7 @@ test "returns the favorites of a user", %{conn: conn} do {:ok, _, _} = CommonAPI.favorite(activity.id, user) - first_conn = - conn - |> assign(:user, user) - |> get("/api/v1/favourites") + first_conn = get(conn, "/api/v1/favourites") assert [status] = json_response(first_conn, 200) assert status["id"] == to_string(activity.id) @@ -1325,18 +1207,12 @@ test "returns the favorites of a user", %{conn: conn} do last_like = status["id"] - second_conn = - conn - |> assign(:user, user) - |> get("/api/v1/favourites?since_id=#{last_like}") + second_conn = get(conn, "/api/v1/favourites?since_id=#{last_like}") assert [second_status] = json_response(second_conn, 200) assert second_status["id"] == to_string(second_activity.id) - third_conn = - conn - |> assign(:user, user) - |> get("/api/v1/favourites?limit=0") + third_conn = get(conn, "/api/v1/favourites?limit=0") assert [] = json_response(third_conn, 200) end diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs index 901f2ae41..9cc534f57 100644 --- a/test/web/oauth/oauth_controller_test.exs +++ b/test/web/oauth/oauth_controller_test.exs @@ -450,7 +450,7 @@ test "properly handles internal calls with `authorization`-wrapped params", %{ test "renders authentication page if user is already authenticated but `force_login` is tru-ish", %{app: app, conn: conn} do - token = insert(:oauth_token, app_id: app.id) + token = insert(:oauth_token, app: app) conn = conn @@ -474,7 +474,7 @@ test "renders authentication page if user is already authenticated but user requ app: app, conn: conn } do - token = insert(:oauth_token, app_id: app.id) + token = insert(:oauth_token, app: app) conn = conn @@ -497,7 +497,7 @@ test "with existing authentication and non-OOB `redirect_uri`, redirects to app app: app, conn: conn } do - token = insert(:oauth_token, app_id: app.id) + token = insert(:oauth_token, app: app) conn = conn @@ -523,7 +523,7 @@ test "with existing authentication and unlisted non-OOB `redirect_uri`, redirect conn: conn } do unlisted_redirect_uri = "http://cross-site-request.com" - token = insert(:oauth_token, app_id: app.id) + token = insert(:oauth_token, app: app) conn = conn @@ -547,7 +547,7 @@ test "with existing authentication and OOB `redirect_uri`, redirects to app with app: app, conn: conn } do - token = insert(:oauth_token, app_id: app.id) + token = insert(:oauth_token, app: app) conn = conn 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 b1b59beed..3f7ef13bc 100644 --- a/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs +++ b/test/web/pleroma_api/controllers/pleroma_api_controller_test.exs @@ -23,6 +23,7 @@ test "POST /api/v1/pleroma/statuses/:id/react_with_emoji", %{conn: conn} do result = conn |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"])) |> post("/api/v1/pleroma/statuses/#{activity.id}/react_with_emoji", %{"emoji" => "☕"}) assert %{"id" => id} = json_response(result, 200) @@ -39,6 +40,7 @@ test "POST /api/v1/pleroma/statuses/:id/unreact_with_emoji", %{conn: conn} do result = conn |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"])) |> post("/api/v1/pleroma/statuses/#{activity.id}/unreact_with_emoji", %{"emoji" => "☕"}) assert %{"id" => id} = json_response(result, 200) @@ -55,6 +57,11 @@ 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") @@ -73,9 +80,9 @@ test "GET /api/v1/pleroma/statuses/:id/emoji_reactions_by", %{conn: conn} do assert represented_user["id"] == other_user.id end - test "/api/v1/pleroma/conversations/:id", %{conn: conn} do + test "/api/v1/pleroma/conversations/:id" do user = insert(:user) - other_user = insert(:user) + %{user: other_user, conn: conn} = oauth_access(["read:statuses"]) {:ok, _activity} = CommonAPI.post(user, %{"status" => "Hi @#{other_user.nickname}!", "visibility" => "direct"}) @@ -84,16 +91,15 @@ test "/api/v1/pleroma/conversations/:id", %{conn: conn} do result = conn - |> assign(:user, other_user) |> get("/api/v1/pleroma/conversations/#{participation.id}") |> json_response(200) assert result["id"] == participation.id |> to_string() end - test "/api/v1/pleroma/conversations/:id/statuses", %{conn: conn} do + test "/api/v1/pleroma/conversations/:id/statuses" do user = insert(:user) - other_user = insert(:user) + %{user: other_user, conn: conn} = oauth_access(["read:statuses"]) third_user = insert(:user) {:ok, _activity} = @@ -113,7 +119,6 @@ test "/api/v1/pleroma/conversations/:id/statuses", %{conn: conn} do result = conn - |> assign(:user, other_user) |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses") |> json_response(200) @@ -124,8 +129,8 @@ test "/api/v1/pleroma/conversations/:id/statuses", %{conn: conn} do assert [%{"id" => ^id_one}, %{"id" => ^id_two}] = result end - test "PATCH /api/v1/pleroma/conversations/:id", %{conn: conn} do - user = insert(:user) + test "PATCH /api/v1/pleroma/conversations/:id" do + %{user: user, conn: conn} = oauth_access(["write:conversations"]) other_user = insert(:user) {:ok, _activity} = CommonAPI.post(user, %{"status" => "Hi", "visibility" => "direct"}) @@ -140,7 +145,6 @@ test "PATCH /api/v1/pleroma/conversations/:id", %{conn: conn} do result = conn - |> assign(:user, user) |> patch("/api/v1/pleroma/conversations/#{participation.id}", %{ "recipients" => [user.id, other_user.id] }) @@ -155,9 +159,9 @@ test "PATCH /api/v1/pleroma/conversations/:id", %{conn: conn} do assert other_user in participation.recipients end - test "POST /api/v1/pleroma/conversations/read", %{conn: conn} do + test "POST /api/v1/pleroma/conversations/read" do user = insert(:user) - other_user = insert(:user) + %{user: other_user, conn: conn} = oauth_access(["write:notifications"]) {:ok, _activity} = CommonAPI.post(user, %{"status" => "Hi @#{other_user.nickname}", "visibility" => "direct"}) @@ -172,7 +176,6 @@ test "POST /api/v1/pleroma/conversations/read", %{conn: conn} do [%{"unread" => false}, %{"unread" => false}] = conn - |> assign(:user, other_user) |> post("/api/v1/pleroma/conversations/read", %{}) |> json_response(200) @@ -183,8 +186,9 @@ test "POST /api/v1/pleroma/conversations/read", %{conn: conn} do end describe "POST /api/v1/pleroma/notifications/read" do - test "it marks a single notification as read", %{conn: conn} do - user1 = insert(:user) + setup do: oauth_access(["write:notifications"]) + + test "it marks a single notification as read", %{user: user1, conn: conn} do user2 = insert(:user) {:ok, activity1} = CommonAPI.post(user2, %{"status" => "hi @#{user1.nickname}"}) {:ok, activity2} = CommonAPI.post(user2, %{"status" => "hi @#{user1.nickname}"}) @@ -193,7 +197,6 @@ test "it marks a single notification as read", %{conn: conn} do response = conn - |> assign(:user, user1) |> post("/api/v1/pleroma/notifications/read", %{"id" => "#{notification1.id}"}) |> json_response(:ok) @@ -202,8 +205,7 @@ test "it marks a single notification as read", %{conn: conn} do refute Repo.get(Notification, notification2.id).seen end - test "it marks multiple notifications as read", %{conn: conn} do - user1 = insert(:user) + test "it marks multiple notifications as read", %{user: user1, conn: conn} do user2 = insert(:user) {:ok, _activity1} = CommonAPI.post(user2, %{"status" => "hi @#{user1.nickname}"}) {:ok, _activity2} = CommonAPI.post(user2, %{"status" => "hi @#{user1.nickname}"}) @@ -213,7 +215,6 @@ test "it marks multiple notifications as read", %{conn: conn} do [response1, response2] = conn - |> assign(:user, user1) |> post("/api/v1/pleroma/notifications/read", %{"max_id" => "#{notification2.id}"}) |> json_response(:ok) @@ -225,11 +226,8 @@ test "it marks multiple notifications as read", %{conn: conn} do end test "it returns error when notification not found", %{conn: conn} do - user1 = insert(:user) - response = conn - |> assign(:user, user1) |> post("/api/v1/pleroma/notifications/read", %{"id" => "22222222222222"}) |> json_response(:bad_request) diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs index 734cd2211..9bfaba9d3 100644 --- a/test/web/twitter_api/util_controller_test.exs +++ b/test/web/twitter_api/util_controller_test.exs @@ -6,10 +6,10 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do use Pleroma.Web.ConnCase use Oban.Testing, repo: Pleroma.Repo - alias Pleroma.Repo alias Pleroma.Tests.ObanHelpers alias Pleroma.User alias Pleroma.Web.CommonAPI + import ExUnit.CaptureLog import Pleroma.Factory import Mock @@ -24,21 +24,20 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do clear_config([:user, :deny_follow_blocked]) describe "POST /api/pleroma/follow_import" do + setup do: oauth_access(["follow"]) + test "it returns HTTP 200", %{conn: conn} do - user1 = insert(:user) user2 = insert(:user) response = conn - |> assign(:user, user1) |> post("/api/pleroma/follow_import", %{"list" => "#{user2.ap_id}"}) |> json_response(:ok) assert response == "job started" end - test "it imports follow lists from file", %{conn: conn} do - user1 = insert(:user) + test "it imports follow lists from file", %{user: user1, conn: conn} do user2 = insert(:user) with_mocks([ @@ -49,7 +48,6 @@ test "it imports follow lists from file", %{conn: conn} do ]) do response = conn - |> assign(:user, user1) |> post("/api/pleroma/follow_import", %{"list" => %Plug.Upload{path: "follow_list.txt"}}) |> json_response(:ok) @@ -67,12 +65,10 @@ test "it imports follow lists from file", %{conn: conn} do end test "it imports new-style mastodon follow lists", %{conn: conn} do - user1 = insert(:user) user2 = insert(:user) response = conn - |> assign(:user, user1) |> post("/api/pleroma/follow_import", %{ "list" => "Account address,Show boosts\n#{user2.ap_id},true" }) @@ -81,7 +77,7 @@ test "it imports new-style mastodon follow lists", %{conn: conn} do assert response == "job started" end - test "requires 'follow' or 'write:follows' permissions", %{conn: conn} do + test "requires 'follow' or 'write:follows' permissions" do token1 = insert(:oauth_token, scopes: ["read", "write"]) token2 = insert(:oauth_token, scopes: ["follow"]) token3 = insert(:oauth_token, scopes: ["something"]) @@ -89,7 +85,7 @@ test "requires 'follow' or 'write:follows' permissions", %{conn: conn} do for token <- [token1, token2, token3] do conn = - conn + build_conn() |> put_req_header("authorization", "Bearer #{token.token}") |> post("/api/pleroma/follow_import", %{"list" => "#{another_user.ap_id}"}) @@ -104,21 +100,21 @@ test "requires 'follow' or 'write:follows' permissions", %{conn: conn} do end describe "POST /api/pleroma/blocks_import" do + # Note: "follow" or "write:blocks" permission is required + setup do: oauth_access(["write:blocks"]) + test "it returns HTTP 200", %{conn: conn} do - user1 = insert(:user) user2 = insert(:user) response = conn - |> assign(:user, user1) |> post("/api/pleroma/blocks_import", %{"list" => "#{user2.ap_id}"}) |> json_response(:ok) assert response == "job started" end - test "it imports blocks users from file", %{conn: conn} do - user1 = insert(:user) + test "it imports blocks users from file", %{user: user1, conn: conn} do user2 = insert(:user) user3 = insert(:user) @@ -127,7 +123,6 @@ test "it imports blocks users from file", %{conn: conn} do ]) do response = conn - |> assign(:user, user1) |> post("/api/pleroma/blocks_import", %{"list" => %Plug.Upload{path: "blocks_list.txt"}}) |> json_response(:ok) @@ -146,18 +141,17 @@ test "it imports blocks users from file", %{conn: conn} do end describe "PUT /api/pleroma/notification_settings" do - test "it updates notification settings", %{conn: conn} do - user = insert(:user) + setup do: oauth_access(["write:accounts"]) + test "it updates notification settings", %{user: user, conn: conn} do conn - |> assign(:user, user) |> put("/api/pleroma/notification_settings", %{ "followers" => false, "bar" => 1 }) |> json_response(:ok) - user = Repo.get(User, user.id) + user = refresh_record(user) assert %Pleroma.User.NotificationSetting{ followers: false, @@ -168,11 +162,8 @@ test "it updates notification settings", %{conn: conn} do } == user.notification_settings end - test "it update notificatin privacy option", %{conn: conn} do - user = insert(:user) - + test "it updates notification privacy option", %{user: user, conn: conn} do conn - |> assign(:user, user) |> put("/api/pleroma/notification_settings", %{"privacy_option" => "1"}) |> json_response(:ok) @@ -374,14 +365,14 @@ test "show follow page with error when user cannot fecth by `acct` link", %{conn end end - describe "POST /ostatus_subscribe - do_remote_follow/2 with assigned user " do - test "follows user", %{conn: conn} do - user = insert(:user) + describe "POST /ostatus_subscribe - do_remote_follow/2 with assigned user" do + setup do: oauth_access(["follow"]) + + test "follows user", %{user: user, conn: conn} do user2 = insert(:user) response = conn - |> assign(:user, user) |> post("/ostatus_subscribe", %{"user" => %{"id" => user2.id}}) |> response(200) @@ -389,55 +380,63 @@ test "follows user", %{conn: conn} do assert user2.follower_address in User.following(user) end - test "returns error when user is deactivated", %{conn: conn} do + test "returns error when user is deactivated" do user = insert(:user, deactivated: true) user2 = insert(:user) response = - conn + build_conn() |> assign(:user, user) + |> assign(:token, insert(:oauth_token, user: user, scopes: ["follow"])) |> post("/ostatus_subscribe", %{"user" => %{"id" => user2.id}}) |> response(200) assert response =~ "Error following account" end - test "returns error when user is blocked", %{conn: conn} do + test "returns error when user is blocked", %{user: user, conn: conn} do Pleroma.Config.put([:user, :deny_follow_blocked], true) - user = insert(:user) user2 = insert(:user) {:ok, _user_block} = Pleroma.User.block(user2, user) response = conn - |> assign(:user, user) |> post("/ostatus_subscribe", %{"user" => %{"id" => user2.id}}) |> response(200) assert response =~ "Error following account" end - test "returns error when followee not found", %{conn: conn} do - user = insert(:user) + test "returns error on insufficient permissions", %{user: user, conn: conn} do + user2 = insert(:user) + for token <- [nil, insert(:oauth_token, user: user, scopes: ["read"])] do + response = + conn + |> assign(:token, token) + |> post("/ostatus_subscribe", %{"user" => %{"id" => user2.id}}) + |> response(200) + + assert response =~ "Error following account" + end + end + + test "returns error when followee not found", %{conn: conn} do response = conn - |> assign(:user, user) |> post("/ostatus_subscribe", %{"user" => %{"id" => "jimm"}}) |> response(200) assert response =~ "Error following account" end - test "returns success result when user already in followers", %{conn: conn} do - user = insert(:user) + test "returns success result when user already in followers", %{user: user, conn: conn} do user2 = insert(:user) {:ok, _, _, _} = CommonAPI.follow(user, user2) response = conn - |> assign(:user, refresh_record(user)) |> post("/ostatus_subscribe", %{"user" => %{"id" => user2.id}}) |> response(200) @@ -445,7 +444,7 @@ test "returns success result when user already in followers", %{conn: conn} do end end - describe "POST /ostatus_subscribe - do_remote_follow/2 without assigned user " do + describe "POST /ostatus_subscribe - do_remote_follow/2 without assigned user" do test "follows", %{conn: conn} do user = insert(:user) user2 = insert(:user) @@ -552,7 +551,7 @@ test "returns 200 when healthcheck enabled and all ok", %{conn: conn} do end end - test "returns 503 when healthcheck enabled and health is false", %{conn: conn} do + test "returns 503 when healthcheck enabled and health is false", %{conn: conn} do Pleroma.Config.put([:instance, :healthcheck], true) with_mock Pleroma.Healthcheck, @@ -574,12 +573,11 @@ test "returns 503 when healthcheck enabled and health is false", %{conn: conn} end describe "POST /api/pleroma/disable_account" do - test "it returns HTTP 200", %{conn: conn} do - user = insert(:user) + setup do: oauth_access(["write:accounts"]) + test "with valid permissions and password, it disables the account", %{conn: conn, user: user} do response = conn - |> assign(:user, user) |> post("/api/pleroma/disable_account", %{"password" => "test"}) |> json_response(:ok) @@ -591,12 +589,11 @@ test "it returns HTTP 200", %{conn: conn} do assert user.deactivated == true end - test "it returns returns when password invalid", %{conn: conn} do + test "with valid permissions and invalid password, it returns an error", %{conn: conn} do user = insert(:user) response = conn - |> assign(:user, user) |> post("/api/pleroma/disable_account", %{"password" => "test1"}) |> json_response(:ok) @@ -666,7 +663,7 @@ test "it redirect to webfinger url", %{conn: conn} do "https://social.heldscal.la/main/ostatussub?profile=#{user.ap_id}" end - test "it renders form with error when use not found", %{conn: conn} do + test "it renders form with error when user not found", %{conn: conn} do user2 = insert(:user, ap_id: "shp@social.heldscal.la") response = @@ -691,29 +688,21 @@ test "it returns new captcha", %{conn: conn} do end end - defp with_credentials(conn, username, password) do - header_content = "Basic " <> Base.encode64("#{username}:#{password}") - put_req_header(conn, "authorization", header_content) - end - - defp valid_user(_context) do - user = insert(:user) - [user: user] - end - describe "POST /api/pleroma/change_email" do - setup [:valid_user] + setup do: oauth_access(["write:accounts"]) - test "without credentials", %{conn: conn} do - conn = post(conn, "/api/pleroma/change_email") - assert json_response(conn, 403) == %{"error" => "Invalid credentials."} - end - - test "with credentials and invalid password", %{conn: conn, user: current_user} do + test "without permissions", %{conn: conn} do conn = conn - |> with_credentials(current_user.nickname, "test") - |> post("/api/pleroma/change_email", %{ + |> assign(:token, nil) + |> post("/api/pleroma/change_email") + + assert json_response(conn, 403) == %{"error" => "Insufficient permissions: write:accounts."} + end + + test "with proper permissions and invalid password", %{conn: conn} do + conn = + post(conn, "/api/pleroma/change_email", %{ "password" => "hi", "email" => "test@test.com" }) @@ -721,14 +710,11 @@ test "with credentials and invalid password", %{conn: conn, user: current_user} assert json_response(conn, 200) == %{"error" => "Invalid password."} end - test "with credentials, valid password and invalid email", %{ - conn: conn, - user: current_user + test "with proper permissions, valid password and invalid email", %{ + conn: conn } do conn = - conn - |> with_credentials(current_user.nickname, "test") - |> post("/api/pleroma/change_email", %{ + post(conn, "/api/pleroma/change_email", %{ "password" => "test", "email" => "foobar" }) @@ -736,28 +722,22 @@ test "with credentials, valid password and invalid email", %{ assert json_response(conn, 200) == %{"error" => "Email has invalid format."} end - test "with credentials, valid password and no email", %{ - conn: conn, - user: current_user + test "with proper permissions, valid password and no email", %{ + conn: conn } do conn = - conn - |> with_credentials(current_user.nickname, "test") - |> post("/api/pleroma/change_email", %{ + post(conn, "/api/pleroma/change_email", %{ "password" => "test" }) assert json_response(conn, 200) == %{"error" => "Email can't be blank."} end - test "with credentials, valid password and blank email", %{ - conn: conn, - user: current_user + test "with proper permissions, valid password and blank email", %{ + conn: conn } do conn = - conn - |> with_credentials(current_user.nickname, "test") - |> post("/api/pleroma/change_email", %{ + post(conn, "/api/pleroma/change_email", %{ "password" => "test", "email" => "" }) @@ -765,16 +745,13 @@ test "with credentials, valid password and blank email", %{ assert json_response(conn, 200) == %{"error" => "Email can't be blank."} end - test "with credentials, valid password and non unique email", %{ - conn: conn, - user: current_user + test "with proper permissions, valid password and non unique email", %{ + conn: conn } do user = insert(:user) conn = - conn - |> with_credentials(current_user.nickname, "test") - |> post("/api/pleroma/change_email", %{ + post(conn, "/api/pleroma/change_email", %{ "password" => "test", "email" => user.email }) @@ -782,14 +759,11 @@ test "with credentials, valid password and non unique email", %{ assert json_response(conn, 200) == %{"error" => "Email has already been taken."} end - test "with credentials, valid password and valid email", %{ - conn: conn, - user: current_user + test "with proper permissions, valid password and valid email", %{ + conn: conn } do conn = - conn - |> with_credentials(current_user.nickname, "test") - |> post("/api/pleroma/change_email", %{ + post(conn, "/api/pleroma/change_email", %{ "password" => "test", "email" => "cofe@foobar.com" }) @@ -799,18 +773,20 @@ test "with credentials, valid password and valid email", %{ end describe "POST /api/pleroma/change_password" do - setup [:valid_user] + setup do: oauth_access(["write:accounts"]) - test "without credentials", %{conn: conn} do - conn = post(conn, "/api/pleroma/change_password") - assert json_response(conn, 403) == %{"error" => "Invalid credentials."} - end - - test "with credentials and invalid password", %{conn: conn, user: current_user} do + test "without permissions", %{conn: conn} do conn = conn - |> with_credentials(current_user.nickname, "test") - |> post("/api/pleroma/change_password", %{ + |> assign(:token, nil) + |> post("/api/pleroma/change_password") + + assert json_response(conn, 403) == %{"error" => "Insufficient permissions: write:accounts."} + end + + test "with proper permissions and invalid password", %{conn: conn} do + conn = + post(conn, "/api/pleroma/change_password", %{ "password" => "hi", "new_password" => "newpass", "new_password_confirmation" => "newpass" @@ -819,14 +795,12 @@ test "with credentials and invalid password", %{conn: conn, user: current_user} assert json_response(conn, 200) == %{"error" => "Invalid password."} end - test "with credentials, valid password and new password and confirmation not matching", %{ - conn: conn, - user: current_user - } do + test "with proper permissions, valid password and new password and confirmation not matching", + %{ + conn: conn + } do conn = - conn - |> with_credentials(current_user.nickname, "test") - |> post("/api/pleroma/change_password", %{ + post(conn, "/api/pleroma/change_password", %{ "password" => "test", "new_password" => "newpass", "new_password_confirmation" => "notnewpass" @@ -837,14 +811,11 @@ test "with credentials, valid password and new password and confirmation not mat } end - test "with credentials, valid password and invalid new password", %{ - conn: conn, - user: current_user + test "with proper permissions, valid password and invalid new password", %{ + conn: conn } do conn = - conn - |> with_credentials(current_user.nickname, "test") - |> post("/api/pleroma/change_password", %{ + post(conn, "/api/pleroma/change_password", %{ "password" => "test", "new_password" => "", "new_password_confirmation" => "" @@ -855,51 +826,48 @@ test "with credentials, valid password and invalid new password", %{ } end - test "with credentials, valid password and matching new password and confirmation", %{ + test "with proper permissions, valid password and matching new password and confirmation", %{ conn: conn, - user: current_user + user: user } do conn = - conn - |> with_credentials(current_user.nickname, "test") - |> post("/api/pleroma/change_password", %{ + post(conn, "/api/pleroma/change_password", %{ "password" => "test", "new_password" => "newpass", "new_password_confirmation" => "newpass" }) assert json_response(conn, 200) == %{"status" => "success"} - fetched_user = User.get_cached_by_id(current_user.id) + fetched_user = User.get_cached_by_id(user.id) assert Comeonin.Pbkdf2.checkpw("newpass", fetched_user.password_hash) == true end end describe "POST /api/pleroma/delete_account" do - setup [:valid_user] + setup do: oauth_access(["write:accounts"]) - test "without credentials", %{conn: conn} do - conn = post(conn, "/api/pleroma/delete_account") - assert json_response(conn, 403) == %{"error" => "Invalid credentials."} - end - - test "with credentials and invalid password", %{conn: conn, user: current_user} do + test "without permissions", %{conn: conn} do conn = conn - |> with_credentials(current_user.nickname, "test") - |> post("/api/pleroma/delete_account", %{"password" => "hi"}) + |> assign(:token, nil) + |> post("/api/pleroma/delete_account") - assert json_response(conn, 200) == %{"error" => "Invalid password."} + assert json_response(conn, 403) == + %{"error" => "Insufficient permissions: write:accounts."} end - test "with credentials and valid password", %{conn: conn, user: current_user} do - conn = - conn - |> with_credentials(current_user.nickname, "test") - |> post("/api/pleroma/delete_account", %{"password" => "test"}) + test "with proper permissions and wrong or missing password", %{conn: conn} do + for params <- [%{"password" => "hi"}, %{}] do + ret_conn = post(conn, "/api/pleroma/delete_account", params) + + assert json_response(ret_conn, 200) == %{"error" => "Invalid password."} + end + end + + test "with proper permissions and valid password", %{conn: conn} do + conn = post(conn, "/api/pleroma/delete_account", %{"password" => "test"}) assert json_response(conn, 200) == %{"status" => "success"} - # Wait a second for the started task to end - :timer.sleep(1000) end end end From 404a9ccb9a220f3f52ee03bd69bd3746d95794cc Mon Sep 17 00:00:00 2001 From: Maxim Filippov Date: Wed, 18 Dec 2019 23:11:42 +0300 Subject: [PATCH 19/34] Stats: return status counts by scope --- CHANGELOG.md | 1 + lib/pleroma/stats.ex | 72 +++++++++++++++++-- test/stats_test.exs | 52 ++++++++++++++ .../controllers/instance_controller_test.exs | 10 ++- 4 files changed, 129 insertions(+), 6 deletions(-) create mode 100644 test/stats_test.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index c133cd9ec..f6cc193a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Breaking:** Admin API: `PUT /api/pleroma/admin/reports/:id` is now `PATCH /api/pleroma/admin/reports`, see admin_api.md for details - **Breaking:** `/api/pleroma/admin/users/invite_token` now uses `POST`, changed accepted params and returns full invite in json instead of only token string. - **Breaking** replying to reports is now "report notes", enpoint changed from `POST /api/pleroma/admin/reports/:id/respond` to `POST /api/pleroma/admin/reports/:id/notes` +- **Breaking** `/api/v1/stats` now return statuses count by scope (i.e. `all`, `public`, `unlisted`, `direct` and `private`) - Admin API: Return `total` when querying for reports - Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`) - Admin API: Return link alongside with token on password reset diff --git a/lib/pleroma/stats.ex b/lib/pleroma/stats.ex index 8154a09b7..c90e8f409 100644 --- a/lib/pleroma/stats.ex +++ b/lib/pleroma/stats.ex @@ -3,11 +3,15 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Stats do + use GenServer + import Ecto.Query + + alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User - use GenServer + require Pleroma.Constants @interval 1000 * 60 * 60 @@ -56,7 +60,7 @@ defp initial_data do %{peers: [], stats: %{}} end - defp get_stat_data do + def get_stat_data do peers = from( u in User, @@ -68,13 +72,71 @@ defp get_stat_data do domain_count = Enum.count(peers) - status_count = Repo.aggregate(User.Query.build(%{local: true}), :sum, :note_count) - user_count = Repo.aggregate(User.Query.build(%{local: true, active: true}), :count, :id) %{ peers: peers, - stats: %{domain_count: domain_count, status_count: status_count, user_count: user_count} + stats: %{domain_count: domain_count, status_count: status_count(), user_count: user_count} } end + + defp status_count do + %{ + all: get_all_statuses_count(), + public: public_statuses_query() |> Repo.aggregate(:count, :id), + unlisted: unlisted_statuses_query() |> Repo.aggregate(:count, :id), + direct: direct_statuses_query() |> Repo.aggregate(:count, :id), + private: private_statuses_query() |> Repo.aggregate(:count, :id) + } + end + + defp get_all_statuses_count do + Repo.aggregate(User.Query.build(%{local: true}), :sum, :note_count) + end + + def public_statuses_query do + from(o in Object, + where: fragment("(?)->'to' \\? ?", o.data, ^Pleroma.Constants.as_public()) + ) + end + + def unlisted_statuses_query do + from(o in Object, + where: not fragment("(?)->'to' \\? ?", o.data, ^Pleroma.Constants.as_public()), + where: fragment("(?)->'cc' \\? ?", o.data, ^Pleroma.Constants.as_public()) + ) + end + + def direct_statuses_query do + private_statuses_ids = from(p in private_statuses_query(), select: p.id) |> Repo.all() + + from(o in Object, + where: + fragment( + "? \\? 'directMessage' AND (?->>'directMessage')::boolean = true", + o.data, + o.data + ) or + (not fragment("(?)->'to' \\? ?", o.data, ^Pleroma.Constants.as_public()) and + not fragment("(?)->'cc' \\? ?", o.data, ^Pleroma.Constants.as_public()) and + o.id not in ^private_statuses_ids) + ) + end + + def private_statuses_query do + from(o in subquery(recipients_query()), + where: ilike(o.recipients, "%/followers%") + ) + end + + defp recipients_query do + from(o in Object, + select: %{ + id: o.id, + recipients: fragment("jsonb_array_elements_text((?)->'to')", o.data) + }, + where: not fragment("(?)->'to' \\? ?", o.data, ^Pleroma.Constants.as_public()), + where: not fragment("(?)->'cc' \\? ?", o.data, ^Pleroma.Constants.as_public()) + ) + end end diff --git a/test/stats_test.exs b/test/stats_test.exs new file mode 100644 index 000000000..31c2f8db3 --- /dev/null +++ b/test/stats_test.exs @@ -0,0 +1,52 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.StatsTest do + use Pleroma.DataCase + + import Pleroma.Factory + + alias Pleroma.Web.CommonAPI + + describe "statuses count" do + setup do + user = insert(:user) + other_user = insert(:user) + + CommonAPI.post(user, %{"visibility" => "public", "status" => "hey"}) + + Enum.each(0..1, fn _ -> + CommonAPI.post(user, %{ + "visibility" => "unlisted", + "status" => "hey" + }) + end) + + Enum.each(0..2, fn _ -> + CommonAPI.post(user, %{ + "visibility" => "direct", + "status" => "hey @#{other_user.nickname}" + }) + end) + + Enum.each(0..3, fn _ -> + CommonAPI.post(user, %{ + "visibility" => "private", + "status" => "hey" + }) + end) + + :ok + end + + test "it returns total number of statuses" do + data = Pleroma.Stats.get_stat_data() + + assert data.stats.status_count.public == 1 + assert data.stats.status_count.unlisted == 2 + assert data.stats.status_count.direct == 3 + assert data.stats.status_count.private == 4 + end + end +end diff --git a/test/web/mastodon_api/controllers/instance_controller_test.exs b/test/web/mastodon_api/controllers/instance_controller_test.exs index e00de6b18..7aa7c8648 100644 --- a/test/web/mastodon_api/controllers/instance_controller_test.exs +++ b/test/web/mastodon_api/controllers/instance_controller_test.exs @@ -58,7 +58,15 @@ test "get instance stats", %{conn: conn} do assert stats assert stats["user_count"] == 1 - assert stats["status_count"] == 1 + + assert stats["status_count"] == %{ + "all" => 1, + "direct" => 0, + "private" => 0, + "public" => 1, + "unlisted" => 0 + } + assert stats["domain_count"] == 2 end From 432b3067d4c62cd27e35f0b6e7bdc61da63310b9 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 19 Dec 2019 19:25:23 +0700 Subject: [PATCH 20/34] Do not crash when remote user follower and following counters are hidden --- lib/pleroma/web/activity_pub/activity_pub.ex | 40 ++++++++------------ test/web/activity_pub/activity_pub_test.exs | 38 +++++++++++++++++++ 2 files changed, 54 insertions(+), 24 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 16e6b0057..60c9e7e64 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1298,28 +1298,26 @@ defp object_to_user_data(data) do def fetch_follow_information_for_user(user) do with {:ok, following_data} <- Fetcher.fetch_and_contain_remote_object_from_id(user.following_address), - following_count when is_integer(following_count) <- following_data["totalItems"], {:ok, hide_follows} <- collection_private(following_data), {:ok, followers_data} <- Fetcher.fetch_and_contain_remote_object_from_id(user.follower_address), - followers_count when is_integer(followers_count) <- followers_data["totalItems"], {:ok, hide_followers} <- collection_private(followers_data) do {:ok, %{ hide_follows: hide_follows, - follower_count: followers_count, - following_count: following_count, + follower_count: normalize_counter(followers_data["totalItems"]), + following_count: normalize_counter(following_data["totalItems"]), hide_followers: hide_followers }} else - {:error, _} = e -> - e - - e -> - {:error, e} + {:error, _} = e -> e + e -> {:error, e} end end + defp normalize_counter(counter) when is_integer(counter), do: counter + defp normalize_counter(_), do: 0 + defp maybe_update_follow_information(data) do with {:enabled, true} <- {:enabled, Pleroma.Config.get([:instance, :external_user_synchronization])}, @@ -1339,24 +1337,18 @@ defp maybe_update_follow_information(data) do end end + defp collection_private(%{"first" => %{"type" => type}}) + when type in ["CollectionPage", "OrderedCollectionPage"], + do: {:ok, false} + defp collection_private(%{"first" => first}) do - if is_map(first) and - first["type"] in ["CollectionPage", "OrderedCollectionPage"] do + with {:ok, %{"type" => type}} when type in ["CollectionPage", "OrderedCollectionPage"] <- + Fetcher.fetch_and_contain_remote_object_from_id(first) do {:ok, false} else - with {:ok, %{"type" => type}} when type in ["CollectionPage", "OrderedCollectionPage"] <- - Fetcher.fetch_and_contain_remote_object_from_id(first) do - {:ok, false} - else - {:error, {:ok, %{status: code}}} when code in [401, 403] -> - {:ok, true} - - {:error, _} = e -> - e - - e -> - {:error, e} - end + {:error, {:ok, %{status: code}}} when code in [401, 403] -> {:ok, true} + {:error, _} = e -> e + e -> {:error, e} end end diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 1520c8a9b..ad6b9810c 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -1623,6 +1623,44 @@ test "detects hidden follows/followers for friendica" do assert follow_info.following_count == 32 assert follow_info.hide_follows == true end + + test "doesn't crash when follower and following counters are hidden" do + mock(fn env -> + case env.url do + "http://localhost:4001/users/masto_hidden_counters/following" -> + json(%{ + "@context" => "https://www.w3.org/ns/activitystreams", + "id" => "http://localhost:4001/users/masto_hidden_counters/followers" + }) + + "http://localhost:4001/users/masto_hidden_counters/following?page=1" -> + %Tesla.Env{status: 403, body: ""} + + "http://localhost:4001/users/masto_hidden_counters/followers" -> + json(%{ + "@context" => "https://www.w3.org/ns/activitystreams", + "id" => "http://localhost:4001/users/masto_hidden_counters/following" + }) + + "http://localhost:4001/users/masto_hidden_counters/followers?page=1" -> + %Tesla.Env{status: 403, body: ""} + end + end) + + user = + insert(:user, + local: false, + follower_address: "http://localhost:4001/users/masto_hidden_counters/followers", + following_address: "http://localhost:4001/users/masto_hidden_counters/following" + ) + + {:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user) + + assert follow_info.hide_followers == true + assert follow_info.follower_count == 0 + assert follow_info.hide_follows == true + assert follow_info.following_count == 0 + end end describe "fetch_favourites/3" do From 455e072d27f28c39050b2dc24b346a8f2ef30f90 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Thu, 19 Dec 2019 17:23:27 +0300 Subject: [PATCH 21/34] [#2068] Introduced proper OAuth tokens usage to controller tests. --- lib/pleroma/web/masto_fe_controller.ex | 25 +- test/support/conn_case.ex | 6 +- test/web/masto_fe_controller_test.exs | 5 +- .../update_credentials_test.exs | 216 ++++------- .../controllers/account_controller_test.exs | 362 ++++++------------ .../conversation_controller_test.exs | 45 +-- .../domain_block_controller_test.exs | 22 +- .../controllers/filter_controller_test.exs | 42 +- .../follow_request_controller_test.exs | 28 +- .../controllers/list_controller_test.exs | 88 ++--- .../controllers/media_controller_test.exs | 22 +- .../notification_controller_test.exs | 156 ++++---- .../controllers/poll_controller_test.exs | 59 +-- .../controllers/report_controller_test.exs | 20 +- .../scheduled_activity_controller_test.exs | 52 +-- .../controllers/search_controller_test.exs | 7 +- .../suggestion_controller_test.exs | 16 +- .../controllers/timeline_controller_test.exs | 92 ++--- .../mastodon_api_controller_test.exs | 74 +--- .../controllers/account_controller_test.exs | 156 +++----- .../controllers/emoji_api_controller_test.exs | 42 +- .../controllers/mascot_controller_test.exs | 41 +- .../controllers/scrobble_controller_test.exs | 17 +- 23 files changed, 548 insertions(+), 1045 deletions(-) diff --git a/lib/pleroma/web/masto_fe_controller.ex b/lib/pleroma/web/masto_fe_controller.ex index ca261ad6e..9f7e4943c 100644 --- a/lib/pleroma/web/masto_fe_controller.ex +++ b/lib/pleroma/web/masto_fe_controller.ex @@ -20,18 +20,21 @@ defmodule Pleroma.Web.MastoFEController do plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action != :index) @doc "GET /web/*path" - def index(%{assigns: %{user: user}} = conn, _params) do - token = get_session(conn, :oauth_token) + def index(%{assigns: %{user: user, token: token}} = conn, _params) + when not is_nil(user) and not is_nil(token) do + conn + |> put_layout(false) + |> render("index.html", + token: token.token, + user: user, + custom_emojis: Pleroma.Emoji.get_all() + ) + end - if user && token do - conn - |> put_layout(false) - |> render("index.html", token: token, user: user, custom_emojis: Pleroma.Emoji.get_all()) - else - conn - |> put_session(:return_to, conn.request_path) - |> redirect(to: "/web/login") - end + def index(conn, _params) do + conn + |> put_session(:return_to, conn.request_path) + |> redirect(to: "/web/login") end @doc "GET /web/manifest.json" diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 95bc2492a..22e72fc09 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -30,14 +30,14 @@ defmodule Pleroma.Web.ConnCase do @endpoint Pleroma.Web.Endpoint # Sets up OAuth access with specified scopes - defp oauth_access(scopes, opts \\ %{}) do + defp oauth_access(scopes, opts \\ []) do user = - Map.get_lazy(opts, :user, fn -> + Keyword.get_lazy(opts, :user, fn -> Pleroma.Factory.insert(:user) end) token = - Map.get_lazy(opts, :oauth_token, fn -> + Keyword.get_lazy(opts, :oauth_token, fn -> Pleroma.Factory.insert(:oauth_token, user: user, scopes: scopes) end) diff --git a/test/web/masto_fe_controller_test.exs b/test/web/masto_fe_controller_test.exs index b5dbd4a25..f9870a852 100644 --- a/test/web/masto_fe_controller_test.exs +++ b/test/web/masto_fe_controller_test.exs @@ -18,6 +18,7 @@ test "put settings", %{conn: conn} do conn = conn |> assign(:user, user) + |> assign(:token, insert(:oauth_token, user: user, scopes: ["write:accounts"])) |> put("/api/web/settings", %{"data" => %{"programming" => "socks"}}) assert _result = json_response(conn, 200) @@ -63,12 +64,12 @@ test "redirects not logged-in users to the login page on private instances", %{ end test "does not redirect logged in users to the login page", %{conn: conn, path: path} do - token = insert(:oauth_token) + token = insert(:oauth_token, scopes: ["read"]) conn = conn |> assign(:user, token.user) - |> put_session(:oauth_token, token.token) + |> assign(:token, token) |> get(path) assert conn.status == 200 diff --git a/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs b/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs index 77cfce4fa..09bdc46e0 100644 --- a/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs +++ b/test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs @@ -12,13 +12,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do clear_config([:instance, :max_account_fields]) describe "updating credentials" do - test "sets user settings in a generic way", %{conn: conn} do - user = insert(:user) + setup do: oauth_access(["write:accounts"]) + test "sets user settings in a generic way", %{conn: conn} do res_conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{ + patch(conn, "/api/v1/accounts/update_credentials", %{ "pleroma_settings_store" => %{ pleroma_fe: %{ theme: "bla" @@ -26,10 +24,10 @@ test "sets user settings in a generic way", %{conn: conn} do } }) - assert user = json_response(res_conn, 200) - assert user["pleroma"]["settings_store"] == %{"pleroma_fe" => %{"theme" => "bla"}} + assert user_data = json_response(res_conn, 200) + assert user_data["pleroma"]["settings_store"] == %{"pleroma_fe" => %{"theme" => "bla"}} - user = Repo.get(User, user["id"]) + user = Repo.get(User, user_data["id"]) res_conn = conn @@ -42,15 +40,15 @@ test "sets user settings in a generic way", %{conn: conn} do } }) - assert user = json_response(res_conn, 200) + assert user_data = json_response(res_conn, 200) - assert user["pleroma"]["settings_store"] == + assert user_data["pleroma"]["settings_store"] == %{ "pleroma_fe" => %{"theme" => "bla"}, "masto_fe" => %{"theme" => "bla"} } - user = Repo.get(User, user["id"]) + user = Repo.get(User, user_data["id"]) res_conn = conn @@ -63,9 +61,9 @@ test "sets user settings in a generic way", %{conn: conn} do } }) - assert user = json_response(res_conn, 200) + assert user_data = json_response(res_conn, 200) - assert user["pleroma"]["settings_store"] == + assert user_data["pleroma"]["settings_store"] == %{ "pleroma_fe" => %{"theme" => "bla"}, "masto_fe" => %{"theme" => "blub"} @@ -73,97 +71,67 @@ test "sets user settings in a generic way", %{conn: conn} do end test "updates the user's bio", %{conn: conn} do - user = insert(:user) user2 = insert(:user) conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{ + patch(conn, "/api/v1/accounts/update_credentials", %{ "note" => "I drink #cofe with @#{user2.nickname}" }) - assert user = json_response(conn, 200) + assert user_data = json_response(conn, 200) - assert user["note"] == + assert user_data["note"] == ~s(I drink #cofe with @#{user2.nickname}) end test "updates the user's locking status", %{conn: conn} do - user = insert(:user) + conn = patch(conn, "/api/v1/accounts/update_credentials", %{locked: "true"}) - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{locked: "true"}) - - assert user = json_response(conn, 200) - assert user["locked"] == true + assert user_data = json_response(conn, 200) + assert user_data["locked"] == true end - test "updates the user's allow_following_move", %{conn: conn} do - user = insert(:user) - + test "updates the user's allow_following_move", %{user: user, conn: conn} do assert user.allow_following_move == true - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{allow_following_move: "false"}) + conn = patch(conn, "/api/v1/accounts/update_credentials", %{allow_following_move: "false"}) assert refresh_record(user).allow_following_move == false - assert user = json_response(conn, 200) - assert user["pleroma"]["allow_following_move"] == false + assert user_data = json_response(conn, 200) + assert user_data["pleroma"]["allow_following_move"] == false end test "updates the user's default scope", %{conn: conn} do - user = insert(:user) + conn = patch(conn, "/api/v1/accounts/update_credentials", %{default_scope: "cofe"}) - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{default_scope: "cofe"}) - - assert user = json_response(conn, 200) - assert user["source"]["privacy"] == "cofe" + assert user_data = json_response(conn, 200) + assert user_data["source"]["privacy"] == "cofe" end test "updates the user's hide_followers status", %{conn: conn} do - user = insert(:user) + conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_followers: "true"}) - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{hide_followers: "true"}) - - assert user = json_response(conn, 200) - assert user["pleroma"]["hide_followers"] == true + assert user_data = json_response(conn, 200) + assert user_data["pleroma"]["hide_followers"] == true end test "updates the user's hide_followers_count and hide_follows_count", %{conn: conn} do - user = insert(:user) - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{ + patch(conn, "/api/v1/accounts/update_credentials", %{ hide_followers_count: "true", hide_follows_count: "true" }) - assert user = json_response(conn, 200) - assert user["pleroma"]["hide_followers_count"] == true - assert user["pleroma"]["hide_follows_count"] == true + assert user_data = json_response(conn, 200) + assert user_data["pleroma"]["hide_followers_count"] == true + assert user_data["pleroma"]["hide_follows_count"] == true end - test "updates the user's skip_thread_containment option", %{conn: conn} do - user = insert(:user) - + test "updates the user's skip_thread_containment option", %{user: user, conn: conn} do response = conn - |> assign(:user, user) |> patch("/api/v1/accounts/update_credentials", %{skip_thread_containment: "true"}) |> json_response(200) @@ -172,104 +140,68 @@ test "updates the user's skip_thread_containment option", %{conn: conn} do end test "updates the user's hide_follows status", %{conn: conn} do - user = insert(:user) + conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_follows: "true"}) - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{hide_follows: "true"}) - - assert user = json_response(conn, 200) - assert user["pleroma"]["hide_follows"] == true + assert user_data = json_response(conn, 200) + assert user_data["pleroma"]["hide_follows"] == true end test "updates the user's hide_favorites status", %{conn: conn} do - user = insert(:user) + conn = patch(conn, "/api/v1/accounts/update_credentials", %{hide_favorites: "true"}) - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{hide_favorites: "true"}) - - assert user = json_response(conn, 200) - assert user["pleroma"]["hide_favorites"] == true + assert user_data = json_response(conn, 200) + assert user_data["pleroma"]["hide_favorites"] == true end test "updates the user's show_role status", %{conn: conn} do - user = insert(:user) + conn = patch(conn, "/api/v1/accounts/update_credentials", %{show_role: "false"}) - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{show_role: "false"}) - - assert user = json_response(conn, 200) - assert user["source"]["pleroma"]["show_role"] == false + assert user_data = json_response(conn, 200) + assert user_data["source"]["pleroma"]["show_role"] == false end test "updates the user's no_rich_text status", %{conn: conn} do - user = insert(:user) + conn = patch(conn, "/api/v1/accounts/update_credentials", %{no_rich_text: "true"}) - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{no_rich_text: "true"}) - - assert user = json_response(conn, 200) - assert user["source"]["pleroma"]["no_rich_text"] == true + assert user_data = json_response(conn, 200) + assert user_data["source"]["pleroma"]["no_rich_text"] == true end test "updates the user's name", %{conn: conn} do - user = insert(:user) - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{"display_name" => "markorepairs"}) + patch(conn, "/api/v1/accounts/update_credentials", %{"display_name" => "markorepairs"}) - assert user = json_response(conn, 200) - assert user["display_name"] == "markorepairs" + assert user_data = json_response(conn, 200) + assert user_data["display_name"] == "markorepairs" end - test "updates the user's avatar", %{conn: conn} do - user = insert(:user) - + test "updates the user's avatar", %{user: user, conn: conn} do new_avatar = %Plug.Upload{ content_type: "image/jpg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{"avatar" => new_avatar}) + conn = patch(conn, "/api/v1/accounts/update_credentials", %{"avatar" => new_avatar}) assert user_response = json_response(conn, 200) assert user_response["avatar"] != User.avatar_url(user) end - test "updates the user's banner", %{conn: conn} do - user = insert(:user) - + test "updates the user's banner", %{user: user, conn: conn} do new_header = %Plug.Upload{ content_type: "image/jpg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{"header" => new_header}) + conn = patch(conn, "/api/v1/accounts/update_credentials", %{"header" => new_header}) assert user_response = json_response(conn, 200) assert user_response["header"] != User.banner_url(user) end test "updates the user's background", %{conn: conn} do - user = insert(:user) - new_header = %Plug.Upload{ content_type: "image/jpg", path: Path.absname("test/fixtures/image.jpg"), @@ -277,9 +209,7 @@ test "updates the user's background", %{conn: conn} do } conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{ + patch(conn, "/api/v1/accounts/update_credentials", %{ "pleroma_background_image" => new_header }) @@ -287,13 +217,13 @@ test "updates the user's background", %{conn: conn} do assert user_response["pleroma"]["background_image"] end - test "requires 'write:accounts' permission", %{conn: conn} do + test "requires 'write:accounts' permission" do token1 = insert(:oauth_token, scopes: ["read"]) token2 = insert(:oauth_token, scopes: ["write", "follow"]) for token <- [token1, token2] do conn = - conn + build_conn() |> put_req_header("authorization", "Bearer #{token.token}") |> patch("/api/v1/accounts/update_credentials", %{}) @@ -306,53 +236,44 @@ test "requires 'write:accounts' permission", %{conn: conn} do end end - test "updates profile emojos", %{conn: conn} do - user = insert(:user) - + test "updates profile emojos", %{user: user, conn: conn} do note = "*sips :blank:*" name = "I am :firefox:" - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/accounts/update_credentials", %{ + ret_conn = + patch(conn, "/api/v1/accounts/update_credentials", %{ "note" => note, "display_name" => name }) - assert json_response(conn, 200) + assert json_response(ret_conn, 200) - conn = - conn - |> get("/api/v1/accounts/#{user.id}") + conn = get(conn, "/api/v1/accounts/#{user.id}") - assert user = json_response(conn, 200) + assert user_data = json_response(conn, 200) - assert user["note"] == note - assert user["display_name"] == name - assert [%{"shortcode" => "blank"}, %{"shortcode" => "firefox"}] = user["emojis"] + assert user_data["note"] == note + assert user_data["display_name"] == name + assert [%{"shortcode" => "blank"}, %{"shortcode" => "firefox"}] = user_data["emojis"] end test "update fields", %{conn: conn} do - user = insert(:user) - fields = [ %{"name" => "foo", "value" => ""}, %{"name" => "link", "value" => "cofe.io"} ] - account = + account_data = conn - |> assign(:user, user) |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) |> json_response(200) - assert account["fields"] == [ + assert account_data["fields"] == [ %{"name" => "foo", "value" => "bar"}, %{"name" => "link", "value" => ~S(cofe.io)} ] - assert account["source"]["fields"] == [ + assert account_data["source"]["fields"] == [ %{ "name" => "foo", "value" => "" @@ -372,7 +293,6 @@ test "update fields", %{conn: conn} do account = conn |> put_req_header("content-type", "application/x-www-form-urlencoded") - |> assign(:user, user) |> patch("/api/v1/accounts/update_credentials", fields) |> json_response(200) @@ -398,7 +318,6 @@ test "update fields", %{conn: conn} do assert %{"error" => "Invalid request"} == conn - |> assign(:user, user) |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) |> json_response(403) @@ -408,7 +327,6 @@ test "update fields", %{conn: conn} do assert %{"error" => "Invalid request"} == conn - |> assign(:user, user) |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) |> json_response(403) @@ -421,7 +339,6 @@ test "update fields", %{conn: conn} do assert %{"error" => "Invalid request"} == conn - |> assign(:user, user) |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) |> json_response(403) @@ -432,7 +349,6 @@ test "update fields", %{conn: conn} do account = conn - |> assign(:user, user) |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields}) |> json_response(200) diff --git a/test/web/mastodon_api/controllers/account_controller_test.exs b/test/web/mastodon_api/controllers/account_controller_test.exs index fa08ae4df..0d4860a42 100644 --- a/test/web/mastodon_api/controllers/account_controller_test.exs +++ b/test/web/mastodon_api/controllers/account_controller_test.exs @@ -87,6 +87,7 @@ test "respects limit_to_local_content == :unauthenticated for remote user nickna conn = build_conn() |> assign(:user, reading_user) + |> assign(:token, insert(:oauth_token, user: reading_user, scopes: ["read:accounts"])) |> get("/api/v1/accounts/#{user.nickname}") Pleroma.Config.put([:instance, :limit_to_local_content], limit_to_local) @@ -144,8 +145,9 @@ test "returns 404 for internal.fetch actor", %{conn: conn} do end describe "user timelines" do - test "respects blocks", %{conn: conn} do - user_one = insert(:user) + setup do: oauth_access(["read:statuses"]) + + test "respects blocks", %{user: user_one, conn: conn} do user_two = insert(:user) user_three = insert(:user) @@ -154,46 +156,35 @@ test "respects blocks", %{conn: conn} do {:ok, activity} = CommonAPI.post(user_two, %{"status" => "User one sux0rz"}) {:ok, repeat, _} = CommonAPI.repeat(activity.id, user_three) - resp = - conn - |> get("/api/v1/accounts/#{user_two.id}/statuses") + resp = get(conn, "/api/v1/accounts/#{user_two.id}/statuses") assert [%{"id" => id}] = json_response(resp, 200) assert id == activity.id # Even a blocked user will deliver the full user timeline, there would be - # no point in looking at a blocked users timeline otherwise - resp = - conn - |> assign(:user, user_one) - |> get("/api/v1/accounts/#{user_two.id}/statuses") + # no point in looking at a blocked users timeline otherwise + resp = get(conn, "/api/v1/accounts/#{user_two.id}/statuses") assert [%{"id" => id}] = json_response(resp, 200) assert id == activity.id - resp = - conn - |> get("/api/v1/accounts/#{user_three.id}/statuses") - + # Third user's timeline includes the repeat when viewed by unauthenticated user + resp = get(build_conn(), "/api/v1/accounts/#{user_three.id}/statuses") assert [%{"id" => id}] = json_response(resp, 200) assert id == repeat.id - # When viewing a third user's timeline, the blocked users will NOT be - # shown. - resp = - conn - |> assign(:user, user_one) - |> get("/api/v1/accounts/#{user_three.id}/statuses") + # When viewing a third user's timeline, the blocked users' statuses will NOT be shown + resp = get(conn, "/api/v1/accounts/#{user_three.id}/statuses") assert [] = json_response(resp, 200) end - test "gets a users statuses", %{conn: conn} do + test "gets users statuses", %{conn: conn} do user_one = insert(:user) user_two = insert(:user) user_three = insert(:user) - {:ok, user_three} = User.follow(user_three, user_one) + {:ok, _user_three} = User.follow(user_three, user_one) {:ok, activity} = CommonAPI.post(user_one, %{"status" => "HI!!!"}) @@ -206,9 +197,7 @@ test "gets a users statuses", %{conn: conn} do {:ok, private_activity} = CommonAPI.post(user_one, %{"status" => "private", "visibility" => "private"}) - resp = - conn - |> get("/api/v1/accounts/#{user_one.id}/statuses") + resp = get(conn, "/api/v1/accounts/#{user_one.id}/statuses") assert [%{"id" => id}] = json_response(resp, 200) assert id == to_string(activity.id) @@ -216,6 +205,7 @@ test "gets a users statuses", %{conn: conn} do resp = conn |> assign(:user, user_two) + |> assign(:token, insert(:oauth_token, user: user_two, scopes: ["read:statuses"])) |> get("/api/v1/accounts/#{user_one.id}/statuses") assert [%{"id" => id_one}, %{"id" => id_two}] = json_response(resp, 200) @@ -225,6 +215,7 @@ test "gets a users statuses", %{conn: conn} do resp = conn |> assign(:user, user_three) + |> assign(:token, insert(:oauth_token, user: user_three, scopes: ["read:statuses"])) |> get("/api/v1/accounts/#{user_one.id}/statuses") assert [%{"id" => id_one}, %{"id" => id_two}] = json_response(resp, 200) @@ -236,9 +227,7 @@ test "unimplemented pinned statuses feature", %{conn: conn} do note = insert(:note_activity) user = User.get_cached_by_ap_id(note.data["actor"]) - conn = - conn - |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true") + conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?pinned=true") assert json_response(conn, 200) == [] end @@ -257,63 +246,51 @@ test "gets an users media", %{conn: conn} do {:ok, image_post} = CommonAPI.post(user, %{"status" => "cofe", "media_ids" => [media_id]}) - conn = - conn - |> get("/api/v1/accounts/#{user.id}/statuses", %{"only_media" => "true"}) + conn = get(conn, "/api/v1/accounts/#{user.id}/statuses", %{"only_media" => "true"}) assert [%{"id" => id}] = json_response(conn, 200) assert id == to_string(image_post.id) - conn = - build_conn() - |> get("/api/v1/accounts/#{user.id}/statuses", %{"only_media" => "1"}) + conn = get(build_conn(), "/api/v1/accounts/#{user.id}/statuses", %{"only_media" => "1"}) assert [%{"id" => id}] = json_response(conn, 200) assert id == to_string(image_post.id) end - test "gets a user's statuses without reblogs", %{conn: conn} do - user = insert(:user) + test "gets a user's statuses without reblogs", %{user: user, conn: conn} do {:ok, post} = CommonAPI.post(user, %{"status" => "HI!!!"}) {:ok, _, _} = CommonAPI.repeat(post.id, user) - conn = - conn - |> get("/api/v1/accounts/#{user.id}/statuses", %{"exclude_reblogs" => "true"}) + conn = get(conn, "/api/v1/accounts/#{user.id}/statuses", %{"exclude_reblogs" => "true"}) assert [%{"id" => id}] = json_response(conn, 200) assert id == to_string(post.id) - conn = - conn - |> get("/api/v1/accounts/#{user.id}/statuses", %{"exclude_reblogs" => "1"}) + conn = get(conn, "/api/v1/accounts/#{user.id}/statuses", %{"exclude_reblogs" => "1"}) assert [%{"id" => id}] = json_response(conn, 200) assert id == to_string(post.id) end - test "filters user's statuses by a hashtag", %{conn: conn} do - user = insert(:user) + test "filters user's statuses by a hashtag", %{user: user, conn: conn} do {:ok, post} = CommonAPI.post(user, %{"status" => "#hashtag"}) {:ok, _post} = CommonAPI.post(user, %{"status" => "hashtag"}) - conn = - conn - |> get("/api/v1/accounts/#{user.id}/statuses", %{"tagged" => "hashtag"}) + conn = get(conn, "/api/v1/accounts/#{user.id}/statuses", %{"tagged" => "hashtag"}) assert [%{"id" => id}] = json_response(conn, 200) assert id == to_string(post.id) end - test "the user views their own timelines and excludes direct messages", %{conn: conn} do - user = insert(:user) + test "the user views their own timelines and excludes direct messages", %{ + user: user, + conn: conn + } do {:ok, public_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"}) {:ok, _direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"}) conn = - conn - |> assign(:user, user) - |> get("/api/v1/accounts/#{user.id}/statuses", %{"exclude_visibilities" => ["direct"]}) + get(conn, "/api/v1/accounts/#{user.id}/statuses", %{"exclude_visibilities" => ["direct"]}) assert [%{"id" => id}] = json_response(conn, 200) assert id == to_string(public_activity.id) @@ -321,46 +298,42 @@ test "the user views their own timelines and excludes direct messages", %{conn: end describe "followers" do - test "getting followers", %{conn: conn} do - user = insert(:user) + setup do: oauth_access(["read:accounts"]) + + test "getting followers", %{user: user, conn: conn} do other_user = insert(:user) {:ok, user} = User.follow(user, other_user) - conn = - conn - |> get("/api/v1/accounts/#{other_user.id}/followers") + conn = get(conn, "/api/v1/accounts/#{other_user.id}/followers") assert [%{"id" => id}] = json_response(conn, 200) assert id == to_string(user.id) end - test "getting followers, hide_followers", %{conn: conn} do - user = insert(:user) + test "getting followers, hide_followers", %{user: user, conn: conn} do other_user = insert(:user, hide_followers: true) {:ok, _user} = User.follow(user, other_user) - conn = - conn - |> get("/api/v1/accounts/#{other_user.id}/followers") + conn = get(conn, "/api/v1/accounts/#{other_user.id}/followers") assert [] == json_response(conn, 200) end - test "getting followers, hide_followers, same user requesting", %{conn: conn} do + test "getting followers, hide_followers, same user requesting" do user = insert(:user) other_user = insert(:user, hide_followers: true) {:ok, _user} = User.follow(user, other_user) conn = - conn + build_conn() |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"])) |> get("/api/v1/accounts/#{other_user.id}/followers") refute [] == json_response(conn, 200) end - test "getting followers, pagination", %{conn: conn} do - user = insert(:user) + test "getting followers, pagination", %{user: user, conn: conn} do follower1 = insert(:user) follower2 = insert(:user) follower3 = insert(:user) @@ -368,29 +341,19 @@ test "getting followers, pagination", %{conn: conn} do {:ok, _} = User.follow(follower2, user) {:ok, _} = User.follow(follower3, user) - conn = - conn - |> assign(:user, user) - - res_conn = - conn - |> get("/api/v1/accounts/#{user.id}/followers?since_id=#{follower1.id}") + res_conn = get(conn, "/api/v1/accounts/#{user.id}/followers?since_id=#{follower1.id}") assert [%{"id" => id3}, %{"id" => id2}] = json_response(res_conn, 200) assert id3 == follower3.id assert id2 == follower2.id - res_conn = - conn - |> get("/api/v1/accounts/#{user.id}/followers?max_id=#{follower3.id}") + res_conn = get(conn, "/api/v1/accounts/#{user.id}/followers?max_id=#{follower3.id}") assert [%{"id" => id2}, %{"id" => id1}] = json_response(res_conn, 200) assert id2 == follower2.id assert id1 == follower1.id - res_conn = - conn - |> get("/api/v1/accounts/#{user.id}/followers?limit=1&max_id=#{follower3.id}") + res_conn = get(conn, "/api/v1/accounts/#{user.id}/followers?limit=1&max_id=#{follower3.id}") assert [%{"id" => id2}] = json_response(res_conn, 200) assert id2 == follower2.id @@ -402,46 +365,47 @@ test "getting followers, pagination", %{conn: conn} do end describe "following" do - test "getting following", %{conn: conn} do - user = insert(:user) + setup do: oauth_access(["read:accounts"]) + + test "getting following", %{user: user, conn: conn} do other_user = insert(:user) {:ok, user} = User.follow(user, other_user) - conn = - conn - |> get("/api/v1/accounts/#{user.id}/following") + conn = get(conn, "/api/v1/accounts/#{user.id}/following") assert [%{"id" => id}] = json_response(conn, 200) assert id == to_string(other_user.id) end - test "getting following, hide_follows", %{conn: conn} do + test "getting following, hide_follows, other user requesting" do user = insert(:user, hide_follows: true) other_user = insert(:user) {:ok, user} = User.follow(user, other_user) conn = - conn + build_conn() + |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"])) |> get("/api/v1/accounts/#{user.id}/following") assert [] == json_response(conn, 200) end - test "getting following, hide_follows, same user requesting", %{conn: conn} do + test "getting following, hide_follows, same user requesting" do user = insert(:user, hide_follows: true) other_user = insert(:user) {:ok, user} = User.follow(user, other_user) conn = - conn + build_conn() |> assign(:user, user) + |> assign(:token, insert(:oauth_token, user: user, scopes: ["read:accounts"])) |> get("/api/v1/accounts/#{user.id}/following") refute [] == json_response(conn, 200) end - test "getting following, pagination", %{conn: conn} do - user = insert(:user) + test "getting following, pagination", %{user: user, conn: conn} do following1 = insert(:user) following2 = insert(:user) following3 = insert(:user) @@ -449,29 +413,20 @@ test "getting following, pagination", %{conn: conn} do {:ok, _} = User.follow(user, following2) {:ok, _} = User.follow(user, following3) - conn = - conn - |> assign(:user, user) - - res_conn = - conn - |> get("/api/v1/accounts/#{user.id}/following?since_id=#{following1.id}") + res_conn = get(conn, "/api/v1/accounts/#{user.id}/following?since_id=#{following1.id}") assert [%{"id" => id3}, %{"id" => id2}] = json_response(res_conn, 200) assert id3 == following3.id assert id2 == following2.id - res_conn = - conn - |> get("/api/v1/accounts/#{user.id}/following?max_id=#{following3.id}") + res_conn = get(conn, "/api/v1/accounts/#{user.id}/following?max_id=#{following3.id}") assert [%{"id" => id2}, %{"id" => id1}] = json_response(res_conn, 200) assert id2 == following2.id assert id1 == following1.id res_conn = - conn - |> get("/api/v1/accounts/#{user.id}/following?limit=1&max_id=#{following3.id}") + get(conn, "/api/v1/accounts/#{user.id}/following?limit=1&max_id=#{following3.id}") assert [%{"id" => id2}] = json_response(res_conn, 200) assert id2 == following2.id @@ -483,82 +438,52 @@ test "getting following, pagination", %{conn: conn} do end describe "follow/unfollow" do + setup do: oauth_access(["follow"]) + test "following / unfollowing a user", %{conn: conn} do - user = insert(:user) other_user = insert(:user) - conn = - conn - |> assign(:user, user) - |> post("/api/v1/accounts/#{other_user.id}/follow") + ret_conn = post(conn, "/api/v1/accounts/#{other_user.id}/follow") - assert %{"id" => _id, "following" => true} = json_response(conn, 200) + assert %{"id" => _id, "following" => true} = json_response(ret_conn, 200) - user = User.get_cached_by_id(user.id) + ret_conn = post(conn, "/api/v1/accounts/#{other_user.id}/unfollow") - conn = - build_conn() - |> assign(:user, user) - |> post("/api/v1/accounts/#{other_user.id}/unfollow") + assert %{"id" => _id, "following" => false} = json_response(ret_conn, 200) - assert %{"id" => _id, "following" => false} = json_response(conn, 200) - - user = User.get_cached_by_id(user.id) - - conn = - build_conn() - |> assign(:user, user) - |> post("/api/v1/follows", %{"uri" => other_user.nickname}) + conn = post(conn, "/api/v1/follows", %{"uri" => other_user.nickname}) assert %{"id" => id} = json_response(conn, 200) assert id == to_string(other_user.id) end test "following without reblogs" do - follower = insert(:user) + %{conn: conn} = oauth_access(["follow", "read:statuses"]) followed = insert(:user) other_user = insert(:user) - conn = - build_conn() - |> assign(:user, follower) - |> post("/api/v1/accounts/#{followed.id}/follow?reblogs=false") + ret_conn = post(conn, "/api/v1/accounts/#{followed.id}/follow?reblogs=false") - assert %{"showing_reblogs" => false} = json_response(conn, 200) + assert %{"showing_reblogs" => false} = json_response(ret_conn, 200) {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hey"}) {:ok, reblog, _} = CommonAPI.repeat(activity.id, followed) - conn = - build_conn() - |> assign(:user, User.get_cached_by_id(follower.id)) - |> get("/api/v1/timelines/home") + ret_conn = get(conn, "/api/v1/timelines/home") - assert [] == json_response(conn, 200) + assert [] == json_response(ret_conn, 200) - conn = - build_conn() - |> assign(:user, User.get_cached_by_id(follower.id)) - |> post("/api/v1/accounts/#{followed.id}/follow?reblogs=true") + ret_conn = post(conn, "/api/v1/accounts/#{followed.id}/follow?reblogs=true") - assert %{"showing_reblogs" => true} = json_response(conn, 200) + assert %{"showing_reblogs" => true} = json_response(ret_conn, 200) - conn = - build_conn() - |> assign(:user, User.get_cached_by_id(follower.id)) - |> get("/api/v1/timelines/home") + conn = get(conn, "/api/v1/timelines/home") expected_activity_id = reblog.id assert [%{"id" => ^expected_activity_id}] = json_response(conn, 200) end - test "following / unfollowing errors" do - user = insert(:user) - - conn = - build_conn() - |> assign(:user, user) - + test "following / unfollowing errors", %{user: user, conn: conn} do # self follow conn_res = post(conn, "/api/v1/accounts/#{user.id}/follow") assert %{"error" => "Record not found"} = json_response(conn_res, 404) @@ -588,47 +513,34 @@ test "following / unfollowing errors" do end describe "mute/unmute" do + setup do: oauth_access(["write:mutes"]) + test "with notifications", %{conn: conn} do - user = insert(:user) other_user = insert(:user) - conn = - conn - |> assign(:user, user) - |> post("/api/v1/accounts/#{other_user.id}/mute") + ret_conn = post(conn, "/api/v1/accounts/#{other_user.id}/mute") - response = json_response(conn, 200) + response = json_response(ret_conn, 200) assert %{"id" => _id, "muting" => true, "muting_notifications" => true} = response - user = User.get_cached_by_id(user.id) - conn = - build_conn() - |> assign(:user, user) - |> post("/api/v1/accounts/#{other_user.id}/unmute") + conn = post(conn, "/api/v1/accounts/#{other_user.id}/unmute") response = json_response(conn, 200) assert %{"id" => _id, "muting" => false, "muting_notifications" => false} = response end test "without notifications", %{conn: conn} do - user = insert(:user) other_user = insert(:user) - conn = - conn - |> assign(:user, user) - |> post("/api/v1/accounts/#{other_user.id}/mute", %{"notifications" => "false"}) + ret_conn = + post(conn, "/api/v1/accounts/#{other_user.id}/mute", %{"notifications" => "false"}) - response = json_response(conn, 200) + response = json_response(ret_conn, 200) assert %{"id" => _id, "muting" => true, "muting_notifications" => false} = response - user = User.get_cached_by_id(user.id) - conn = - build_conn() - |> assign(:user, user) - |> post("/api/v1/accounts/#{other_user.id}/unmute") + conn = post(conn, "/api/v1/accounts/#{other_user.id}/unmute") response = json_response(conn, 200) assert %{"id" => _id, "muting" => false, "muting_notifications" => false} = response @@ -639,8 +551,9 @@ test "without notifications", %{conn: conn} do setup do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{"status" => "HI!!!"}) + %{conn: conn} = oauth_access(["read:statuses"], user: user) - [user: user, activity: activity] + [conn: conn, user: user, activity: activity] end test "returns pinned statuses", %{conn: conn, user: user, activity: activity} do @@ -648,7 +561,6 @@ test "returns pinned statuses", %{conn: conn, user: user, activity: activity} do result = conn - |> assign(:user, user) |> get("/api/v1/accounts/#{user.id}/statuses?pinned=true") |> json_response(200) @@ -658,23 +570,15 @@ test "returns pinned statuses", %{conn: conn, user: user, activity: activity} do end end - test "blocking / unblocking a user", %{conn: conn} do - user = insert(:user) + test "blocking / unblocking a user" do + %{conn: conn} = oauth_access(["follow"]) other_user = insert(:user) - conn = - conn - |> assign(:user, user) - |> post("/api/v1/accounts/#{other_user.id}/block") + ret_conn = post(conn, "/api/v1/accounts/#{other_user.id}/block") - assert %{"id" => _id, "blocking" => true} = json_response(conn, 200) + assert %{"id" => _id, "blocking" => true} = json_response(ret_conn, 200) - user = User.get_cached_by_id(user.id) - - conn = - build_conn() - |> assign(:user, user) - |> post("/api/v1/accounts/#{other_user.id}/unblock") + conn = post(conn, "/api/v1/accounts/#{other_user.id}/unblock") assert %{"id" => _id, "blocking" => false} = json_response(conn, 200) end @@ -693,8 +597,7 @@ test "blocking / unblocking a user", %{conn: conn} do test "Account registration via Application", %{conn: conn} do conn = - conn - |> post("/api/v1/apps", %{ + post(conn, "/api/v1/apps", %{ client_name: "client_name", redirect_uris: "urn:ietf:wg:oauth:2.0:oob", scopes: "read, write, follow" @@ -711,8 +614,7 @@ test "Account registration via Application", %{conn: conn} do } = json_response(conn, 200) conn = - conn - |> post("/oauth/token", %{ + post(conn, "/oauth/token", %{ grant_type: "client_credentials", client_id: client_id, client_secret: client_secret @@ -769,13 +671,13 @@ test "rate limit", %{conn: conn} do app_token = insert(:oauth_token, user: nil) conn = - put_req_header(conn, "authorization", "Bearer " <> app_token.token) + conn + |> put_req_header("authorization", "Bearer " <> app_token.token) |> Map.put(:remote_ip, {15, 15, 15, 15}) for i <- 1..5 do conn = - conn - |> post("/api/v1/accounts", %{ + post(conn, "/api/v1/accounts", %{ username: "#{i}lain", email: "#{i}lain@example.org", password: "PlzDontHackLain", @@ -798,8 +700,7 @@ test "rate limit", %{conn: conn} do end conn = - conn - |> post("/api/v1/accounts", %{ + post(conn, "/api/v1/accounts", %{ username: "6lain", email: "6lain@example.org", password: "PlzDontHackLain", @@ -815,9 +716,7 @@ test "returns bad_request if missing required params", %{ } do app_token = insert(:oauth_token, user: nil) - conn = - conn - |> put_req_header("authorization", "Bearer " <> app_token.token) + conn = put_req_header(conn, "authorization", "Bearer " <> app_token.token) res = post(conn, "/api/v1/accounts", valid_params) assert json_response(res, 200) @@ -836,9 +735,7 @@ test "returns bad_request if missing required params", %{ end test "returns forbidden if token is invalid", %{conn: conn, valid_params: valid_params} do - conn = - conn - |> put_req_header("authorization", "Bearer " <> "invalid-token") + conn = put_req_header(conn, "authorization", "Bearer " <> "invalid-token") res = post(conn, "/api/v1/accounts", valid_params) assert json_response(res, 403) == %{"error" => "Invalid credentials"} @@ -846,15 +743,14 @@ test "returns forbidden if token is invalid", %{conn: conn, valid_params: valid_ end describe "GET /api/v1/accounts/:id/lists - account_lists" do - test "returns lists to which the account belongs", %{conn: conn} do - user = insert(:user) + test "returns lists to which the account belongs" do + %{user: user, conn: conn} = oauth_access(["read:lists"]) other_user = insert(:user) assert {:ok, %Pleroma.List{} = list} = Pleroma.List.create("Test List", user) {:ok, %{following: _following}} = Pleroma.List.follow(list, other_user) res = conn - |> assign(:user, user) |> get("/api/v1/accounts/#{other_user.id}/lists") |> json_response(200) @@ -863,13 +759,9 @@ test "returns lists to which the account belongs", %{conn: conn} do end describe "verify_credentials" do - test "verify_credentials", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - |> get("/api/v1/accounts/verify_credentials") + test "verify_credentials" do + %{user: user, conn: conn} = oauth_access(["read:accounts"]) + conn = get(conn, "/api/v1/accounts/verify_credentials") response = json_response(conn, 200) @@ -878,25 +770,21 @@ test "verify_credentials", %{conn: conn} do assert id == to_string(user.id) end - test "verify_credentials default scope unlisted", %{conn: conn} do + test "verify_credentials default scope unlisted" do user = insert(:user, default_scope: "unlisted") + %{conn: conn} = oauth_access(["read:accounts"], user: user) - conn = - conn - |> assign(:user, user) - |> get("/api/v1/accounts/verify_credentials") + conn = get(conn, "/api/v1/accounts/verify_credentials") assert %{"id" => id, "source" => %{"privacy" => "unlisted"}} = json_response(conn, 200) assert id == to_string(user.id) end - test "locked accounts", %{conn: conn} do + test "locked accounts" do user = insert(:user, default_scope: "private") + %{conn: conn} = oauth_access(["read:accounts"], user: user) - conn = - conn - |> assign(:user, user) - |> get("/api/v1/accounts/verify_credentials") + conn = get(conn, "/api/v1/accounts/verify_credentials") assert %{"id" => id, "source" => %{"privacy" => "private"}} = json_response(conn, 200) assert id == to_string(user.id) @@ -904,15 +792,13 @@ test "locked accounts", %{conn: conn} do end describe "user relationships" do - test "returns the relationships for the current user", %{conn: conn} do - user = insert(:user) - other_user = insert(:user) - {:ok, user} = User.follow(user, other_user) + setup do: oauth_access(["read:follows"]) - conn = - conn - |> assign(:user, user) - |> get("/api/v1/accounts/relationships", %{"id" => [other_user.id]}) + test "returns the relationships for the current user", %{user: user, conn: conn} do + other_user = insert(:user) + {:ok, _user} = User.follow(user, other_user) + + conn = get(conn, "/api/v1/accounts/relationships", %{"id" => [other_user.id]}) assert [relationship] = json_response(conn, 200) @@ -920,34 +806,26 @@ test "returns the relationships for the current user", %{conn: conn} do end test "returns an empty list on a bad request", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - |> get("/api/v1/accounts/relationships", %{}) + conn = get(conn, "/api/v1/accounts/relationships", %{}) assert [] = json_response(conn, 200) end end - test "getting a list of mutes", %{conn: conn} do - user = insert(:user) + test "getting a list of mutes" do + %{user: user, conn: conn} = oauth_access(["read:mutes"]) other_user = insert(:user) {:ok, _user_relationships} = User.mute(user, other_user) - conn = - conn - |> assign(:user, user) - |> get("/api/v1/mutes") + conn = get(conn, "/api/v1/mutes") other_user_id = to_string(other_user.id) assert [%{"id" => ^other_user_id}] = json_response(conn, 200) end - test "getting a list of blocks", %{conn: conn} do - user = insert(:user) + test "getting a list of blocks" do + %{user: user, conn: conn} = oauth_access(["read:blocks"]) other_user = insert(:user) {:ok, _user_relationship} = User.block(user, other_user) diff --git a/test/web/mastodon_api/controllers/conversation_controller_test.exs b/test/web/mastodon_api/controllers/conversation_controller_test.exs index 2a1223b18..4bb9781a6 100644 --- a/test/web/mastodon_api/controllers/conversation_controller_test.exs +++ b/test/web/mastodon_api/controllers/conversation_controller_test.exs @@ -10,8 +10,9 @@ defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do import Pleroma.Factory - test "returns a list of conversations", %{conn: conn} do - user_one = insert(:user) + setup do: oauth_access(["read:statuses"]) + + test "returns a list of conversations", %{user: user_one, conn: conn} do user_two = insert(:user) user_three = insert(:user) @@ -33,10 +34,7 @@ test "returns a list of conversations", %{conn: conn} do "visibility" => "private" }) - res_conn = - conn - |> assign(:user, user_one) - |> get("/api/v1/conversations") + res_conn = get(conn, "/api/v1/conversations") assert response = json_response(res_conn, 200) @@ -59,8 +57,7 @@ test "returns a list of conversations", %{conn: conn} do assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0 end - test "filters conversations by recipients", %{conn: conn} do - user_one = insert(:user) + test "filters conversations by recipients", %{user: user_one, conn: conn} do user_two = insert(:user) user_three = insert(:user) @@ -96,7 +93,6 @@ test "filters conversations by recipients", %{conn: conn} do [conversation1, conversation2] = conn - |> assign(:user, user_one) |> get("/api/v1/conversations", %{"recipients" => [user_two.id]}) |> json_response(200) @@ -105,15 +101,13 @@ test "filters conversations by recipients", %{conn: conn} do [conversation1] = conn - |> assign(:user, user_one) |> get("/api/v1/conversations", %{"recipients" => [user_two.id, user_three.id]}) |> json_response(200) assert conversation1["last_status"]["id"] == direct3.id end - test "updates the last_status on reply", %{conn: conn} do - user_one = insert(:user) + test "updates the last_status on reply", %{user: user_one, conn: conn} do user_two = insert(:user) {:ok, direct} = @@ -131,15 +125,13 @@ test "updates the last_status on reply", %{conn: conn} do [%{"last_status" => res_last_status}] = conn - |> assign(:user, user_one) |> get("/api/v1/conversations") |> json_response(200) assert res_last_status["id"] == direct_reply.id end - test "the user marks a conversation as read", %{conn: conn} do - user_one = insert(:user) + test "the user marks a conversation as read", %{user: user_one, conn: conn} do user_two = insert(:user) {:ok, direct} = @@ -151,15 +143,21 @@ test "the user marks a conversation as read", %{conn: conn} do assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0 assert User.get_cached_by_id(user_two.id).unread_conversation_count == 1 - [%{"id" => direct_conversation_id, "unread" => true}] = - conn + user_two_conn = + build_conn() |> assign(:user, user_two) + |> assign( + :token, + insert(:oauth_token, user: user_two, scopes: ["read:statuses", "write:conversations"]) + ) + + [%{"id" => direct_conversation_id, "unread" => true}] = + user_two_conn |> get("/api/v1/conversations") |> json_response(200) %{"unread" => false} = - conn - |> assign(:user, user_two) + user_two_conn |> post("/api/v1/conversations/#{direct_conversation_id}/read") |> json_response(200) @@ -176,7 +174,6 @@ test "the user marks a conversation as read", %{conn: conn} do [%{"unread" => true}] = conn - |> assign(:user, user_one) |> get("/api/v1/conversations") |> json_response(200) @@ -195,8 +192,7 @@ test "the user marks a conversation as read", %{conn: conn} do assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0 end - test "(vanilla) Mastodon frontend behaviour", %{conn: conn} do - user_one = insert(:user) + test "(vanilla) Mastodon frontend behaviour", %{user: user_one, conn: conn} do user_two = insert(:user) {:ok, direct} = @@ -205,10 +201,7 @@ test "(vanilla) Mastodon frontend behaviour", %{conn: conn} do "visibility" => "direct" }) - res_conn = - conn - |> assign(:user, user_one) - |> get("/api/v1/statuses/#{direct.id}/context") + res_conn = get(conn, "/api/v1/statuses/#{direct.id}/context") assert %{"ancestors" => [], "descendants" => []} == json_response(res_conn, 200) end diff --git a/test/web/mastodon_api/controllers/domain_block_controller_test.exs b/test/web/mastodon_api/controllers/domain_block_controller_test.exs index 25a279cdc..55de625ba 100644 --- a/test/web/mastodon_api/controllers/domain_block_controller_test.exs +++ b/test/web/mastodon_api/controllers/domain_block_controller_test.exs @@ -9,31 +9,25 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockControllerTest do import Pleroma.Factory - test "blocking / unblocking a domain", %{conn: conn} do - user = insert(:user) + test "blocking / unblocking a domain" do + %{user: user, conn: conn} = oauth_access(["write:blocks"]) other_user = insert(:user, %{ap_id: "https://dogwhistle.zone/@pundit"}) - conn = - conn - |> assign(:user, user) - |> post("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"}) + ret_conn = post(conn, "/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"}) - assert %{} = json_response(conn, 200) + assert %{} = json_response(ret_conn, 200) user = User.get_cached_by_ap_id(user.ap_id) assert User.blocks?(user, other_user) - conn = - build_conn() - |> assign(:user, user) - |> delete("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"}) + ret_conn = delete(conn, "/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"}) - assert %{} = json_response(conn, 200) + assert %{} = json_response(ret_conn, 200) user = User.get_cached_by_ap_id(user.ap_id) refute User.blocks?(user, other_user) end - test "getting a list of domain blocks", %{conn: conn} do - user = insert(:user) + test "getting a list of domain blocks" do + %{user: user, conn: conn} = oauth_access(["read:blocks"]) {:ok, user} = User.block_domain(user, "bad.site") {:ok, user} = User.block_domain(user, "even.worse.site") diff --git a/test/web/mastodon_api/controllers/filter_controller_test.exs b/test/web/mastodon_api/controllers/filter_controller_test.exs index 550689788..3aea17ec7 100644 --- a/test/web/mastodon_api/controllers/filter_controller_test.exs +++ b/test/web/mastodon_api/controllers/filter_controller_test.exs @@ -7,20 +7,15 @@ defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do alias Pleroma.Web.MastodonAPI.FilterView - import Pleroma.Factory - - test "creating a filter", %{conn: conn} do - user = insert(:user) + test "creating a filter" do + %{conn: conn} = oauth_access(["write:filters"]) filter = %Pleroma.Filter{ phrase: "knights", context: ["home"] } - conn = - conn - |> assign(:user, user) - |> post("/api/v1/filters", %{"phrase" => filter.phrase, context: filter.context}) + conn = post(conn, "/api/v1/filters", %{"phrase" => filter.phrase, context: filter.context}) assert response = json_response(conn, 200) assert response["phrase"] == filter.phrase @@ -30,8 +25,8 @@ test "creating a filter", %{conn: conn} do assert response["id"] != "" end - test "fetching a list of filters", %{conn: conn} do - user = insert(:user) + test "fetching a list of filters" do + %{user: user, conn: conn} = oauth_access(["read:filters"]) query_one = %Pleroma.Filter{ user_id: user.id, @@ -52,7 +47,6 @@ test "fetching a list of filters", %{conn: conn} do response = conn - |> assign(:user, user) |> get("/api/v1/filters") |> json_response(200) @@ -64,8 +58,8 @@ test "fetching a list of filters", %{conn: conn} do ) end - test "get a filter", %{conn: conn} do - user = insert(:user) + test "get a filter" do + %{user: user, conn: conn} = oauth_access(["read:filters"]) query = %Pleroma.Filter{ user_id: user.id, @@ -76,16 +70,13 @@ test "get a filter", %{conn: conn} do {:ok, filter} = Pleroma.Filter.create(query) - conn = - conn - |> assign(:user, user) - |> get("/api/v1/filters/#{filter.filter_id}") + conn = get(conn, "/api/v1/filters/#{filter.filter_id}") assert _response = json_response(conn, 200) end - test "update a filter", %{conn: conn} do - user = insert(:user) + test "update a filter" do + %{user: user, conn: conn} = oauth_access(["write:filters"]) query = %Pleroma.Filter{ user_id: user.id, @@ -102,9 +93,7 @@ test "update a filter", %{conn: conn} do } conn = - conn - |> assign(:user, user) - |> put("/api/v1/filters/#{query.filter_id}", %{ + put(conn, "/api/v1/filters/#{query.filter_id}", %{ phrase: new.phrase, context: new.context }) @@ -114,8 +103,8 @@ test "update a filter", %{conn: conn} do assert response["context"] == new.context end - test "delete a filter", %{conn: conn} do - user = insert(:user) + test "delete a filter" do + %{user: user, conn: conn} = oauth_access(["write:filters"]) query = %Pleroma.Filter{ user_id: user.id, @@ -126,10 +115,7 @@ test "delete a filter", %{conn: conn} do {:ok, filter} = Pleroma.Filter.create(query) - conn = - conn - |> assign(:user, user) - |> delete("/api/v1/filters/#{filter.filter_id}") + conn = delete(conn, "/api/v1/filters/#{filter.filter_id}") assert response = json_response(conn, 200) assert response == %{} diff --git a/test/web/mastodon_api/controllers/follow_request_controller_test.exs b/test/web/mastodon_api/controllers/follow_request_controller_test.exs index 288cd9029..6e4a76501 100644 --- a/test/web/mastodon_api/controllers/follow_request_controller_test.exs +++ b/test/web/mastodon_api/controllers/follow_request_controller_test.exs @@ -11,8 +11,13 @@ defmodule Pleroma.Web.MastodonAPI.FollowRequestControllerTest do import Pleroma.Factory describe "locked accounts" do - test "/api/v1/follow_requests works" do + setup do user = insert(:user, locked: true) + %{conn: conn} = oauth_access(["follow"], user: user) + %{user: user, conn: conn} + end + + test "/api/v1/follow_requests works", %{user: user, conn: conn} do other_user = insert(:user) {:ok, _activity} = ActivityPub.follow(other_user, user) @@ -20,17 +25,13 @@ test "/api/v1/follow_requests works" do assert User.following?(other_user, user) == false - conn = - build_conn() - |> assign(:user, user) - |> get("/api/v1/follow_requests") + conn = get(conn, "/api/v1/follow_requests") assert [relationship] = json_response(conn, 200) assert to_string(other_user.id) == relationship["id"] end - test "/api/v1/follow_requests/:id/authorize works" do - user = insert(:user, locked: true) + test "/api/v1/follow_requests/:id/authorize works", %{user: user, conn: conn} do other_user = insert(:user) {:ok, _activity} = ActivityPub.follow(other_user, user) @@ -41,10 +42,7 @@ test "/api/v1/follow_requests/:id/authorize works" do assert User.following?(other_user, user) == false - conn = - build_conn() - |> assign(:user, user) - |> post("/api/v1/follow_requests/#{other_user.id}/authorize") + conn = post(conn, "/api/v1/follow_requests/#{other_user.id}/authorize") assert relationship = json_response(conn, 200) assert to_string(other_user.id) == relationship["id"] @@ -55,18 +53,14 @@ test "/api/v1/follow_requests/:id/authorize works" do assert User.following?(other_user, user) == true end - test "/api/v1/follow_requests/:id/reject works" do - user = insert(:user, locked: true) + test "/api/v1/follow_requests/:id/reject works", %{user: user, conn: conn} do other_user = insert(:user) {:ok, _activity} = ActivityPub.follow(other_user, user) user = User.get_cached_by_id(user.id) - conn = - build_conn() - |> assign(:user, user) - |> post("/api/v1/follow_requests/#{other_user.id}/reject") + conn = post(conn, "/api/v1/follow_requests/#{other_user.id}/reject") assert relationship = json_response(conn, 200) assert to_string(other_user.id) == relationship["id"] diff --git a/test/web/mastodon_api/controllers/list_controller_test.exs b/test/web/mastodon_api/controllers/list_controller_test.exs index 093506309..a6effbb69 100644 --- a/test/web/mastodon_api/controllers/list_controller_test.exs +++ b/test/web/mastodon_api/controllers/list_controller_test.exs @@ -9,44 +9,35 @@ defmodule Pleroma.Web.MastodonAPI.ListControllerTest do import Pleroma.Factory - test "creating a list", %{conn: conn} do - user = insert(:user) + test "creating a list" do + %{conn: conn} = oauth_access(["write:lists"]) - conn = - conn - |> assign(:user, user) - |> post("/api/v1/lists", %{"title" => "cuties"}) + conn = post(conn, "/api/v1/lists", %{"title" => "cuties"}) assert %{"title" => title} = json_response(conn, 200) assert title == "cuties" end - test "renders error for invalid params", %{conn: conn} do - user = insert(:user) + test "renders error for invalid params" do + %{conn: conn} = oauth_access(["write:lists"]) - conn = - conn - |> assign(:user, user) - |> post("/api/v1/lists", %{"title" => nil}) + conn = post(conn, "/api/v1/lists", %{"title" => nil}) assert %{"error" => "can't be blank"} == json_response(conn, :unprocessable_entity) end - test "listing a user's lists", %{conn: conn} do - user = insert(:user) + test "listing a user's lists" do + %{conn: conn} = oauth_access(["read:lists", "write:lists"]) conn - |> assign(:user, user) |> post("/api/v1/lists", %{"title" => "cuties"}) + |> json_response(:ok) conn - |> assign(:user, user) |> post("/api/v1/lists", %{"title" => "cofe"}) + |> json_response(:ok) - conn = - conn - |> assign(:user, user) - |> get("/api/v1/lists") + conn = get(conn, "/api/v1/lists") assert [ %{"id" => _, "title" => "cofe"}, @@ -54,41 +45,35 @@ test "listing a user's lists", %{conn: conn} do ] = json_response(conn, :ok) end - test "adding users to a list", %{conn: conn} do - user = insert(:user) + test "adding users to a list" do + %{user: user, conn: conn} = oauth_access(["write:lists"]) other_user = insert(:user) {:ok, list} = Pleroma.List.create("name", user) - conn = - conn - |> assign(:user, user) - |> post("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) + conn = post(conn, "/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) assert %{} == json_response(conn, 200) %Pleroma.List{following: following} = Pleroma.List.get(list.id, user) assert following == [other_user.follower_address] end - test "removing users from a list", %{conn: conn} do - user = insert(:user) + test "removing users from a list" do + %{user: user, conn: conn} = oauth_access(["write:lists"]) other_user = insert(:user) third_user = insert(:user) {:ok, list} = Pleroma.List.create("name", user) {:ok, list} = Pleroma.List.follow(list, other_user) {:ok, list} = Pleroma.List.follow(list, third_user) - conn = - conn - |> assign(:user, user) - |> delete("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) + conn = delete(conn, "/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]}) assert %{} == json_response(conn, 200) %Pleroma.List{following: following} = Pleroma.List.get(list.id, user) assert following == [third_user.follower_address] end - test "listing users in a list", %{conn: conn} do - user = insert(:user) + test "listing users in a list" do + %{user: user, conn: conn} = oauth_access(["read:lists"]) other_user = insert(:user) {:ok, list} = Pleroma.List.create("name", user) {:ok, list} = Pleroma.List.follow(list, other_user) @@ -102,8 +87,8 @@ test "listing users in a list", %{conn: conn} do assert id == to_string(other_user.id) end - test "retrieving a list", %{conn: conn} do - user = insert(:user) + test "retrieving a list" do + %{user: user, conn: conn} = oauth_access(["read:lists"]) {:ok, list} = Pleroma.List.create("name", user) conn = @@ -115,32 +100,26 @@ test "retrieving a list", %{conn: conn} do assert id == to_string(list.id) end - test "renders 404 if list is not found", %{conn: conn} do - user = insert(:user) + test "renders 404 if list is not found" do + %{conn: conn} = oauth_access(["read:lists"]) - conn = - conn - |> assign(:user, user) - |> get("/api/v1/lists/666") + conn = get(conn, "/api/v1/lists/666") assert %{"error" => "List not found"} = json_response(conn, :not_found) end - test "renaming a list", %{conn: conn} do - user = insert(:user) + test "renaming a list" do + %{user: user, conn: conn} = oauth_access(["write:lists"]) {:ok, list} = Pleroma.List.create("name", user) - conn = - conn - |> assign(:user, user) - |> put("/api/v1/lists/#{list.id}", %{"title" => "newname"}) + conn = put(conn, "/api/v1/lists/#{list.id}", %{"title" => "newname"}) assert %{"title" => name} = json_response(conn, 200) assert name == "newname" end - test "validates title when renaming a list", %{conn: conn} do - user = insert(:user) + test "validates title when renaming a list" do + %{user: user, conn: conn} = oauth_access(["write:lists"]) {:ok, list} = Pleroma.List.create("name", user) conn = @@ -151,14 +130,11 @@ test "validates title when renaming a list", %{conn: conn} do assert %{"error" => "can't be blank"} == json_response(conn, :unprocessable_entity) end - test "deleting a list", %{conn: conn} do - user = insert(:user) + test "deleting a list" do + %{user: user, conn: conn} = oauth_access(["write:lists"]) {:ok, list} = Pleroma.List.create("name", user) - conn = - conn - |> assign(:user, user) - |> delete("/api/v1/lists/#{list.id}") + conn = delete(conn, "/api/v1/lists/#{list.id}") assert %{} = json_response(conn, 200) assert is_nil(Repo.get(Pleroma.List, list.id)) diff --git a/test/web/mastodon_api/controllers/media_controller_test.exs b/test/web/mastodon_api/controllers/media_controller_test.exs index 06c6a1cb3..042511ca4 100644 --- a/test/web/mastodon_api/controllers/media_controller_test.exs +++ b/test/web/mastodon_api/controllers/media_controller_test.exs @@ -9,23 +9,17 @@ defmodule Pleroma.Web.MastodonAPI.MediaControllerTest do alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub - import Pleroma.Factory + setup do: oauth_access(["write:media"]) describe "media upload" do setup do - user = insert(:user) - - conn = - build_conn() - |> assign(:user, user) - image = %Plug.Upload{ content_type: "image/jpg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg" } - [conn: conn, image: image] + [image: image] end clear_config([:media_proxy]) @@ -49,9 +43,7 @@ test "returns uploaded image", %{conn: conn, image: image} do end describe "PUT /api/v1/media/:id" do - setup do - actor = insert(:user) - + setup %{user: actor} do file = %Plug.Upload{ content_type: "image/jpg", path: Path.absname("test/fixtures/image.jpg"), @@ -65,13 +57,12 @@ test "returns uploaded image", %{conn: conn, image: image} do description: "test-m" ) - [actor: actor, object: object] + [object: object] end - test "updates name of media", %{conn: conn, actor: actor, object: object} do + test "updates name of media", %{conn: conn, object: object} do media = conn - |> assign(:user, actor) |> put("/api/v1/media/#{object.id}", %{"description" => "test-media"}) |> json_response(:ok) @@ -79,10 +70,9 @@ test "updates name of media", %{conn: conn, actor: actor, object: object} do assert refresh_record(object).data["name"] == "test-media" end - test "returns error wheb request is bad", %{conn: conn, actor: actor, object: object} do + test "returns error when request is bad", %{conn: conn, object: object} do media = conn - |> assign(:user, actor) |> put("/api/v1/media/#{object.id}", %{}) |> json_response(400) diff --git a/test/web/mastodon_api/controllers/notification_controller_test.exs b/test/web/mastodon_api/controllers/notification_controller_test.exs index 6635ea7a2..86303f92f 100644 --- a/test/web/mastodon_api/controllers/notification_controller_test.exs +++ b/test/web/mastodon_api/controllers/notification_controller_test.exs @@ -12,8 +12,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationControllerTest do import Pleroma.Factory - test "list of notifications", %{conn: conn} do - user = insert(:user) + test "list of notifications" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) other_user = insert(:user) {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"}) @@ -34,18 +34,15 @@ test "list of notifications", %{conn: conn} do assert response == expected_response end - test "getting a single notification", %{conn: conn} do - user = insert(:user) + test "getting a single notification" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) other_user = insert(:user) {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"}) {:ok, [notification]} = Notification.create_notifications(activity) - conn = - conn - |> assign(:user, user) - |> get("/api/v1/notifications/#{notification.id}") + conn = get(conn, "/api/v1/notifications/#{notification.id}") expected_response = "hi "hi @#{user.nickname}"}) @@ -72,32 +69,26 @@ test "dismissing a single notification", %{conn: conn} do assert %{} = json_response(conn, 200) end - test "clearing all notifications", %{conn: conn} do - user = insert(:user) + test "clearing all notifications" do + %{user: user, conn: conn} = oauth_access(["write:notifications", "read:notifications"]) other_user = insert(:user) {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"}) {:ok, [_notification]} = Notification.create_notifications(activity) - conn = - conn - |> assign(:user, user) - |> post("/api/v1/notifications/clear") + ret_conn = post(conn, "/api/v1/notifications/clear") - assert %{} = json_response(conn, 200) + assert %{} = json_response(ret_conn, 200) - conn = - build_conn() - |> assign(:user, user) - |> get("/api/v1/notifications") + ret_conn = get(conn, "/api/v1/notifications") - assert all = json_response(conn, 200) + assert all = json_response(ret_conn, 200) assert all == [] end - test "paginates notifications using min_id, since_id, max_id, and limit", %{conn: conn} do - user = insert(:user) + test "paginates notifications using min_id, since_id, max_id, and limit" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) other_user = insert(:user) {:ok, activity1} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"}) @@ -138,8 +129,8 @@ test "paginates notifications using min_id, since_id, max_id, and limit", %{conn end describe "exclude_visibilities" do - test "filters notifications for mentions", %{conn: conn} do - user = insert(:user) + test "filters notifications for mentions" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) other_user = insert(:user) {:ok, public_activity} = @@ -154,8 +145,6 @@ test "filters notifications for mentions", %{conn: conn} do {:ok, private_activity} = CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "private"}) - conn = assign(conn, :user, user) - conn_res = get(conn, "/api/v1/notifications", %{ exclude_visibilities: ["public", "unlisted", "private"] @@ -189,9 +178,9 @@ test "filters notifications for mentions", %{conn: conn} do assert id == public_activity.id end - test "filters notifications for Like activities", %{conn: conn} do + test "filters notifications for Like activities" do user = insert(:user) - other_user = insert(:user) + %{user: other_user, conn: conn} = oauth_access(["read:notifications"]) {:ok, public_activity} = CommonAPI.post(other_user, %{"status" => ".", "visibility" => "public"}) @@ -212,7 +201,6 @@ test "filters notifications for Like activities", %{conn: conn} do activity_ids = conn - |> assign(:user, other_user) |> get("/api/v1/notifications", %{exclude_visibilities: ["direct"]}) |> json_response(200) |> Enum.map(& &1["status"]["id"]) @@ -224,7 +212,6 @@ test "filters notifications for Like activities", %{conn: conn} do activity_ids = conn - |> assign(:user, other_user) |> get("/api/v1/notifications", %{exclude_visibilities: ["unlisted"]}) |> json_response(200) |> Enum.map(& &1["status"]["id"]) @@ -236,7 +223,6 @@ test "filters notifications for Like activities", %{conn: conn} do activity_ids = conn - |> assign(:user, other_user) |> get("/api/v1/notifications", %{exclude_visibilities: ["private"]}) |> json_response(200) |> Enum.map(& &1["status"]["id"]) @@ -248,7 +234,6 @@ test "filters notifications for Like activities", %{conn: conn} do activity_ids = conn - |> assign(:user, other_user) |> get("/api/v1/notifications", %{exclude_visibilities: ["public"]}) |> json_response(200) |> Enum.map(& &1["status"]["id"]) @@ -259,9 +244,9 @@ test "filters notifications for Like activities", %{conn: conn} do assert direct_activity.id in activity_ids end - test "filters notifications for Announce activities", %{conn: conn} do + test "filters notifications for Announce activities" do user = insert(:user) - other_user = insert(:user) + %{user: other_user, conn: conn} = oauth_access(["read:notifications"]) {:ok, public_activity} = CommonAPI.post(other_user, %{"status" => ".", "visibility" => "public"}) @@ -274,7 +259,6 @@ test "filters notifications for Announce activities", %{conn: conn} do activity_ids = conn - |> assign(:user, other_user) |> get("/api/v1/notifications", %{exclude_visibilities: ["unlisted"]}) |> json_response(200) |> Enum.map(& &1["status"]["id"]) @@ -284,8 +268,8 @@ test "filters notifications for Announce activities", %{conn: conn} do end end - test "filters notifications using exclude_types", %{conn: conn} do - user = insert(:user) + test "filters notifications using exclude_types" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) other_user = insert(:user) {:ok, mention_activity} = CommonAPI.post(other_user, %{"status" => "hey @#{user.nickname}"}) @@ -299,8 +283,6 @@ test "filters notifications using exclude_types", %{conn: conn} do reblog_notification_id = get_notification_id_by_activity(reblog_activity) follow_notification_id = get_notification_id_by_activity(follow_activity) - conn = assign(conn, :user, user) - conn_res = get(conn, "/api/v1/notifications", %{exclude_types: ["mention", "favourite", "reblog"]}) @@ -322,8 +304,8 @@ test "filters notifications using exclude_types", %{conn: conn} do assert [%{"id" => ^reblog_notification_id}] = json_response(conn_res, 200) end - test "destroy multiple", %{conn: conn} do - user = insert(:user) + test "destroy multiple" do + %{user: user, conn: conn} = oauth_access(["read:notifications", "write:notifications"]) other_user = insert(:user) {:ok, activity1} = CommonAPI.post(other_user, %{"status" => "hi @#{user.nickname}"}) @@ -336,8 +318,6 @@ test "destroy multiple", %{conn: conn} do notification3_id = get_notification_id_by_activity(activity3) notification4_id = get_notification_id_by_activity(activity4) - conn = assign(conn, :user, user) - result = conn |> get("/api/v1/notifications") @@ -348,6 +328,7 @@ test "destroy multiple", %{conn: conn} do conn2 = conn |> assign(:user, other_user) + |> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:notifications"])) result = conn2 @@ -372,97 +353,110 @@ test "destroy multiple", %{conn: conn} do assert [%{"id" => ^notification4_id}, %{"id" => ^notification3_id}] = result end - test "doesn't see notifications after muting user with notifications", %{conn: conn} do - user = insert(:user) + test "doesn't see notifications after muting user with notifications" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) user2 = insert(:user) {:ok, _, _, _} = CommonAPI.follow(user, user2) {:ok, _} = CommonAPI.post(user2, %{"status" => "hey @#{user.nickname}"}) - conn = assign(conn, :user, user) + ret_conn = get(conn, "/api/v1/notifications") - conn = get(conn, "/api/v1/notifications") - - assert length(json_response(conn, 200)) == 1 + assert length(json_response(ret_conn, 200)) == 1 {:ok, _user_relationships} = User.mute(user, user2) - conn = assign(build_conn(), :user, user) conn = get(conn, "/api/v1/notifications") assert json_response(conn, 200) == [] end - test "see notifications after muting user without notifications", %{conn: conn} do - user = insert(:user) + test "see notifications after muting user without notifications" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) user2 = insert(:user) {:ok, _, _, _} = CommonAPI.follow(user, user2) {:ok, _} = CommonAPI.post(user2, %{"status" => "hey @#{user.nickname}"}) - conn = assign(conn, :user, user) + ret_conn = get(conn, "/api/v1/notifications") - conn = get(conn, "/api/v1/notifications") - - assert length(json_response(conn, 200)) == 1 + assert length(json_response(ret_conn, 200)) == 1 {:ok, _user_relationships} = User.mute(user, user2, false) - conn = assign(build_conn(), :user, user) conn = get(conn, "/api/v1/notifications") assert length(json_response(conn, 200)) == 1 end - test "see notifications after muting user with notifications and with_muted parameter", %{ - conn: conn - } do - user = insert(:user) + test "see notifications after muting user with notifications and with_muted parameter" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) user2 = insert(:user) {:ok, _, _, _} = CommonAPI.follow(user, user2) {:ok, _} = CommonAPI.post(user2, %{"status" => "hey @#{user.nickname}"}) - conn = assign(conn, :user, user) + ret_conn = get(conn, "/api/v1/notifications") - conn = get(conn, "/api/v1/notifications") - - assert length(json_response(conn, 200)) == 1 + assert length(json_response(ret_conn, 200)) == 1 {:ok, _user_relationships} = User.mute(user, user2) - conn = assign(build_conn(), :user, user) conn = get(conn, "/api/v1/notifications", %{"with_muted" => "true"}) assert length(json_response(conn, 200)) == 1 end - test "see move notifications with `with_move` parameter", %{ - conn: conn - } do + test "see move notifications with `with_move` parameter" do old_user = insert(:user) new_user = insert(:user, also_known_as: [old_user.ap_id]) - follower = insert(:user) + %{user: follower, conn: conn} = oauth_access(["read:notifications"]) User.follow(follower, old_user) Pleroma.Web.ActivityPub.ActivityPub.move(old_user, new_user) Pleroma.Tests.ObanHelpers.perform_all() - conn = - conn - |> assign(:user, follower) - |> get("/api/v1/notifications") + ret_conn = get(conn, "/api/v1/notifications") - assert json_response(conn, 200) == [] + assert json_response(ret_conn, 200) == [] - conn = - build_conn() - |> assign(:user, follower) - |> get("/api/v1/notifications", %{"with_move" => "true"}) + conn = get(conn, "/api/v1/notifications", %{"with_move" => "true"}) assert length(json_response(conn, 200)) == 1 end + describe "link headers" do + test "preserves parameters in link headers" do + %{user: user, conn: conn} = oauth_access(["read:notifications"]) + other_user = insert(:user) + + {:ok, activity1} = + CommonAPI.post(other_user, %{ + "status" => "hi @#{user.nickname}", + "visibility" => "public" + }) + + {:ok, activity2} = + CommonAPI.post(other_user, %{ + "status" => "hi @#{user.nickname}", + "visibility" => "public" + }) + + notification1 = Repo.get_by(Notification, activity_id: activity1.id) + notification2 = Repo.get_by(Notification, activity_id: activity2.id) + + conn = + conn + |> assign(:user, user) + |> get("/api/v1/notifications", %{media_only: true}) + + assert [link_header] = get_resp_header(conn, "link") + assert link_header =~ ~r/media_only=true/ + assert link_header =~ ~r/min_id=#{notification2.id}/ + assert link_header =~ ~r/max_id=#{notification1.id}/ + end + end + defp get_notification_id_by_activity(%{id: id}) do Notification |> Repo.get_by(activity_id: id) diff --git a/test/web/mastodon_api/controllers/poll_controller_test.exs b/test/web/mastodon_api/controllers/poll_controller_test.exs index 40cf3e879..5a1cea11b 100644 --- a/test/web/mastodon_api/controllers/poll_controller_test.exs +++ b/test/web/mastodon_api/controllers/poll_controller_test.exs @@ -11,9 +11,9 @@ defmodule Pleroma.Web.MastodonAPI.PollControllerTest do import Pleroma.Factory describe "GET /api/v1/polls/:id" do - test "returns poll entity for object id", %{conn: conn} do - user = insert(:user) + setup do: oauth_access(["read:statuses"]) + test "returns poll entity for object id", %{user: user, conn: conn} do {:ok, activity} = CommonAPI.post(user, %{ "status" => "Pleroma does", @@ -22,10 +22,7 @@ test "returns poll entity for object id", %{conn: conn} do object = Object.normalize(activity) - conn = - conn - |> assign(:user, user) - |> get("/api/v1/polls/#{object.id}") + conn = get(conn, "/api/v1/polls/#{object.id}") response = json_response(conn, 200) id = to_string(object.id) @@ -33,11 +30,10 @@ test "returns poll entity for object id", %{conn: conn} do end test "does not expose polls for private statuses", %{conn: conn} do - user = insert(:user) other_user = insert(:user) {:ok, activity} = - CommonAPI.post(user, %{ + CommonAPI.post(other_user, %{ "status" => "Pleroma does", "poll" => %{"options" => ["what Mastodon't", "n't what Mastodoes"], "expires_in" => 20}, "visibility" => "private" @@ -45,22 +41,20 @@ test "does not expose polls for private statuses", %{conn: conn} do object = Object.normalize(activity) - conn = - conn - |> assign(:user, other_user) - |> get("/api/v1/polls/#{object.id}") + conn = get(conn, "/api/v1/polls/#{object.id}") assert json_response(conn, 404) end end describe "POST /api/v1/polls/:id/votes" do + setup do: oauth_access(["write:statuses"]) + test "votes are added to the poll", %{conn: conn} do - user = insert(:user) other_user = insert(:user) {:ok, activity} = - CommonAPI.post(user, %{ + CommonAPI.post(other_user, %{ "status" => "A very delicious sandwich", "poll" => %{ "options" => ["Lettuce", "Grilled Bacon", "Tomato"], @@ -71,10 +65,7 @@ test "votes are added to the poll", %{conn: conn} do object = Object.normalize(activity) - conn = - conn - |> assign(:user, other_user) - |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1, 2]}) + conn = post(conn, "/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1, 2]}) assert json_response(conn, 200) object = Object.get_by_id(object.id) @@ -84,9 +75,7 @@ test "votes are added to the poll", %{conn: conn} do end) end - test "author can't vote", %{conn: conn} do - user = insert(:user) - + test "author can't vote", %{user: user, conn: conn} do {:ok, activity} = CommonAPI.post(user, %{ "status" => "Am I cute?", @@ -96,7 +85,6 @@ test "author can't vote", %{conn: conn} do object = Object.normalize(activity) assert conn - |> assign(:user, user) |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [1]}) |> json_response(422) == %{"error" => "Poll's author can't vote"} @@ -106,11 +94,10 @@ test "author can't vote", %{conn: conn} do end test "does not allow multiple choices on a single-choice question", %{conn: conn} do - user = insert(:user) other_user = insert(:user) {:ok, activity} = - CommonAPI.post(user, %{ + CommonAPI.post(other_user, %{ "status" => "The glass is", "poll" => %{"options" => ["half empty", "half full"], "expires_in" => 20} }) @@ -118,7 +105,6 @@ test "does not allow multiple choices on a single-choice question", %{conn: conn object = Object.normalize(activity) assert conn - |> assign(:user, other_user) |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1]}) |> json_response(422) == %{"error" => "Too many choices"} @@ -130,42 +116,32 @@ test "does not allow multiple choices on a single-choice question", %{conn: conn end test "does not allow choice index to be greater than options count", %{conn: conn} do - user = insert(:user) other_user = insert(:user) {:ok, activity} = - CommonAPI.post(user, %{ + CommonAPI.post(other_user, %{ "status" => "Am I cute?", "poll" => %{"options" => ["Yes", "No"], "expires_in" => 20} }) object = Object.normalize(activity) - conn = - conn - |> assign(:user, other_user) - |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [2]}) + conn = post(conn, "/api/v1/polls/#{object.id}/votes", %{"choices" => [2]}) assert json_response(conn, 422) == %{"error" => "Invalid indices"} end test "returns 404 error when object is not exist", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - |> post("/api/v1/polls/1/votes", %{"choices" => [0]}) + conn = post(conn, "/api/v1/polls/1/votes", %{"choices" => [0]}) assert json_response(conn, 404) == %{"error" => "Record not found"} end test "returns 404 when poll is private and not available for user", %{conn: conn} do - user = insert(:user) other_user = insert(:user) {:ok, activity} = - CommonAPI.post(user, %{ + CommonAPI.post(other_user, %{ "status" => "Am I cute?", "poll" => %{"options" => ["Yes", "No"], "expires_in" => 20}, "visibility" => "private" @@ -173,10 +149,7 @@ test "returns 404 when poll is private and not available for user", %{conn: conn object = Object.normalize(activity) - conn = - conn - |> assign(:user, other_user) - |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0]}) + conn = post(conn, "/api/v1/polls/#{object.id}/votes", %{"choices" => [0]}) assert json_response(conn, 404) == %{"error" => "Record not found"} end diff --git a/test/web/mastodon_api/controllers/report_controller_test.exs b/test/web/mastodon_api/controllers/report_controller_test.exs index 979ca48f3..53c132ff4 100644 --- a/test/web/mastodon_api/controllers/report_controller_test.exs +++ b/test/web/mastodon_api/controllers/report_controller_test.exs @@ -9,32 +9,30 @@ defmodule Pleroma.Web.MastodonAPI.ReportControllerTest do import Pleroma.Factory + setup do: oauth_access(["write:reports"]) + setup do - reporter = insert(:user) target_user = insert(:user) {:ok, activity} = CommonAPI.post(target_user, %{"status" => "foobar"}) - [reporter: reporter, target_user: target_user, activity: activity] + [target_user: target_user, activity: activity] end - test "submit a basic report", %{conn: conn, reporter: reporter, target_user: target_user} do + test "submit a basic report", %{conn: conn, target_user: target_user} do assert %{"action_taken" => false, "id" => _} = conn - |> assign(:user, reporter) |> post("/api/v1/reports", %{"account_id" => target_user.id}) |> json_response(200) end test "submit a report with statuses and comment", %{ conn: conn, - reporter: reporter, target_user: target_user, activity: activity } do assert %{"action_taken" => false, "id" => _} = conn - |> assign(:user, reporter) |> post("/api/v1/reports", %{ "account_id" => target_user.id, "status_ids" => [activity.id], @@ -46,19 +44,16 @@ test "submit a report with statuses and comment", %{ test "account_id is required", %{ conn: conn, - reporter: reporter, activity: activity } do assert %{"error" => "Valid `account_id` required"} = conn - |> assign(:user, reporter) |> post("/api/v1/reports", %{"status_ids" => [activity.id]}) |> json_response(400) end test "comment must be up to the size specified in the config", %{ conn: conn, - reporter: reporter, target_user: target_user } do max_size = Pleroma.Config.get([:instance, :max_report_comment_size], 1000) @@ -68,20 +63,15 @@ test "comment must be up to the size specified in the config", %{ assert ^error = conn - |> assign(:user, reporter) |> post("/api/v1/reports", %{"account_id" => target_user.id, "comment" => comment}) |> json_response(400) end test "returns error when account is not exist", %{ conn: conn, - reporter: reporter, activity: activity } do - conn = - conn - |> assign(:user, reporter) - |> post("/api/v1/reports", %{"status_ids" => [activity.id], "account_id" => "foo"}) + conn = post(conn, "/api/v1/reports", %{"status_ids" => [activity.id], "account_id" => "foo"}) assert json_response(conn, 400) == %{"error" => "Account not found"} end diff --git a/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs b/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs index ae5fee2bc..9666a7f2e 100644 --- a/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs +++ b/test/web/mastodon_api/controllers/scheduled_activity_controller_test.exs @@ -10,89 +10,69 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityControllerTest do import Pleroma.Factory - test "shows scheduled activities", %{conn: conn} do - user = insert(:user) + test "shows scheduled activities" do + %{user: user, conn: conn} = oauth_access(["read:statuses"]) + scheduled_activity_id1 = insert(:scheduled_activity, user: user).id |> to_string() scheduled_activity_id2 = insert(:scheduled_activity, user: user).id |> to_string() scheduled_activity_id3 = insert(:scheduled_activity, user: user).id |> to_string() scheduled_activity_id4 = insert(:scheduled_activity, user: user).id |> to_string() - conn = - conn - |> assign(:user, user) - # min_id - conn_res = - conn - |> get("/api/v1/scheduled_statuses?limit=2&min_id=#{scheduled_activity_id1}") + conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&min_id=#{scheduled_activity_id1}") result = json_response(conn_res, 200) assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result # since_id - conn_res = - conn - |> get("/api/v1/scheduled_statuses?limit=2&since_id=#{scheduled_activity_id1}") + conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&since_id=#{scheduled_activity_id1}") result = json_response(conn_res, 200) assert [%{"id" => ^scheduled_activity_id4}, %{"id" => ^scheduled_activity_id3}] = result # max_id - conn_res = - conn - |> get("/api/v1/scheduled_statuses?limit=2&max_id=#{scheduled_activity_id4}") + conn_res = get(conn, "/api/v1/scheduled_statuses?limit=2&max_id=#{scheduled_activity_id4}") result = json_response(conn_res, 200) assert [%{"id" => ^scheduled_activity_id3}, %{"id" => ^scheduled_activity_id2}] = result end - test "shows a scheduled activity", %{conn: conn} do - user = insert(:user) + test "shows a scheduled activity" do + %{user: user, conn: conn} = oauth_access(["read:statuses"]) scheduled_activity = insert(:scheduled_activity, user: user) - res_conn = - conn - |> assign(:user, user) - |> get("/api/v1/scheduled_statuses/#{scheduled_activity.id}") + res_conn = get(conn, "/api/v1/scheduled_statuses/#{scheduled_activity.id}") assert %{"id" => scheduled_activity_id} = json_response(res_conn, 200) assert scheduled_activity_id == scheduled_activity.id |> to_string() - res_conn = - conn - |> assign(:user, user) - |> get("/api/v1/scheduled_statuses/404") + res_conn = get(conn, "/api/v1/scheduled_statuses/404") assert %{"error" => "Record not found"} = json_response(res_conn, 404) end - test "updates a scheduled activity", %{conn: conn} do - user = insert(:user) + test "updates a scheduled activity" do + %{user: user, conn: conn} = oauth_access(["write:statuses"]) scheduled_activity = insert(:scheduled_activity, user: user) new_scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond) res_conn = - conn - |> assign(:user, user) - |> put("/api/v1/scheduled_statuses/#{scheduled_activity.id}", %{ + put(conn, "/api/v1/scheduled_statuses/#{scheduled_activity.id}", %{ scheduled_at: new_scheduled_at }) assert %{"scheduled_at" => expected_scheduled_at} = json_response(res_conn, 200) assert expected_scheduled_at == Pleroma.Web.CommonAPI.Utils.to_masto_date(new_scheduled_at) - res_conn = - conn - |> assign(:user, user) - |> put("/api/v1/scheduled_statuses/404", %{scheduled_at: new_scheduled_at}) + res_conn = put(conn, "/api/v1/scheduled_statuses/404", %{scheduled_at: new_scheduled_at}) assert %{"error" => "Record not found"} = json_response(res_conn, 404) end - test "deletes a scheduled activity", %{conn: conn} do - user = insert(:user) + test "deletes a scheduled activity" do + %{user: user, conn: conn} = oauth_access(["write:statuses"]) scheduled_activity = insert(:scheduled_activity, user: user) res_conn = diff --git a/test/web/mastodon_api/controllers/search_controller_test.exs b/test/web/mastodon_api/controllers/search_controller_test.exs index 34deeba47..7fedf42e5 100644 --- a/test/web/mastodon_api/controllers/search_controller_test.exs +++ b/test/web/mastodon_api/controllers/search_controller_test.exs @@ -77,13 +77,11 @@ test "search", %{conn: conn} do describe ".account_search" do test "account search", %{conn: conn} do - user = insert(:user) user_two = insert(:user, %{nickname: "shp@shitposter.club"}) user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) results = conn - |> assign(:user, user) |> get("/api/v1/accounts/search", %{"q" => "shp"}) |> json_response(200) @@ -94,7 +92,6 @@ test "account search", %{conn: conn} do results = conn - |> assign(:user, user) |> get("/api/v1/accounts/search", %{"q" => "2hu"}) |> json_response(200) @@ -104,11 +101,10 @@ test "account search", %{conn: conn} do end test "returns account if query contains a space", %{conn: conn} do - user = insert(:user, %{nickname: "shp@shitposter.club"}) + insert(:user, %{nickname: "shp@shitposter.club"}) results = conn - |> assign(:user, user) |> get("/api/v1/accounts/search", %{"q" => "shp@shitposter.club xxx "}) |> json_response(200) @@ -209,6 +205,7 @@ test "search fetches remote accounts", %{conn: conn} do conn = conn |> assign(:user, user) + |> assign(:token, insert(:oauth_token, user: user, scopes: ["read"])) |> get("/api/v1/search", %{"q" => "mike@osada.macgirvin.com", "resolve" => "true"}) assert results = json_response(conn, 200) diff --git a/test/web/mastodon_api/controllers/suggestion_controller_test.exs b/test/web/mastodon_api/controllers/suggestion_controller_test.exs index 78620a873..c4118a576 100644 --- a/test/web/mastodon_api/controllers/suggestion_controller_test.exs +++ b/test/web/mastodon_api/controllers/suggestion_controller_test.exs @@ -11,8 +11,9 @@ defmodule Pleroma.Web.MastodonAPI.SuggestionControllerTest do import Pleroma.Factory import Tesla.Mock - setup do - user = insert(:user) + setup do: oauth_access(["read"]) + + setup %{user: user} do other_user = insert(:user) host = Config.get([Pleroma.Web.Endpoint, :url, :host]) url500 = "http://test500?#{host}&#{user.nickname}" @@ -32,31 +33,29 @@ defmodule Pleroma.Web.MastodonAPI.SuggestionControllerTest do } end) - [user: user, other_user: other_user] + [other_user: other_user] end clear_config(:suggestions) - test "returns empty result when suggestions disabled", %{conn: conn, user: user} do + test "returns empty result when suggestions disabled", %{conn: conn} do Config.put([:suggestions, :enabled], false) res = conn - |> assign(:user, user) |> get("/api/v1/suggestions") |> json_response(200) assert res == [] end - test "returns error", %{conn: conn, user: user} do + test "returns error", %{conn: conn} do Config.put([:suggestions, :enabled], true) Config.put([:suggestions, :third_party_engine], "http://test500?{{host}}&{{user}}") assert capture_log(fn -> res = conn - |> assign(:user, user) |> get("/api/v1/suggestions") |> json_response(500) @@ -64,13 +63,12 @@ test "returns error", %{conn: conn, user: user} do end) =~ "Could not retrieve suggestions" end - test "returns suggestions", %{conn: conn, user: user, other_user: other_user} do + test "returns suggestions", %{conn: conn, other_user: other_user} do Config.put([:suggestions, :enabled], true) Config.put([:suggestions, :third_party_engine], "http://test200?{{host}}&{{user}}") res = conn - |> assign(:user, user) |> get("/api/v1/suggestions") |> json_response(200) diff --git a/test/web/mastodon_api/controllers/timeline_controller_test.exs b/test/web/mastodon_api/controllers/timeline_controller_test.exs index dc17cc963..bb94d8e5a 100644 --- a/test/web/mastodon_api/controllers/timeline_controller_test.exs +++ b/test/web/mastodon_api/controllers/timeline_controller_test.exs @@ -20,31 +20,25 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do end describe "home" do - test "the home timeline", %{conn: conn} do - user = insert(:user) + setup do: oauth_access(["read:statuses"]) + + test "the home timeline", %{user: user, conn: conn} do following = insert(:user) {:ok, _activity} = CommonAPI.post(following, %{"status" => "test"}) - conn = - conn - |> assign(:user, user) - |> get("/api/v1/timelines/home") + ret_conn = get(conn, "/api/v1/timelines/home") - assert Enum.empty?(json_response(conn, :ok)) + assert Enum.empty?(json_response(ret_conn, :ok)) - {:ok, user} = User.follow(user, following) + {:ok, _user} = User.follow(user, following) - conn = - build_conn() - |> assign(:user, user) - |> get("/api/v1/timelines/home") + conn = get(conn, "/api/v1/timelines/home") assert [%{"content" => "test"}] = json_response(conn, :ok) end - test "the home timeline when the direct messages are excluded", %{conn: conn} do - user = insert(:user) + test "the home timeline when the direct messages are excluded", %{user: user, conn: conn} do {:ok, public_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"}) {:ok, direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"}) @@ -54,10 +48,7 @@ test "the home timeline when the direct messages are excluded", %{conn: conn} do {:ok, private_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "private"}) - conn = - conn - |> assign(:user, user) - |> get("/api/v1/timelines/home", %{"exclude_visibilities" => ["direct"]}) + conn = get(conn, "/api/v1/timelines/home", %{"exclude_visibilities" => ["direct"]}) assert status_ids = json_response(conn, :ok) |> Enum.map(& &1["id"]) assert public_activity.id in status_ids @@ -99,11 +90,7 @@ test "the public timeline when public is set to false", %{conn: conn} do end test "the public timeline includes only public statuses for an authenticated user" do - user = insert(:user) - - conn = - build_conn() - |> assign(:user, user) + %{user: user, conn: conn} = oauth_access(["read:statuses"]) {:ok, _activity} = CommonAPI.post(user, %{"status" => "test"}) {:ok, _activity} = CommonAPI.post(user, %{"status" => "test", "visibility" => "private"}) @@ -134,11 +121,13 @@ test "direct timeline", %{conn: conn} do "visibility" => "private" }) - # Only direct should be visible here - res_conn = + conn_user_two = conn |> assign(:user, user_two) - |> get("api/v1/timelines/direct") + |> assign(:token, insert(:oauth_token, user: user_two, scopes: ["read:statuses"])) + + # Only direct should be visible here + res_conn = get(conn_user_two, "api/v1/timelines/direct") [status] = json_response(res_conn, :ok) @@ -149,6 +138,7 @@ test "direct timeline", %{conn: conn} do res_conn = build_conn() |> assign(:user, user_one) + |> assign(:token, insert(:oauth_token, user: user_one, scopes: ["read:statuses"])) |> get("api/v1/timelines/direct") [status] = json_response(res_conn, :ok) @@ -156,10 +146,7 @@ test "direct timeline", %{conn: conn} do assert %{"visibility" => "direct"} = status # Both should be visible here - res_conn = - conn - |> assign(:user, user_two) - |> get("api/v1/timelines/home") + res_conn = get(conn_user_two, "api/v1/timelines/home") [_s1, _s2] = json_response(res_conn, :ok) @@ -172,28 +159,23 @@ test "direct timeline", %{conn: conn} do }) end) - res_conn = - conn - |> assign(:user, user_two) - |> get("api/v1/timelines/direct") + res_conn = get(conn_user_two, "api/v1/timelines/direct") statuses = json_response(res_conn, :ok) assert length(statuses) == 20 res_conn = - conn - |> assign(:user, user_two) - |> get("api/v1/timelines/direct", %{max_id: List.last(statuses)["id"]}) + get(conn_user_two, "api/v1/timelines/direct", %{max_id: List.last(statuses)["id"]}) [status] = json_response(res_conn, :ok) assert status["url"] != direct.data["id"] end - test "doesn't include DMs from blocked users", %{conn: conn} do - blocker = insert(:user) + test "doesn't include DMs from blocked users" do + %{user: blocker, conn: conn} = oauth_access(["read:statuses"]) blocked = insert(:user) - user = insert(:user) + other_user = insert(:user) {:ok, _user_relationship} = User.block(blocker, blocked) {:ok, _blocked_direct} = @@ -203,15 +185,12 @@ test "doesn't include DMs from blocked users", %{conn: conn} do }) {:ok, direct} = - CommonAPI.post(user, %{ + CommonAPI.post(other_user, %{ "status" => "Hi @#{blocker.nickname}!", "visibility" => "direct" }) - res_conn = - conn - |> assign(:user, user) - |> get("api/v1/timelines/direct") + res_conn = get(conn, "api/v1/timelines/direct") [status] = json_response(res_conn, :ok) assert status["id"] == direct.id @@ -219,26 +198,26 @@ test "doesn't include DMs from blocked users", %{conn: conn} do end describe "list" do - test "list timeline", %{conn: conn} do - user = insert(:user) + setup do: oauth_access(["read:lists"]) + + test "list timeline", %{user: user, conn: conn} do other_user = insert(:user) {:ok, _activity_one} = CommonAPI.post(user, %{"status" => "Marisa is cute."}) {:ok, activity_two} = CommonAPI.post(other_user, %{"status" => "Marisa is cute."}) {:ok, list} = Pleroma.List.create("name", user) {:ok, list} = Pleroma.List.follow(list, other_user) - conn = - conn - |> assign(:user, user) - |> get("/api/v1/timelines/list/#{list.id}") + conn = get(conn, "/api/v1/timelines/list/#{list.id}") assert [%{"id" => id}] = json_response(conn, :ok) assert id == to_string(activity_two.id) end - test "list timeline does not leak non-public statuses for unfollowed users", %{conn: conn} do - user = insert(:user) + test "list timeline does not leak non-public statuses for unfollowed users", %{ + user: user, + conn: conn + } do other_user = insert(:user) {:ok, activity_one} = CommonAPI.post(other_user, %{"status" => "Marisa is cute."}) @@ -251,10 +230,7 @@ test "list timeline does not leak non-public statuses for unfollowed users", %{c {:ok, list} = Pleroma.List.create("name", user) {:ok, list} = Pleroma.List.follow(list, other_user) - conn = - conn - |> assign(:user, user) - |> get("/api/v1/timelines/list/#{list.id}") + conn = get(conn, "/api/v1/timelines/list/#{list.id}") assert [%{"id" => id}] = json_response(conn, :ok) @@ -263,6 +239,8 @@ test "list timeline does not leak non-public statuses for unfollowed users", %{c end describe "hashtag" do + setup do: oauth_access(["n/a"]) + @tag capture_log: true test "hashtag timeline", %{conn: conn} do following = insert(:user) diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index 42a8779c0..c1f70f9fe 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -5,69 +5,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do use Pleroma.Web.ConnCase - alias Pleroma.Notification - alias Pleroma.Repo - alias Pleroma.Web.CommonAPI - - import Pleroma.Factory - import Tesla.Mock - - setup do - mock(fn env -> apply(HttpRequestMock, :request, [env]) end) - :ok - end - - clear_config([:rich_media, :enabled]) - - test "unimplemented follow_requests, blocks, domain blocks" do - user = insert(:user) - - ["blocks", "domain_blocks", "follow_requests"] - |> Enum.each(fn endpoint -> - conn = - build_conn() - |> assign(:user, user) - |> get("/api/v1/#{endpoint}") - - assert [] = json_response(conn, 200) - end) - end - - describe "link headers" do - test "preserves parameters in link headers", %{conn: conn} do - user = insert(:user) - other_user = insert(:user) - - {:ok, activity1} = - CommonAPI.post(other_user, %{ - "status" => "hi @#{user.nickname}", - "visibility" => "public" - }) - - {:ok, activity2} = - CommonAPI.post(other_user, %{ - "status" => "hi @#{user.nickname}", - "visibility" => "public" - }) - - notification1 = Repo.get_by(Notification, activity_id: activity1.id) - notification2 = Repo.get_by(Notification, activity_id: activity2.id) - - conn = - conn - |> assign(:user, user) - |> get("/api/v1/notifications", %{media_only: true}) - - assert [link_header] = get_resp_header(conn, "link") - assert link_header =~ ~r/media_only=true/ - assert link_header =~ ~r/min_id=#{notification2.id}/ - assert link_header =~ ~r/max_id=#{notification1.id}/ - end - end - - describe "empty_array, stubs for mastodon api" do - test "GET /api/v1/accounts/:id/identity_proofs", %{conn: conn} do - user = insert(:user) + describe "empty_array/2 (stubs)" do + test "GET /api/v1/accounts/:id/identity_proofs" do + %{user: user, conn: conn} = oauth_access(["n/a"]) res = conn @@ -78,12 +18,11 @@ test "GET /api/v1/accounts/:id/identity_proofs", %{conn: conn} do assert res == [] end - test "GET /api/v1/endorsements", %{conn: conn} do - user = insert(:user) + test "GET /api/v1/endorsements" do + %{conn: conn} = oauth_access(["read:accounts"]) res = conn - |> assign(:user, user) |> get("/api/v1/endorsements") |> json_response(200) @@ -91,11 +30,8 @@ test "GET /api/v1/endorsements", %{conn: conn} do end test "GET /api/v1/trends", %{conn: conn} do - user = insert(:user) - res = conn - |> assign(:user, user) |> get("/api/v1/trends") |> json_response(200) diff --git a/test/web/pleroma_api/controllers/account_controller_test.exs b/test/web/pleroma_api/controllers/account_controller_test.exs index c809f510f..d17026a6b 100644 --- a/test/web/pleroma_api/controllers/account_controller_test.exs +++ b/test/web/pleroma_api/controllers/account_controller_test.exs @@ -33,7 +33,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountControllerTest do test "resend account confirmation email", %{conn: conn, user: user} do conn - |> assign(:user, user) |> post("/api/v1/pleroma/accounts/confirmation_resend?email=#{user.email}") |> json_response(:no_content) @@ -52,14 +51,12 @@ test "resend account confirmation email", %{conn: conn, user: user} do end describe "PATCH /api/v1/pleroma/accounts/update_avatar" do - test "user avatar can be set", %{conn: conn} do - user = insert(:user) + setup do: oauth_access(["write:accounts"]) + + test "user avatar can be set", %{user: user, conn: conn} do avatar_image = File.read!("test/fixtures/avatar_data_uri") - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/pleroma/accounts/update_avatar", %{img: avatar_image}) + conn = patch(conn, "/api/v1/pleroma/accounts/update_avatar", %{img: avatar_image}) user = refresh_record(user) @@ -78,13 +75,8 @@ test "user avatar can be set", %{conn: conn} do assert %{"url" => _} = json_response(conn, 200) end - test "user avatar can be reset", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/pleroma/accounts/update_avatar", %{img: ""}) + test "user avatar can be reset", %{user: user, conn: conn} do + conn = patch(conn, "/api/v1/pleroma/accounts/update_avatar", %{img: ""}) user = User.get_cached_by_id(user.id) @@ -95,13 +87,10 @@ test "user avatar can be reset", %{conn: conn} do end describe "PATCH /api/v1/pleroma/accounts/update_banner" do - test "can set profile banner", %{conn: conn} do - user = insert(:user) + setup do: oauth_access(["write:accounts"]) - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/pleroma/accounts/update_banner", %{"banner" => @image}) + test "can set profile banner", %{user: user, conn: conn} do + conn = patch(conn, "/api/v1/pleroma/accounts/update_banner", %{"banner" => @image}) user = refresh_record(user) assert user.banner["type"] == "Image" @@ -109,13 +98,8 @@ test "can set profile banner", %{conn: conn} do assert %{"url" => _} = json_response(conn, 200) end - test "can reset profile banner", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/pleroma/accounts/update_banner", %{"banner" => ""}) + test "can reset profile banner", %{user: user, conn: conn} do + conn = patch(conn, "/api/v1/pleroma/accounts/update_banner", %{"banner" => ""}) user = refresh_record(user) assert user.banner == %{} @@ -125,26 +109,18 @@ test "can reset profile banner", %{conn: conn} do end describe "PATCH /api/v1/pleroma/accounts/update_background" do - test "background image can be set", %{conn: conn} do - user = insert(:user) + setup do: oauth_access(["write:accounts"]) - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/pleroma/accounts/update_background", %{"img" => @image}) + test "background image can be set", %{user: user, conn: conn} do + conn = patch(conn, "/api/v1/pleroma/accounts/update_background", %{"img" => @image}) user = refresh_record(user) assert user.background["type"] == "Image" assert %{"url" => _} = json_response(conn, 200) end - test "background image can be reset", %{conn: conn} do - user = insert(:user) - - conn = - conn - |> assign(:user, user) - |> patch("/api/v1/pleroma/accounts/update_background", %{"img" => ""}) + test "background image can be reset", %{user: user, conn: conn} do + conn = patch(conn, "/api/v1/pleroma/accounts/update_background", %{"img" => ""}) user = refresh_record(user) assert user.background == %{} @@ -155,12 +131,12 @@ test "background image can be reset", %{conn: conn} do describe "getting favorites timeline of specified user" do setup do [current_user, user] = insert_pair(:user, hide_favorites: false) - [current_user: current_user, user: user] + %{user: current_user, conn: conn} = oauth_access(["read:favourites"], user: current_user) + [current_user: current_user, user: user, conn: conn] end test "returns list of statuses favorited by specified user", %{ conn: conn, - current_user: current_user, user: user } do [activity | _] = insert_pair(:note_activity) @@ -168,7 +144,6 @@ test "returns list of statuses favorited by specified user", %{ response = conn - |> assign(:user, current_user) |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") |> json_response(:ok) @@ -178,23 +153,18 @@ test "returns list of statuses favorited by specified user", %{ assert like["id"] == activity.id end - test "returns favorites for specified user_id when user is not logged in", %{ - conn: conn, + test "does not return favorites for specified user_id when user is not logged in", %{ user: user } do activity = insert(:note_activity) CommonAPI.favorite(activity.id, user) - response = - conn - |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") - |> json_response(:ok) - - assert length(response) == 1 + build_conn() + |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") + |> json_response(403) end test "returns favorited DM only when user is logged in and he is one of recipients", %{ - conn: conn, current_user: current_user, user: user } do @@ -206,25 +176,24 @@ test "returns favorited DM only when user is logged in and he is one of recipien CommonAPI.favorite(direct.id, user) - response = - conn - |> assign(:user, current_user) - |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") - |> json_response(:ok) + for u <- [user, current_user] do + response = + build_conn() + |> assign(:user, u) + |> assign(:token, insert(:oauth_token, user: u, scopes: ["read:favourites"])) + |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") + |> json_response(:ok) - assert length(response) == 1 + assert length(response) == 1 + end - anonymous_response = - conn - |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") - |> json_response(:ok) - - assert Enum.empty?(anonymous_response) + build_conn() + |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") + |> json_response(403) end test "does not return others' favorited DM when user is not one of recipients", %{ conn: conn, - current_user: current_user, user: user } do user_two = insert(:user) @@ -239,7 +208,6 @@ test "does not return others' favorited DM when user is not one of recipients", response = conn - |> assign(:user, current_user) |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") |> json_response(:ok) @@ -248,7 +216,6 @@ test "does not return others' favorited DM when user is not one of recipients", test "paginates favorites using since_id and max_id", %{ conn: conn, - current_user: current_user, user: user } do activities = insert_list(10, :note_activity) @@ -262,7 +229,6 @@ test "paginates favorites using since_id and max_id", %{ response = conn - |> assign(:user, current_user) |> get("/api/v1/pleroma/accounts/#{user.id}/favourites", %{ since_id: third_activity.id, max_id: seventh_activity.id @@ -276,7 +242,6 @@ test "paginates favorites using since_id and max_id", %{ test "limits favorites using limit parameter", %{ conn: conn, - current_user: current_user, user: user } do 7 @@ -287,7 +252,6 @@ test "limits favorites using limit parameter", %{ response = conn - |> assign(:user, current_user) |> get("/api/v1/pleroma/accounts/#{user.id}/favourites", %{limit: "3"}) |> json_response(:ok) @@ -296,12 +260,10 @@ test "limits favorites using limit parameter", %{ test "returns empty response when user does not have any favorited statuses", %{ conn: conn, - current_user: current_user, user: user } do response = conn - |> assign(:user, current_user) |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") |> json_response(:ok) @@ -314,79 +276,61 @@ test "returns 404 error when specified user is not exist", %{conn: conn} do assert json_response(conn, 404) == %{"error" => "Record not found"} end - test "returns 403 error when user has hidden own favorites", %{ - conn: conn, - current_user: current_user - } do + test "returns 403 error when user has hidden own favorites", %{conn: conn} do user = insert(:user, hide_favorites: true) activity = insert(:note_activity) CommonAPI.favorite(activity.id, user) - conn = - conn - |> assign(:user, current_user) - |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") + conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/favourites") assert json_response(conn, 403) == %{"error" => "Can't get favorites"} end - test "hides favorites for new users by default", %{conn: conn, current_user: current_user} do + test "hides favorites for new users by default", %{conn: conn} do user = insert(:user) activity = insert(:note_activity) CommonAPI.favorite(activity.id, user) - conn = - conn - |> assign(:user, current_user) - |> get("/api/v1/pleroma/accounts/#{user.id}/favourites") - assert user.hide_favorites + conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/favourites") + assert json_response(conn, 403) == %{"error" => "Can't get favorites"} end end describe "subscribing / unsubscribing" do - test "subscribing / unsubscribing to a user", %{conn: conn} do - user = insert(:user) + test "subscribing / unsubscribing to a user" do + %{user: user, conn: conn} = oauth_access(["follow"]) subscription_target = insert(:user) - conn = + ret_conn = conn |> assign(:user, user) |> post("/api/v1/pleroma/accounts/#{subscription_target.id}/subscribe") - assert %{"id" => _id, "subscribing" => true} = json_response(conn, 200) + assert %{"id" => _id, "subscribing" => true} = json_response(ret_conn, 200) - conn = - build_conn() - |> assign(:user, user) - |> post("/api/v1/pleroma/accounts/#{subscription_target.id}/unsubscribe") + conn = post(conn, "/api/v1/pleroma/accounts/#{subscription_target.id}/unsubscribe") assert %{"id" => _id, "subscribing" => false} = json_response(conn, 200) end end describe "subscribing" do - test "returns 404 when subscription_target not found", %{conn: conn} do - user = insert(:user) + test "returns 404 when subscription_target not found" do + %{conn: conn} = oauth_access(["write:follows"]) - conn = - conn - |> assign(:user, user) - |> post("/api/v1/pleroma/accounts/target_id/subscribe") + conn = post(conn, "/api/v1/pleroma/accounts/target_id/subscribe") assert %{"error" => "Record not found"} = json_response(conn, 404) end end describe "unsubscribing" do - test "returns 404 when subscription_target not found", %{conn: conn} do - user = insert(:user) + test "returns 404 when subscription_target not found" do + %{conn: conn} = oauth_access(["follow"]) - conn = - conn - |> assign(:user, user) - |> post("/api/v1/pleroma/accounts/target_id/unsubscribe") + conn = post(conn, "/api/v1/pleroma/accounts/target_id/unsubscribe") assert %{"error" => "Record not found"} = json_response(conn, 404) end diff --git a/test/web/pleroma_api/controllers/emoji_api_controller_test.exs b/test/web/pleroma_api/controllers/emoji_api_controller_test.exs index 3d3becefd..e1b484dae 100644 --- a/test/web/pleroma_api/controllers/emoji_api_controller_test.exs +++ b/test/web/pleroma_api/controllers/emoji_api_controller_test.exs @@ -39,9 +39,12 @@ test "shared & non-shared pack information in list_packs is ok" do test "listing remote packs" do admin = insert(:user, is_admin: true) - conn = build_conn() |> assign(:user, admin) + %{conn: conn} = oauth_access(["admin:write"], user: admin) - resp = conn |> get(emoji_api_path(conn, :list_packs)) |> json_response(200) + resp = + build_conn() + |> get(emoji_api_path(conn, :list_packs)) + |> json_response(200) mock(fn %{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> @@ -123,7 +126,10 @@ test "downloading shared & unshared packs from another instance via download_fro admin = insert(:user, is_admin: true) - conn = build_conn() |> assign(:user, admin) + conn = + build_conn() + |> assign(:user, admin) + |> assign(:token, insert(:oauth_admin_token, user: admin, scopes: ["admin:write"])) assert (conn |> put_req_header("content-type", "application/json") @@ -168,8 +174,6 @@ test "downloading shared & unshared packs from another instance via download_fro # non-shared, downloaded from the fallback URL - conn = build_conn() |> assign(:user, admin) - assert conn |> put_req_header("content-type", "application/json") |> post( @@ -205,8 +209,12 @@ test "downloading shared & unshared packs from another instance via download_fro File.write!(pack_file, original_content) end) + admin = insert(:user, is_admin: true) + %{conn: conn} = oauth_access(["admin:write"], user: admin) + {:ok, - admin: insert(:user, is_admin: true), + admin: admin, + conn: conn, pack_file: pack_file, new_data: %{ "license" => "Test license changed", @@ -217,10 +225,9 @@ test "downloading shared & unshared packs from another instance via download_fro end test "for a pack without a fallback source", ctx do - conn = build_conn() + conn = ctx[:conn] assert conn - |> assign(:user, ctx[:admin]) |> post( emoji_api_path(conn, :update_metadata, "test_pack"), %{ @@ -250,10 +257,9 @@ test "for a pack with a fallback source", ctx do "74409E2674DAA06C072729C6C8426C4CB3B7E0B85ED77792DB7A436E11D76DAF" ) - conn = build_conn() + conn = ctx[:conn] assert conn - |> assign(:user, ctx[:admin]) |> post( emoji_api_path(conn, :update_metadata, "test_pack"), %{ @@ -277,10 +283,9 @@ test "when the fallback source doesn't have all the files", ctx do new_data = Map.put(ctx[:new_data], "fallback-src", "https://nonshared-pack") - conn = build_conn() + conn = ctx[:conn] assert (conn - |> assign(:user, ctx[:admin]) |> post( emoji_api_path(conn, :update_metadata, "test_pack"), %{ @@ -304,8 +309,7 @@ test "updating pack files" do end) admin = insert(:user, is_admin: true) - - conn = build_conn() + %{conn: conn} = oauth_access(["admin:write"], user: admin) same_name = %{ "action" => "add", @@ -319,8 +323,6 @@ test "updating pack files" do different_name = %{same_name | "shortcode" => "blank_2"} - conn = conn |> assign(:user, admin) - assert (conn |> post(emoji_api_path(conn, :update_file, "test_pack"), same_name) |> json_response(:conflict))["error"] =~ "already exists" @@ -392,8 +394,7 @@ test "creating and deleting a pack" do end) admin = insert(:user, is_admin: true) - - conn = build_conn() |> assign(:user, admin) + %{conn: conn} = oauth_access(["admin:write"], user: admin) assert conn |> put_req_header("content-type", "application/json") @@ -432,9 +433,9 @@ test "filesystem import" do refute Map.has_key?(resp, "test_pack_for_import") admin = insert(:user, is_admin: true) + %{conn: conn} = oauth_access(["admin:write"], user: admin) assert conn - |> assign(:user, admin) |> post(emoji_api_path(conn, :import_from_fs)) |> json_response(200) == ["test_pack_for_import"] @@ -449,11 +450,10 @@ test "filesystem import" do File.write!("#{@emoji_dir_path}/test_pack_for_import/emoji.txt", emoji_txt_content) assert conn - |> assign(:user, admin) |> post(emoji_api_path(conn, :import_from_fs)) |> json_response(200) == ["test_pack_for_import"] - resp = conn |> get(emoji_api_path(conn, :list_packs)) |> json_response(200) + resp = build_conn() |> get(emoji_api_path(conn, :list_packs)) |> json_response(200) assert resp["test_pack_for_import"]["files"] == %{ "blank" => "blank.png", diff --git a/test/web/pleroma_api/controllers/mascot_controller_test.exs b/test/web/pleroma_api/controllers/mascot_controller_test.exs index ae9539b04..40c33e609 100644 --- a/test/web/pleroma_api/controllers/mascot_controller_test.exs +++ b/test/web/pleroma_api/controllers/mascot_controller_test.exs @@ -7,10 +7,8 @@ defmodule Pleroma.Web.PleromaAPI.MascotControllerTest do alias Pleroma.User - import Pleroma.Factory - - test "mascot upload", %{conn: conn} do - user = insert(:user) + test "mascot upload" do + %{conn: conn} = oauth_access(["write:accounts"]) non_image_file = %Plug.Upload{ content_type: "audio/mpeg", @@ -18,12 +16,9 @@ test "mascot upload", %{conn: conn} do filename: "sound.mp3" } - conn = - conn - |> assign(:user, user) - |> put("/api/v1/pleroma/mascot", %{"file" => non_image_file}) + ret_conn = put(conn, "/api/v1/pleroma/mascot", %{"file" => non_image_file}) - assert json_response(conn, 415) + assert json_response(ret_conn, 415) file = %Plug.Upload{ content_type: "image/jpg", @@ -31,23 +26,18 @@ test "mascot upload", %{conn: conn} do filename: "an_image.jpg" } - conn = - build_conn() - |> assign(:user, user) - |> put("/api/v1/pleroma/mascot", %{"file" => file}) + conn = put(conn, "/api/v1/pleroma/mascot", %{"file" => file}) assert %{"id" => _, "type" => image} = json_response(conn, 200) end - test "mascot retrieving", %{conn: conn} do - user = insert(:user) - # When user hasn't set a mascot, we should just get pleroma tan back - conn = - conn - |> assign(:user, user) - |> get("/api/v1/pleroma/mascot") + test "mascot retrieving" do + %{user: user, conn: conn} = oauth_access(["read:accounts", "write:accounts"]) - assert %{"url" => url} = json_response(conn, 200) + # When user hasn't set a mascot, we should just get pleroma tan back + ret_conn = get(conn, "/api/v1/pleroma/mascot") + + assert %{"url" => url} = json_response(ret_conn, 200) assert url =~ "pleroma-fox-tan-smol" # When a user sets their mascot, we should get that back @@ -57,17 +47,14 @@ test "mascot retrieving", %{conn: conn} do filename: "an_image.jpg" } - conn = - build_conn() - |> assign(:user, user) - |> put("/api/v1/pleroma/mascot", %{"file" => file}) + ret_conn = put(conn, "/api/v1/pleroma/mascot", %{"file" => file}) - assert json_response(conn, 200) + assert json_response(ret_conn, 200) user = User.get_cached_by_id(user.id) conn = - build_conn() + conn |> assign(:user, user) |> get("/api/v1/pleroma/mascot") diff --git a/test/web/pleroma_api/controllers/scrobble_controller_test.exs b/test/web/pleroma_api/controllers/scrobble_controller_test.exs index 881f8012c..2242610f1 100644 --- a/test/web/pleroma_api/controllers/scrobble_controller_test.exs +++ b/test/web/pleroma_api/controllers/scrobble_controller_test.exs @@ -6,16 +6,13 @@ defmodule Pleroma.Web.PleromaAPI.ScrobbleControllerTest do use Pleroma.Web.ConnCase alias Pleroma.Web.CommonAPI - import Pleroma.Factory describe "POST /api/v1/pleroma/scrobble" do - test "works correctly", %{conn: conn} do - user = insert(:user) + test "works correctly" do + %{conn: conn} = oauth_access(["write"]) conn = - conn - |> assign(:user, user) - |> post("/api/v1/pleroma/scrobble", %{ + post(conn, "/api/v1/pleroma/scrobble", %{ "title" => "lain radio episode 1", "artist" => "lain", "album" => "lain radio", @@ -27,8 +24,8 @@ test "works correctly", %{conn: conn} do end describe "GET /api/v1/pleroma/accounts/:id/scrobbles" do - test "works correctly", %{conn: conn} do - user = insert(:user) + test "works correctly" do + %{user: user, conn: conn} = oauth_access(["read"]) {:ok, _activity} = CommonAPI.listen(user, %{ @@ -51,9 +48,7 @@ test "works correctly", %{conn: conn} do "album" => "lain radio" }) - conn = - conn - |> get("/api/v1/pleroma/accounts/#{user.id}/scrobbles") + conn = get(conn, "/api/v1/pleroma/accounts/#{user.id}/scrobbles") result = json_response(conn, 200) From 4079d66f00adc665e4c70abad03eab254695e793 Mon Sep 17 00:00:00 2001 From: Maxim Filippov Date: Thu, 19 Dec 2019 19:47:36 +0300 Subject: [PATCH 22/34] Expose issue via failing test --- test/stats_test.exs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/stats_test.exs b/test/stats_test.exs index 31c2f8db3..02a92dc64 100644 --- a/test/stats_test.exs +++ b/test/stats_test.exs @@ -43,6 +43,7 @@ defmodule Pleroma.StatsTest do test "it returns total number of statuses" do data = Pleroma.Stats.get_stat_data() + assert data.stats.status_count.all == 10 assert data.stats.status_count.public == 1 assert data.stats.status_count.unlisted == 2 assert data.stats.status_count.direct == 3 From 5fc84552d311efd606f66775c55862b3d11ad258 Mon Sep 17 00:00:00 2001 From: Maxim Filippov Date: Thu, 19 Dec 2019 19:52:55 +0300 Subject: [PATCH 23/34] Fix all count --- lib/pleroma/stats.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/stats.ex b/lib/pleroma/stats.ex index c90e8f409..97e8b1990 100644 --- a/lib/pleroma/stats.ex +++ b/lib/pleroma/stats.ex @@ -82,7 +82,7 @@ def get_stat_data do defp status_count do %{ - all: get_all_statuses_count(), + all: all_statuses_query() |> Repo.aggregate(:count, :id), public: public_statuses_query() |> Repo.aggregate(:count, :id), unlisted: unlisted_statuses_query() |> Repo.aggregate(:count, :id), direct: direct_statuses_query() |> Repo.aggregate(:count, :id), @@ -90,8 +90,8 @@ defp status_count do } end - defp get_all_statuses_count do - Repo.aggregate(User.Query.build(%{local: true}), :sum, :note_count) + defp all_statuses_query do + from(o in Object, where: fragment("(?)->>'type' = 'Note'", o.data)) end def public_statuses_query do From fc79c691736725e62dad1c7a701d8c5f3435410f Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Fri, 20 Dec 2019 19:47:44 +0700 Subject: [PATCH 24/34] Remove `/api/account/register` documentation from pleroma_api.md --- docs/API/pleroma_api.md | 53 ----------------------------------------- 1 file changed, 53 deletions(-) diff --git a/docs/API/pleroma_api.md b/docs/API/pleroma_api.md index 7228d805b..689edbcc2 100644 --- a/docs/API/pleroma_api.md +++ b/docs/API/pleroma_api.md @@ -70,59 +70,6 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi * Response: JSON. Returns `{"status": "success"}` if the account was successfully disabled, `{"error": "[error message]"}` otherwise * Example response: `{"error": "Invalid password."}` -## `/api/account/register` -### Register a new user -* Method `POST` -* Authentication: not required -* Params: - * `nickname` - * `fullname` - * `bio` - * `email` - * `password` - * `confirm` - * `captcha_solution`: optional, contains provider-specific captcha solution, - * `captcha_token`: optional, contains provider-specific captcha token - * `token`: invite token required when the registrations aren't public. -* Response: JSON. Returns a user object on success, otherwise returns `{"error": "error_msg"}` -* Example response: -```json -{ - "background_image": null, - "cover_photo": "https://pleroma.soykaf.com/images/banner.png", - "created_at": "Tue Dec 18 16:55:56 +0000 2018", - "default_scope": "public", - "description": "blushy-crushy fediverse idol + pleroma dev\nlet's be friends \nぷれろまの生徒会長。謎の外人。日本語OK. \n公主病.", - "description_html": "blushy-crushy fediverse idol + pleroma dev.
let's be friends
ぷれろまの生徒会長。謎の外人。日本語OK.
公主病.", - "favourites_count": 0, - "fields": [], - "followers_count": 0, - "following": false, - "follows_you": false, - "friends_count": 0, - "id": 6, - "is_local": true, - "locked": false, - "name": "lain", - "name_html": "lain", - "no_rich_text": false, - "pleroma": { - "tags": [] - }, - "profile_image_url": "https://pleroma.soykaf.com/images/avi.png", - "profile_image_url_https": "https://pleroma.soykaf.com/images/avi.png", - "profile_image_url_original": "https://pleroma.soykaf.com/images/avi.png", - "profile_image_url_profile_size": "https://pleroma.soykaf.com/images/avi.png", - "rights": { - "delete_others_notice": false - }, - "screen_name": "lain", - "statuses_count": 0, - "statusnet_blocking": false, - "statusnet_profile_url": "https://pleroma.soykaf.com/users/lain" -} -``` - ## `/api/pleroma/admin/`… See [Admin-API](admin_api.md) From 06ae56a3ae93c494f9c5d15b097d75c6ab7fcc29 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 20 Dec 2019 16:32:04 -0600 Subject: [PATCH 25/34] Posts without media attachments should get the Summary TwitterCard --- lib/pleroma/web/metadata/twitter_card.ex | 2 +- test/web/metadata/twitter_card_test.exs | 29 ++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/metadata/twitter_card.ex b/lib/pleroma/web/metadata/twitter_card.ex index d6a6049b3..67419a666 100644 --- a/lib/pleroma/web/metadata/twitter_card.ex +++ b/lib/pleroma/web/metadata/twitter_card.ex @@ -31,7 +31,7 @@ def build_tags(%{activity_id: id, object: object, user: user}) do if attachments == [] or Metadata.activity_nsfw?(object) do [ image_tag(user), - {:meta, [property: "twitter:card", content: "summary_large_image"], []} + {:meta, [property: "twitter:card", content: "summary"], []} ] else attachments diff --git a/test/web/metadata/twitter_card_test.exs b/test/web/metadata/twitter_card_test.exs index 0814006d2..85a654f52 100644 --- a/test/web/metadata/twitter_card_test.exs +++ b/test/web/metadata/twitter_card_test.exs @@ -26,7 +26,32 @@ test "it renders twitter card for user info" do ] end - test "it does not render attachments if post is nsfw" do + test "it uses summary twittercard if post has no attachment" do + user = insert(:user, name: "Jimmy Hendriks", bio: "born 19 March 1994") + {:ok, activity} = CommonAPI.post(user, %{"status" => "HI"}) + + note = + insert(:note, %{ + data: %{ + "actor" => user.ap_id, + "tag" => [], + "id" => "https://pleroma.gov/objects/whatever", + "content" => "pleroma in a nutshell" + } + }) + + result = TwitterCard.build_tags(%{object: note, user: user, activity_id: activity.id}) + + assert [ + {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []}, + {:meta, [property: "twitter:description", content: "“pleroma in a nutshell”"], []}, + {:meta, [property: "twitter:image", content: "http://localhost:4001/images/avi.png"], + []}, + {:meta, [property: "twitter:card", content: "summary"], []} + ] == result + end + + test "it renders avatar not attachment if post is nsfw and unfurl_nsfw is disabled" do Pleroma.Config.put([Pleroma.Web.Metadata, :unfurl_nsfw], false) user = insert(:user, name: "Jimmy Hendriks", bio: "born 19 March 1994") {:ok, activity} = CommonAPI.post(user, %{"status" => "HI"}) @@ -67,7 +92,7 @@ test "it does not render attachments if post is nsfw" do {:meta, [property: "twitter:description", content: "“pleroma in a nutshell”"], []}, {:meta, [property: "twitter:image", content: "http://localhost:4001/images/avi.png"], []}, - {:meta, [property: "twitter:card", content: "summary_large_image"], []} + {:meta, [property: "twitter:card", content: "summary"], []} ] == result end From e71a13ad57d2604b45c0beb278f47d25c284783a Mon Sep 17 00:00:00 2001 From: Maxim Filippov Date: Sat, 21 Dec 2019 11:41:19 +0000 Subject: [PATCH 26/34] Revert "Merge branch 'feature/status-counts-by-scope' into 'develop'" This reverts merge request !2076 --- CHANGELOG.md | 1 - lib/pleroma/stats.ex | 72 ++----------------- test/stats_test.exs | 53 -------------- .../controllers/instance_controller_test.exs | 10 +-- 4 files changed, 6 insertions(+), 130 deletions(-) delete mode 100644 test/stats_test.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index f6cc193a2..c133cd9ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - **Breaking:** Admin API: `PUT /api/pleroma/admin/reports/:id` is now `PATCH /api/pleroma/admin/reports`, see admin_api.md for details - **Breaking:** `/api/pleroma/admin/users/invite_token` now uses `POST`, changed accepted params and returns full invite in json instead of only token string. - **Breaking** replying to reports is now "report notes", enpoint changed from `POST /api/pleroma/admin/reports/:id/respond` to `POST /api/pleroma/admin/reports/:id/notes` -- **Breaking** `/api/v1/stats` now return statuses count by scope (i.e. `all`, `public`, `unlisted`, `direct` and `private`) - Admin API: Return `total` when querying for reports - Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`) - Admin API: Return link alongside with token on password reset diff --git a/lib/pleroma/stats.ex b/lib/pleroma/stats.ex index 97e8b1990..8154a09b7 100644 --- a/lib/pleroma/stats.ex +++ b/lib/pleroma/stats.ex @@ -3,15 +3,11 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Stats do - use GenServer - import Ecto.Query - - alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User - require Pleroma.Constants + use GenServer @interval 1000 * 60 * 60 @@ -60,7 +56,7 @@ defp initial_data do %{peers: [], stats: %{}} end - def get_stat_data do + defp get_stat_data do peers = from( u in User, @@ -72,71 +68,13 @@ def get_stat_data do domain_count = Enum.count(peers) + status_count = Repo.aggregate(User.Query.build(%{local: true}), :sum, :note_count) + user_count = Repo.aggregate(User.Query.build(%{local: true, active: true}), :count, :id) %{ peers: peers, - stats: %{domain_count: domain_count, status_count: status_count(), user_count: user_count} + stats: %{domain_count: domain_count, status_count: status_count, user_count: user_count} } end - - defp status_count do - %{ - all: all_statuses_query() |> Repo.aggregate(:count, :id), - public: public_statuses_query() |> Repo.aggregate(:count, :id), - unlisted: unlisted_statuses_query() |> Repo.aggregate(:count, :id), - direct: direct_statuses_query() |> Repo.aggregate(:count, :id), - private: private_statuses_query() |> Repo.aggregate(:count, :id) - } - end - - defp all_statuses_query do - from(o in Object, where: fragment("(?)->>'type' = 'Note'", o.data)) - end - - def public_statuses_query do - from(o in Object, - where: fragment("(?)->'to' \\? ?", o.data, ^Pleroma.Constants.as_public()) - ) - end - - def unlisted_statuses_query do - from(o in Object, - where: not fragment("(?)->'to' \\? ?", o.data, ^Pleroma.Constants.as_public()), - where: fragment("(?)->'cc' \\? ?", o.data, ^Pleroma.Constants.as_public()) - ) - end - - def direct_statuses_query do - private_statuses_ids = from(p in private_statuses_query(), select: p.id) |> Repo.all() - - from(o in Object, - where: - fragment( - "? \\? 'directMessage' AND (?->>'directMessage')::boolean = true", - o.data, - o.data - ) or - (not fragment("(?)->'to' \\? ?", o.data, ^Pleroma.Constants.as_public()) and - not fragment("(?)->'cc' \\? ?", o.data, ^Pleroma.Constants.as_public()) and - o.id not in ^private_statuses_ids) - ) - end - - def private_statuses_query do - from(o in subquery(recipients_query()), - where: ilike(o.recipients, "%/followers%") - ) - end - - defp recipients_query do - from(o in Object, - select: %{ - id: o.id, - recipients: fragment("jsonb_array_elements_text((?)->'to')", o.data) - }, - where: not fragment("(?)->'to' \\? ?", o.data, ^Pleroma.Constants.as_public()), - where: not fragment("(?)->'cc' \\? ?", o.data, ^Pleroma.Constants.as_public()) - ) - end end diff --git a/test/stats_test.exs b/test/stats_test.exs deleted file mode 100644 index 02a92dc64..000000000 --- a/test/stats_test.exs +++ /dev/null @@ -1,53 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.StatsTest do - use Pleroma.DataCase - - import Pleroma.Factory - - alias Pleroma.Web.CommonAPI - - describe "statuses count" do - setup do - user = insert(:user) - other_user = insert(:user) - - CommonAPI.post(user, %{"visibility" => "public", "status" => "hey"}) - - Enum.each(0..1, fn _ -> - CommonAPI.post(user, %{ - "visibility" => "unlisted", - "status" => "hey" - }) - end) - - Enum.each(0..2, fn _ -> - CommonAPI.post(user, %{ - "visibility" => "direct", - "status" => "hey @#{other_user.nickname}" - }) - end) - - Enum.each(0..3, fn _ -> - CommonAPI.post(user, %{ - "visibility" => "private", - "status" => "hey" - }) - end) - - :ok - end - - test "it returns total number of statuses" do - data = Pleroma.Stats.get_stat_data() - - assert data.stats.status_count.all == 10 - assert data.stats.status_count.public == 1 - assert data.stats.status_count.unlisted == 2 - assert data.stats.status_count.direct == 3 - assert data.stats.status_count.private == 4 - end - end -end diff --git a/test/web/mastodon_api/controllers/instance_controller_test.exs b/test/web/mastodon_api/controllers/instance_controller_test.exs index 7aa7c8648..e00de6b18 100644 --- a/test/web/mastodon_api/controllers/instance_controller_test.exs +++ b/test/web/mastodon_api/controllers/instance_controller_test.exs @@ -58,15 +58,7 @@ test "get instance stats", %{conn: conn} do assert stats assert stats["user_count"] == 1 - - assert stats["status_count"] == %{ - "all" => 1, - "direct" => 0, - "private" => 0, - "public" => 1, - "unlisted" => 0 - } - + assert stats["status_count"] == 1 assert stats["domain_count"] == 2 end From b012e66e4e70247329c4d043221eb2b988bfb7b3 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Mon, 23 Dec 2019 15:14:46 +0700 Subject: [PATCH 27/34] Increase CAPTCHA expiration time to 5 minutes --- config/config.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index 5839cbe4a..c8d42e83e 100644 --- a/config/config.exs +++ b/config/config.exs @@ -67,7 +67,7 @@ config :pleroma, Pleroma.Captcha, enabled: true, - seconds_valid: 60, + seconds_valid: 3000, method: Pleroma.Captcha.Native config :pleroma, Pleroma.Captcha.Kocaptcha, endpoint: "https://captcha.kotobank.ch" From 520940d3e23dc4d4274f65626ba45c0afebea731 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Sat, 28 Dec 2019 19:48:54 +0700 Subject: [PATCH 28/34] Update `captcha` dependency --- mix.exs | 2 +- mix.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index f1a62f6e3..c2e0b940f 100644 --- a/mix.exs +++ b/mix.exs @@ -165,7 +165,7 @@ defp deps do ref: "825dc00aaba5a1b7c4202a532b696b595dd3bcb3"}, {:captcha, git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", - ref: "c3c795c55f6b49d79d6ac70a0f91e525099fc3e2"}, + ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"}, {:mox, "~> 0.5", only: :test} ] ++ oauth_deps() end diff --git a/mix.lock b/mix.lock index 401bcdb6e..ff22791a4 100644 --- a/mix.lock +++ b/mix.lock @@ -8,7 +8,7 @@ "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, "cachex": {:hex, :cachex, "3.0.3", "4e2d3e05814a5738f5ff3903151d5c25636d72a3527251b753f501ad9c657967", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm"}, "calendar": {:hex, :calendar, "0.17.6", "ec291cb2e4ba499c2e8c0ef5f4ace974e2f9d02ae9e807e711a9b0c7850b9aee", [:mix], [{:tzdata, "~> 0.5.20 or ~> 0.1.201603 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, - "captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "c3c795c55f6b49d79d6ac70a0f91e525099fc3e2", [ref: "c3c795c55f6b49d79d6ac70a0f91e525099fc3e2"]}, + "captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "e0f16822d578866e186a0974d65ad58cddc1e2ab", [ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"]}, "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"}, "comeonin": {:hex, :comeonin, "4.1.2", "3eb5620fd8e35508991664b4c2b04dd41e52f1620b36957be837c1d7784b7592", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm"}, From b55f2563d05f2d0ca5e1293391c67d73d60d501d Mon Sep 17 00:00:00 2001 From: RX14 Date: Sun, 5 Jan 2020 19:00:48 +0000 Subject: [PATCH 29/34] Fix SMTP mailer example `ssl: true` and `tls: :always` tries to use both TLS and STARTTLS on the same SMTP connection, causing it to fail. --- docs/configuration/cheatsheet.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index a214b6e2f..cad3af68d 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -453,6 +453,7 @@ An example for Sendgrid adapter: ```elixir config :pleroma, Pleroma.Emails.Mailer, + enabled: true, adapter: Swoosh.Adapters.Sendgrid, api_key: "YOUR_API_KEY" ``` @@ -461,13 +462,13 @@ An example for SMTP adapter: ```elixir config :pleroma, Pleroma.Emails.Mailer, + enabled: true, adapter: Swoosh.Adapters.SMTP, relay: "smtp.gmail.com", username: "YOUR_USERNAME@gmail.com", password: "YOUR_SMTP_PASSWORD", port: 465, ssl: true, - tls: :always, auth: :always ``` From 70410dfafd272bd1f38602446cc4f6e83645326f Mon Sep 17 00:00:00 2001 From: Maksim Pechnikov Date: Wed, 8 Jan 2020 16:40:38 +0300 Subject: [PATCH 30/34] fix create service actor --- lib/pleroma/user.ex | 53 ++++++++++++++++++++------- lib/pleroma/web/activity_pub/relay.ex | 4 +- test/user_test.exs | 37 +++++++++++++++++++ 3 files changed, 80 insertions(+), 14 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 706aee2ff..7ce9e17df 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1430,20 +1430,47 @@ def get_or_fetch_by_ap_id(ap_id) do Creates an internal service actor by URI if missing. Optionally takes nickname for addressing. """ - def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do - with user when is_nil(user) <- get_cached_by_ap_id(uri) do - {:ok, user} = - %User{ - invisible: true, - local: true, - ap_id: uri, - nickname: nickname, - follower_address: uri <> "/followers" - } - |> Repo.insert() + @spec get_or_create_service_actor_by_ap_id(String.t(), String.t()) :: User.t() | nil + def get_or_create_service_actor_by_ap_id(uri, nickname) do + {_, user} = + case get_cached_by_ap_id(uri) do + nil -> + with {:error, %{errors: errors}} <- create_service_actor(uri, nickname) do + Logger.error("Cannot create service actor: #{uri}/.\n#{inspect(errors)}") + {:error, nil} + end - user - end + %User{invisible: false} = user -> + set_invisible(user) + + user -> + {:ok, user} + end + + user + end + + @spec set_invisible(User.t()) :: {:ok, User.t()} + defp set_invisible(user) do + user + |> change(%{invisible: true}) + |> update_and_set_cache() + end + + @spec create_service_actor(String.t(), String.t()) :: + {:ok, User.t()} | {:error, Ecto.Changeset.t()} + defp create_service_actor(uri, nickname) do + %User{ + invisible: true, + local: true, + ap_id: uri, + nickname: nickname, + follower_address: uri <> "/followers" + } + |> change + |> unique_constraint(:nickname) + |> Repo.insert() + |> set_cache() end # AP style diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex index 99a804568..48a1b71e0 100644 --- a/lib/pleroma/web/activity_pub/relay.ex +++ b/lib/pleroma/web/activity_pub/relay.ex @@ -9,10 +9,12 @@ defmodule Pleroma.Web.ActivityPub.Relay do alias Pleroma.Web.ActivityPub.ActivityPub require Logger + @relay_nickname "relay" + def get_actor do actor = relay_ap_id() - |> User.get_or_create_service_actor_by_ap_id() + |> User.get_or_create_service_actor_by_ap_id(@relay_nickname) actor end diff --git a/test/user_test.exs b/test/user_test.exs index d7ab63463..9da1e02a9 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -17,6 +17,7 @@ defmodule Pleroma.UserTest do import Mock import Pleroma.Factory + import ExUnit.CaptureLog setup_all do Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end) @@ -26,6 +27,42 @@ defmodule Pleroma.UserTest do clear_config([:instance, :account_activation_required]) describe "service actors" do + test "returns updated invisible actor" do + uri = "#{Pleroma.Web.Endpoint.url()}/relay" + followers_uri = "#{uri}/followers" + + insert( + :user, + %{ + nickname: "relay", + invisible: false, + local: true, + ap_id: uri, + follower_address: followers_uri + } + ) + + actor = User.get_or_create_service_actor_by_ap_id(uri, "relay") + assert actor.invisible + end + + test "returns relay user" do + uri = "#{Pleroma.Web.Endpoint.url()}/relay" + followers_uri = "#{uri}/followers" + + assert %User{ + nickname: "relay", + invisible: true, + local: true, + ap_id: ^uri, + follower_address: ^followers_uri + } = User.get_or_create_service_actor_by_ap_id(uri, "relay") + + assert capture_log(fn -> + refute User.get_or_create_service_actor_by_ap_id("/relay", "relay") + end) =~ "Cannot create service actor:" + end + test "returns invisible actor" do uri = "#{Pleroma.Web.Endpoint.url()}/internal/fetch-test" followers_uri = "#{uri}/followers" From fd3c23af63fa7a3642aef37cdb82561a5955310e Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 9 Jan 2020 10:00:15 -0600 Subject: [PATCH 31/34] Update AdminFE build --- .../{app.8589ec81.css => app.fdd73ce4.css} | Bin 12809 -> 12836 bytes priv/static/adminfe/chunk-0cb6.8d811a09.css | Bin 480 -> 0 bytes priv/static/adminfe/chunk-0cc4.571d0025.css | Bin 0 -> 2617 bytes ...a.6e185c68.css => chunk-15fa.2246593e.css} | Bin ...1.5bd2ca85.css => chunk-18e1.ed715f8d.css} | Bin ...b.c7882778.css => chunk-1c46.f36071a4.css} | Bin priv/static/adminfe/chunk-2943.1b6fd9a7.css | Bin 2321 -> 0 bytes priv/static/adminfe/chunk-3d1c.b2eb7234.css | Bin 723 -> 0 bytes priv/static/adminfe/chunk-4df4.e217dea0.css | Bin 3624 -> 0 bytes priv/static/adminfe/chunk-7de9.889d1da1.css | Bin 0 -> 7044 bytes ...e.b6944d38.css => chunk-7f8e.d508c376.css} | Bin ...a.062aa087.css => chunk-9bb0.2a82c722.css} | Bin 2044 -> 2044 bytes priv/static/adminfe/chunk-a601.62c86eea.css | Bin 0 -> 2784 bytes ...2.723b6cc5.css => chunk-d01a.03fe0a3f.css} | Bin 3252 -> 3252 bytes priv/static/adminfe/chunk-f3c9.155bfc51.css | Bin 0 -> 3758 bytes priv/static/adminfe/index.html | 2 +- .../static/adminfe/static/js/ZhIB.861df339.js | Bin 0 -> 11328 bytes .../adminfe/static/js/ZhIB.861df339.js.map | Bin 0 -> 49483 bytes priv/static/adminfe/static/js/app.19b7049e.js | Bin 0 -> 180590 bytes .../adminfe/static/js/app.19b7049e.js.map | Bin 0 -> 396651 bytes priv/static/adminfe/static/js/app.9c4316f1.js | Bin 167236 -> 0 bytes .../adminfe/static/js/app.9c4316f1.js.map | Bin 366548 -> 0 bytes .../adminfe/static/js/chunk-0620.c765c190.js | Bin 13030 -> 0 bytes .../static/js/chunk-0620.c765c190.js.map | Bin 63567 -> 0 bytes .../adminfe/static/js/chunk-0cb6.b9f32e0c.js | Bin 16157 -> 0 bytes .../static/js/chunk-0cb6.b9f32e0c.js.map | Bin 57112 -> 0 bytes .../adminfe/static/js/chunk-0cc4.35b47d0a.js | Bin 0 -> 26736 bytes .../static/js/chunk-0cc4.35b47d0a.js.map | Bin 0 -> 81998 bytes ...5fa.34dcb9d8.js => chunk-15fa.10871dbf.js} | Bin 7919 -> 7919 bytes ...b9d8.js.map => chunk-15fa.10871dbf.js.map} | Bin 17438 -> 17438 bytes ...8e1.f8bb78f3.js => chunk-18e1.9f7c9b0f.js} | Bin 2080 -> 2080 bytes ...78f3.js.map => chunk-18e1.9f7c9b0f.js.map} | Bin 9090 -> 9090 bytes .../adminfe/static/js/chunk-1c46.b92c7c1b.js | Bin 0 -> 7737 bytes .../static/js/chunk-1c46.b92c7c1b.js.map | Bin 0 -> 26010 bytes .../adminfe/static/js/chunk-23b2.442bb8df.js | Bin 28092 -> 0 bytes .../static/js/chunk-23b2.442bb8df.js.map | Bin 91362 -> 0 bytes .../adminfe/static/js/chunk-2943.8ab5d0d9.js | Bin 231394 -> 0 bytes .../static/js/chunk-2943.8ab5d0d9.js.map | Bin 689117 -> 0 bytes .../adminfe/static/js/chunk-3d1c.3334d3f1.js | Bin 4822 -> 0 bytes .../static/js/chunk-3d1c.3334d3f1.js.map | Bin 18519 -> 0 bytes .../adminfe/static/js/chunk-4df4.9655f394.js | Bin 17765 -> 0 bytes .../static/js/chunk-4df4.9655f394.js.map | Bin 66937 -> 0 bytes .../adminfe/static/js/chunk-538a.04530055.js | Bin 5112 -> 0 bytes .../static/js/chunk-538a.04530055.js.map | Bin 19586 -> 0 bytes .../adminfe/static/js/chunk-7c6b.5240e052.js | Bin 7947 -> 0 bytes .../static/js/chunk-7c6b.5240e052.js.map | Bin 26432 -> 0 bytes .../adminfe/static/js/chunk-7de9.7b8cda50.js | Bin 0 -> 29969 bytes .../static/js/chunk-7de9.7b8cda50.js.map | Bin 0 -> 117302 bytes ...f8e.c1eb619d.js => chunk-7f8e.2c3e63e9.js} | Bin 9618 -> 9618 bytes ...619d.js.map => chunk-7f8e.2c3e63e9.js.map} | Bin 39890 -> 39890 bytes .../adminfe/static/js/chunk-9bb0.9c56835f.js | Bin 0 -> 5112 bytes .../static/js/chunk-9bb0.9c56835f.js.map | Bin 0 -> 19744 bytes .../adminfe/static/js/chunk-a601.cc880efe.js | Bin 0 -> 15418 bytes .../static/js/chunk-a601.cc880efe.js.map | Bin 0 -> 53763 bytes .../adminfe/static/js/chunk-d01a.970cf312.js | Bin 0 -> 32170 bytes .../static/js/chunk-d01a.970cf312.js.map | Bin 0 -> 110610 bytes .../adminfe/static/js/chunk-f3c9.b3de53e2.js | Bin 0 -> 242980 bytes .../static/js/chunk-f3c9.b3de53e2.js.map | Bin 0 -> 745354 bytes .../adminfe/static/js/runtime.46db235c.js | Bin 3922 -> 0 bytes .../adminfe/static/js/runtime.46db235c.js.map | Bin 16658 -> 0 bytes .../adminfe/static/js/runtime.d6d1aaab.js | Bin 0 -> 3906 bytes .../adminfe/static/js/runtime.d6d1aaab.js.map | Bin 0 -> 16640 bytes .../static/tinymce4.7.5/langs/zh_CN.js | Bin 9928 -> 0 bytes .../plugins/codesample/css/prism.css | Bin 2334 -> 0 bytes .../plugins/emoticons/img/smiley-cool.gif | Bin 354 -> 0 bytes .../plugins/emoticons/img/smiley-cry.gif | Bin 329 -> 0 bytes .../emoticons/img/smiley-embarassed.gif | Bin 331 -> 0 bytes .../emoticons/img/smiley-foot-in-mouth.gif | Bin 342 -> 0 bytes .../plugins/emoticons/img/smiley-frown.gif | Bin 340 -> 0 bytes .../plugins/emoticons/img/smiley-innocent.gif | Bin 336 -> 0 bytes .../plugins/emoticons/img/smiley-kiss.gif | Bin 338 -> 0 bytes .../plugins/emoticons/img/smiley-laughing.gif | Bin 343 -> 0 bytes .../emoticons/img/smiley-money-mouth.gif | Bin 321 -> 0 bytes .../plugins/emoticons/img/smiley-sealed.gif | Bin 323 -> 0 bytes .../plugins/emoticons/img/smiley-smile.gif | Bin 344 -> 0 bytes .../emoticons/img/smiley-surprised.gif | Bin 338 -> 0 bytes .../emoticons/img/smiley-tongue-out.gif | Bin 328 -> 0 bytes .../emoticons/img/smiley-undecided.gif | Bin 337 -> 0 bytes .../plugins/emoticons/img/smiley-wink.gif | Bin 350 -> 0 bytes .../plugins/emoticons/img/smiley-yell.gif | Bin 336 -> 0 bytes .../plugins/visualblocks/css/visualblocks.css | Bin 5473 -> 0 bytes .../skins/lightgray/content.inline.min.css | Bin 3326 -> 0 bytes .../skins/lightgray/content.min.css | Bin 3753 -> 0 bytes .../skins/lightgray/fonts/tinymce-mobile.woff | Bin 4624 -> 0 bytes .../skins/lightgray/fonts/tinymce-small.eot | Bin 9492 -> 0 bytes .../skins/lightgray/fonts/tinymce-small.svg | 63 --------- .../skins/lightgray/fonts/tinymce-small.ttf | Bin 9304 -> 0 bytes .../skins/lightgray/fonts/tinymce-small.woff | Bin 9380 -> 0 bytes .../skins/lightgray/fonts/tinymce.eot | Bin 18808 -> 0 bytes .../skins/lightgray/fonts/tinymce.svg | 131 ------------------ .../skins/lightgray/fonts/tinymce.ttf | Bin 18644 -> 0 bytes .../skins/lightgray/fonts/tinymce.woff | Bin 18720 -> 0 bytes .../skins/lightgray/img/anchor.gif | Bin 53 -> 0 bytes .../skins/lightgray/img/loader.gif | Bin 2608 -> 0 bytes .../skins/lightgray/img/object.gif | Bin 152 -> 0 bytes .../skins/lightgray/img/trans.gif | Bin 43 -> 0 bytes .../tinymce4.7.5/skins/lightgray/skin.min.css | Bin 43307 -> 0 bytes .../skins/lightgray/skin.min.css.map | 1 - .../static/tinymce4.7.5/tinymce.min.js | Bin 834083 -> 0 bytes 99 files changed, 1 insertion(+), 196 deletions(-) rename priv/static/adminfe/{app.8589ec81.css => app.fdd73ce4.css} (50%) delete mode 100644 priv/static/adminfe/chunk-0cb6.8d811a09.css create mode 100644 priv/static/adminfe/chunk-0cc4.571d0025.css rename priv/static/adminfe/{chunk-15fa.6e185c68.css => chunk-15fa.2246593e.css} (100%) rename priv/static/adminfe/{chunk-18e1.5bd2ca85.css => chunk-18e1.ed715f8d.css} (100%) rename priv/static/adminfe/{chunk-7c6b.c7882778.css => chunk-1c46.f36071a4.css} (100%) delete mode 100644 priv/static/adminfe/chunk-2943.1b6fd9a7.css delete mode 100644 priv/static/adminfe/chunk-3d1c.b2eb7234.css delete mode 100644 priv/static/adminfe/chunk-4df4.e217dea0.css create mode 100644 priv/static/adminfe/chunk-7de9.889d1da1.css rename priv/static/adminfe/{chunk-7f8e.b6944d38.css => chunk-7f8e.d508c376.css} (100%) rename priv/static/adminfe/{chunk-538a.062aa087.css => chunk-9bb0.2a82c722.css} (62%) create mode 100644 priv/static/adminfe/chunk-a601.62c86eea.css rename priv/static/adminfe/{chunk-23b2.723b6cc5.css => chunk-d01a.03fe0a3f.css} (86%) create mode 100644 priv/static/adminfe/chunk-f3c9.155bfc51.css create mode 100644 priv/static/adminfe/static/js/ZhIB.861df339.js create mode 100644 priv/static/adminfe/static/js/ZhIB.861df339.js.map create mode 100644 priv/static/adminfe/static/js/app.19b7049e.js create mode 100644 priv/static/adminfe/static/js/app.19b7049e.js.map delete mode 100644 priv/static/adminfe/static/js/app.9c4316f1.js delete mode 100644 priv/static/adminfe/static/js/app.9c4316f1.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-0620.c765c190.js delete mode 100644 priv/static/adminfe/static/js/chunk-0620.c765c190.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-0cb6.b9f32e0c.js delete mode 100644 priv/static/adminfe/static/js/chunk-0cb6.b9f32e0c.js.map create mode 100644 priv/static/adminfe/static/js/chunk-0cc4.35b47d0a.js create mode 100644 priv/static/adminfe/static/js/chunk-0cc4.35b47d0a.js.map rename priv/static/adminfe/static/js/{chunk-15fa.34dcb9d8.js => chunk-15fa.10871dbf.js} (99%) rename priv/static/adminfe/static/js/{chunk-15fa.34dcb9d8.js.map => chunk-15fa.10871dbf.js.map} (99%) rename priv/static/adminfe/static/js/{chunk-18e1.f8bb78f3.js => chunk-18e1.9f7c9b0f.js} (97%) rename priv/static/adminfe/static/js/{chunk-18e1.f8bb78f3.js.map => chunk-18e1.9f7c9b0f.js.map} (98%) create mode 100644 priv/static/adminfe/static/js/chunk-1c46.b92c7c1b.js create mode 100644 priv/static/adminfe/static/js/chunk-1c46.b92c7c1b.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-23b2.442bb8df.js delete mode 100644 priv/static/adminfe/static/js/chunk-23b2.442bb8df.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-2943.8ab5d0d9.js delete mode 100644 priv/static/adminfe/static/js/chunk-2943.8ab5d0d9.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-3d1c.3334d3f1.js delete mode 100644 priv/static/adminfe/static/js/chunk-3d1c.3334d3f1.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-4df4.9655f394.js delete mode 100644 priv/static/adminfe/static/js/chunk-4df4.9655f394.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-538a.04530055.js delete mode 100644 priv/static/adminfe/static/js/chunk-538a.04530055.js.map delete mode 100644 priv/static/adminfe/static/js/chunk-7c6b.5240e052.js delete mode 100644 priv/static/adminfe/static/js/chunk-7c6b.5240e052.js.map create mode 100644 priv/static/adminfe/static/js/chunk-7de9.7b8cda50.js create mode 100644 priv/static/adminfe/static/js/chunk-7de9.7b8cda50.js.map rename priv/static/adminfe/static/js/{chunk-7f8e.c1eb619d.js => chunk-7f8e.2c3e63e9.js} (99%) rename priv/static/adminfe/static/js/{chunk-7f8e.c1eb619d.js.map => chunk-7f8e.2c3e63e9.js.map} (99%) create mode 100644 priv/static/adminfe/static/js/chunk-9bb0.9c56835f.js create mode 100644 priv/static/adminfe/static/js/chunk-9bb0.9c56835f.js.map create mode 100644 priv/static/adminfe/static/js/chunk-a601.cc880efe.js create mode 100644 priv/static/adminfe/static/js/chunk-a601.cc880efe.js.map create mode 100644 priv/static/adminfe/static/js/chunk-d01a.970cf312.js create mode 100644 priv/static/adminfe/static/js/chunk-d01a.970cf312.js.map create mode 100644 priv/static/adminfe/static/js/chunk-f3c9.b3de53e2.js create mode 100644 priv/static/adminfe/static/js/chunk-f3c9.b3de53e2.js.map delete mode 100644 priv/static/adminfe/static/js/runtime.46db235c.js delete mode 100644 priv/static/adminfe/static/js/runtime.46db235c.js.map create mode 100644 priv/static/adminfe/static/js/runtime.d6d1aaab.js create mode 100644 priv/static/adminfe/static/js/runtime.d6d1aaab.js.map delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/langs/zh_CN.js delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/plugins/codesample/css/prism.css delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-cool.gif delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-cry.gif delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-embarassed.gif delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-foot-in-mouth.gif delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-frown.gif delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-innocent.gif delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-kiss.gif delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-laughing.gif delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-money-mouth.gif delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-sealed.gif delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-smile.gif delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-surprised.gif delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-tongue-out.gif delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-undecided.gif delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-wink.gif delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-yell.gif delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/plugins/visualblocks/css/visualblocks.css delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/content.inline.min.css delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/content.min.css delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-mobile.woff delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.eot delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.svg delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.ttf delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.woff delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.eot delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.svg delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.ttf delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.woff delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/anchor.gif delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/loader.gif delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/object.gif delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/trans.gif delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/skin.min.css delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/skin.min.css.map delete mode 100644 priv/static/adminfe/static/tinymce4.7.5/tinymce.min.js diff --git a/priv/static/adminfe/app.8589ec81.css b/priv/static/adminfe/app.fdd73ce4.css similarity index 50% rename from priv/static/adminfe/app.8589ec81.css rename to priv/static/adminfe/app.fdd73ce4.css index b82fcc39e855a2a8b6907b9c8ffd69ea0651eab3..473ec1b86bbe0189960fea70c7be503c65500663 100644 GIT binary patch delta 428 zcmeB7S(36LSW?Aqp+Ff=zbw@6D%p1jM@9{?`3 BcE|t# delta 398 zcmZ3I(wVX$SaNfolt_;OyJ#b{uBrsg`ERCPteT zHG~-1p}ffs$`X@ U_YE_LNvgS_L6W7}e=Af>wh^!hs}tYtxQw%bX9(NXX(5S-bcLHg!$Y%SoQuAc$T6y2UT6!j{1zHL{uV4P*FVD@jCjbBd diff --git a/priv/static/adminfe/chunk-0cc4.571d0025.css b/priv/static/adminfe/chunk-0cc4.571d0025.css new file mode 100644 index 0000000000000000000000000000000000000000..8bd6a2e50292892916d3bf1b1e434b1e4bbbcd60 GIT binary patch literal 2617 zcmd5;+m4$s5d9UTs!~^~Bf>V>m3irZRMpC01MY&2%#cf?{Cmg7CJ@+%Zd<7@B=(HQ zb2)Ry894h*_i`!G$GR~zc z=@u8)&D;{nwE`xUFiV?Uc5ThhlpIC&DuHDBl#Y8%M~YgUrh9gsPLxT@@>lw!??4vk zl=u_#T`zTR2nDpmGlFCSjzXyGM%08G-Ew_giMgmX18QVhfzy;23lUtxGL(jyQYzwf z>CUDBSovSaK1<|K*c=li%q@|#UAG_-MAOkhafs0ZW0?;R37?R}0Yhn0>B9;TsN25e zy4Hq%*vI>1zfUu)ZE19`IHu7FilRvK-gKzMdeGNR2Mqd-g8ryiG(pCCM z8f_45uBR>@Bes18h|KQuix9@x2k-5TS@Fi+nC-5Wbk(N=vVkEs+TfN{E}L1c^H2>*bsOe1?Y)TV!3 znTuM0;Ql5!*Mdqw9OS957U-5Po5A~835Lz|=NbNU-VTX#t?w5{36<_4Q2jE->(>U^ zn?;`*q-rh>5+}jX;7|MK_Vf@j#ZkzxY*XPjPQIY@vsU#v>T-jF^Nye z*IDMUcMQU3OAR-P9dn*FoMaXXs#!XFdGV>9j@D@Vv?wd;j3cl zavM!|(0LO%^RYg?@y(mui{hPV32k*Wtflrd^wYRpkM3^)^q*2da9!Z&3Xh{pD0Cc6 zn|G=bjq_Q*Oi-vc?HMG3zxfWje#dXRH|7Zy7#Z+^8IVupUHlI(hQ>fnRMy K-JQW0LG}v*v(j+@ literal 0 HcmV?d00001 diff --git a/priv/static/adminfe/chunk-15fa.6e185c68.css b/priv/static/adminfe/chunk-15fa.2246593e.css similarity index 100% rename from priv/static/adminfe/chunk-15fa.6e185c68.css rename to priv/static/adminfe/chunk-15fa.2246593e.css diff --git a/priv/static/adminfe/chunk-18e1.5bd2ca85.css b/priv/static/adminfe/chunk-18e1.ed715f8d.css similarity index 100% rename from priv/static/adminfe/chunk-18e1.5bd2ca85.css rename to priv/static/adminfe/chunk-18e1.ed715f8d.css diff --git a/priv/static/adminfe/chunk-7c6b.c7882778.css b/priv/static/adminfe/chunk-1c46.f36071a4.css similarity index 100% rename from priv/static/adminfe/chunk-7c6b.c7882778.css rename to priv/static/adminfe/chunk-1c46.f36071a4.css diff --git a/priv/static/adminfe/chunk-2943.1b6fd9a7.css b/priv/static/adminfe/chunk-2943.1b6fd9a7.css deleted file mode 100644 index 0c9284744e242456ea5327b6682c1ec4f8abe280..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2321 zcmd6pTWjM+6oCJVAS}e-5t8lLS?Q&;u$z~b!akK{F(c_%9x|E{GviAt{NH=#B1?*s z-7WN?J7Km=xJTRG>H$aEVJtbz zsv*ok(=}ApOc2HDy~&nI_^yFNX}S}u%sxw#^9d7k*S=_rtvt#SAnL1dAC%(~&~n%rWN69yh?!^)J=@jehaL(7i7c2$FDYPzKMxR-Q1dKB zK`l9}qB{TxCNNcG(PS3&C2puLr5S+<@dTk1eO-loNK=8$=)0f|j4WMo{u{(Uus#XF zx1)#Ve28k7()09Vnk=yUW^*a{OQcH$AO^`^ zAHmJK?_3Jc5if}=({{^*UA@Dham!_VXtrBU3*ecM<~|mZ7}*lvE3;mWxZ2vg5$yCe zvsIjwbvPx~PUbchI&1Jo(Hc6-!zY4`Q}M9>5z5Zs6bR@Acr%F|g}uGDr^RfUIk!X6}~*E$|8)4CI!>;sLY0ZKl`O z(b&|w-Jh2@YzIAzMPwVmpo^~+HIy95X7TZ1xhmmX7ou}s9fUWu_vw3j?n2pL0FmpX Fe*ifSS-=1Q diff --git a/priv/static/adminfe/chunk-3d1c.b2eb7234.css b/priv/static/adminfe/chunk-3d1c.b2eb7234.css deleted file mode 100644 index ba85e77d555e97cbaf09bdaef884580de5d557c3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 723 zcmZ{iL2iRE5Jj)Trn?R@RBa@4l&XsX4`9{UmOV*`qTIchKokWkcB40s?{D{nio&-- zMmWKtXby^$__@NF>R-)JyAjan&dP=?Q>b8w&>DJ~&Io9xA+Dg((Hp$TCsXy9Et1Lp zm?dd7VCb}!W$DLER34SmwgW>g%i`0Iw|0V+;(iK|#5oz57hviq?DZ$HoyvND0=wXjexrTmwm?m-3v{iq`ArI|Qal$V z2ei>+m~Q2kduL2`+(~V8WQcq*1bp!%t+TY&Dn)fa)Q5Px<$Azwr>r|sK8Q>Y-6rI9 zDMutMGV(D}+*0dx2Ho{6%el$eyEFKpPslreXBv5Ve)Cdgv?b_i7JME2xSj=`oPWX$ B0WSam diff --git a/priv/static/adminfe/chunk-4df4.e217dea0.css b/priv/static/adminfe/chunk-4df4.e217dea0.css deleted file mode 100644 index 4672a9f758af89f89f69ce05a0216d8cece66601..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3624 zcmcIm>u%dR41N^@!+;H#3@1(6&i-@W<6szMn{l+tmLVxkvcT^?QWsykv>Ubs0h~mn zXkI>kWNIw82g7((sY7QkwJ^Qp=bRm(d=i$G>QszptzkO}r}-KEFJ{V&OVMQtDp(3+q)q|uFMZn3aU&L6ni_+moHTD zHZe)q+1%A~M3m#35l;?Eqb%gD#0G0LGH{IzB$dwJ?Kpjk@fyK!jSmNC)RDY5a4D5W z$7AnziUJ_pLGDO)gmj?M%{@};j8G7_*Dgis2_D3EME3>C8P{4JJr_qbdt$~{IGawZ zk_ug2M1l!Qd}m=uMRm{yWA2rpsWI#~5#%F(`v&yMaunk`Xq-%cI;|EIHj?z>^i5)0 zu)ZICB{x7wnt1ZX`F#A1%CR=C!lJ%-6f_#nMW6|*`rM_JjK}GiW$)vN%L)&MJf-9s zB}JXQgA!o34JO=dAbRTH$U0>q>F0w5ao}Nb^y7A$Ez&<4aU7R89FURAeEOR`uhlWn z5}daQ`H>~MDfx%&6MfRvM|ASUeyz2nMnVdDt(?;)_-xM-a$zx>RWW62NHz3ITSmZq zg0okiMf4K*LEQ=~r?nw+9GHMaM%kxb1SN;ErNzjzaYSG#RrM58gt@`M39|`5ST&?p z;Bd0M?Sna7OuQej1ab3mNe&T?96?MCm8Ac^#cs*y5&Mr0Ag>06{p0~f4TP1+1}q~; zT+!H0fUN(oaqDEyEPJ^Bye+HEm(X5!Te5reV*HLPE9&%RR}dN_+|9g(H~?ZQVq|c) zt{&>@{!2{6hMIGFtUhD&M}|g*@LjR6x)Tz~d3w*QrfJts+}{3TbX>K&U%hPztL2Pc z^$95OK8xR O;Z_a*SmA;`3_k$memCF% diff --git a/priv/static/adminfe/chunk-7de9.889d1da1.css b/priv/static/adminfe/chunk-7de9.889d1da1.css new file mode 100644 index 0000000000000000000000000000000000000000..29f7b475dfee937fd42276ef126a623b6ab48723 GIT binary patch literal 7044 zcmdT}3vb#w6#grbsw=J190dkzmq^w9AJa5Bi2<)k9NDJ85dZxiA3u^nO1l+yJ5{5_ z$B*;;&SSA;t&p0m7%iFHlMTxsxhA>T5?b+Pljc;hiZ?7HwIYj(ovh+z zvwcnFk~e9RIcBHRKC@3$Q%QKe+;4cPSLtNlZv9UD+a4zp1qEVwA{XWE?P3baLB#Zl zNzDsd4OCEa$qLOylQu%uhUjh6Q3_G5>n1~s9j^vf3o1*e2QazTqDDD8x&SRVdcT0* zMDd?&NOn#W%-1vn`Jryax}T?S>J7NK6QT*04M>4fRaaw@4L{Y6nj-CI>bs-y;i81D5gar*5E6e zZ8hk*5R#&ct{VUrL=f`b6>m|Z+2=6pTCS#qdawB0QNAuEAA`%;gg; z+VnIS+*Oytg#omBh$e&8^MLEcF`yV^+i<-SYwa=mRK`Do(=}7dAcx5XHZblDGysA- ziYaa!t{`P#rdI&{@qSI0&iux35u}vJ{7%Nb#o^2OG%$F=eSz``GQJvu8NT@JOL*Wn zOA%jDY$R<{^I=Pg@O50eOF8J36PK+>SxKyJee{~xGqv6I{nYBI=L8;jdzJ35)lk>e zn6G=WnmN$tQ~V)rgf=<;dd&nr@c*_F9Si3g3A?My+^;K~gwX8RYsYcx3=UhWlOOBXr52lf*QY*b$; zdb=V&WONLv(70jG7HG~Db7(}D655y&bX^IV-Yls3$-*(y?@h6(GiOCr(N?j2E+jNL zzJ+c?!?57+bexWa-qBficGLku-7t2@m!^&b|AuFu1D#MQq1C}jgimF5ECEm8ow%#0 z1h6`g1RN)_Ei8zX6>F(L=2mbsTRE&|zN`r&65HusK6$-0f(O$P+jw1IZpF*J^@62j zyR28AqgqXK;L6AeG@m|4CYg1HG&agrqT*$A!*a$Jb9V)n7^XaI*oK&>joNfMem>?O)d}KSjxhGMt^+(5dGeXUE)lKHYEr@Lkn00xNG*uc11xj=W zTcI@a<#)IZ8IB{YOJGd+6AIcL82{mn!k?Mqq5cs+J+yTqQ*I{-m13;cd2I%!cEsR; z*Y@RBeJR)ocr~#T(X|$bSaWDaG_70zaAGa4`u$)}nODzGh9PWr%^0^!Gf{l`Q2%3f zDr$LDD2Quk83<(bB4}oyFE2D%a8{LHOPKwVCvtZ`nJ1G>c)5tD~LG5 z=mg(|%xc2{W^zz?;gm5lov*jAmn@lop^MpF@y#n@W<0<9n0x^c@|hw8+2Ga%uOLZu z@0?!kW8r17>&WR4vyii~YX@3a?!MPZ72_O=eYr!ZZ=en5ChJD)zIJA<;e zq%i?877s6buJaAhzp>p7ZQF;EKbe1V4MEj8{e!aq4-?3U`w#2tRDbTyF2+p6p)C&o E0gxR)-T(jq literal 0 HcmV?d00001 diff --git a/priv/static/adminfe/chunk-7f8e.b6944d38.css b/priv/static/adminfe/chunk-7f8e.d508c376.css similarity index 100% rename from priv/static/adminfe/chunk-7f8e.b6944d38.css rename to priv/static/adminfe/chunk-7f8e.d508c376.css diff --git a/priv/static/adminfe/chunk-538a.062aa087.css b/priv/static/adminfe/chunk-9bb0.2a82c722.css similarity index 62% rename from priv/static/adminfe/chunk-538a.062aa087.css rename to priv/static/adminfe/chunk-9bb0.2a82c722.css index 9e23d0fdb80fb09a99ad8017c5e8f3b54830c9d3..c0074e6f7803f95e710260f30559db5772ba4a77 100644 GIT binary patch delta 141 zcmeyv|A&7=7Bh!wQj(#unW5R_BIYav=LvHIf|J7%gy0-w@kDUMSvwG%<*Y6U&Szwq TX108U%rCaf$Q-H3@7Z|)b)_k= delta 141 zcmeyv|A&7=7BfeRfsuKNg@MuJBIYav=LvHIf|J7%gy0-w@kDUMSvwG%<*Y6U&Szwq TX108U%rCaf$Q-H3@7Z|)cGfAn diff --git a/priv/static/adminfe/chunk-a601.62c86eea.css b/priv/static/adminfe/chunk-a601.62c86eea.css new file mode 100644 index 0000000000000000000000000000000000000000..a036b0253f933dd2b3d1ef7006721eabc6563671 GIT binary patch literal 2784 zcmcImZEu?}5dJGj)1*zRBcL7a%$NO-X{rod60gQa#^hyD{`>BXNkSm4x))VV8{hd} zpL=|Z)(O|OtQ5A27NRUor`^H9&anzgV}+AO^G;V_mDG^a2U~+&tsPIZW|uFGcC3}Z zfG79O?zkA)leJjEVJj=Q=4t$KP8!L26*Q7~qSCI-mes}rD>~ z3(ki$D%mzl>^a*)@gf~7%q~Bj)h$~p*j=8{_xwVc2_;vWm!KWkoP`_XQ?J6LgSrih zS6L1*ZG;n+QMN+}Qu}Ha%9Jm8U!(+c%0qem{~=%N3;tHZOp6-k&G&YAJQz>YTg59( zwLjUoD8~CLXRO4k@eNofOQA+pt)zw0<4PM_lW?f)SyCF+)jFq+uK9 zv!`bFITE@;p}@9X3X?c}KAho2_!^&1)ZD7 z5gzvKn9RJ#o1SP++CN0gFjiYD8crYmJe|JIXZNoF`d=yF*hl%jLRMuPmVC{!GEE+S z94;7ZOw=cm8Ik&wzRx`Oo>wFAE758c=K*p3Gv$e-4>wvT(})LZj$-^Fj2@jR6x>ww zF!s8HGp`#hi1`tZYIzK?)Q!qc`omh`H;m~nq|iYzG#@x$pEfM#PT=o&s0{btOk+>Z;a nk;O2~QeuCIYhS-K-DG;tBQvjq9&^9ap6TXkl8C?QH$MIb5YPBj literal 0 HcmV?d00001 diff --git a/priv/static/adminfe/chunk-23b2.723b6cc5.css b/priv/static/adminfe/chunk-d01a.03fe0a3f.css similarity index 86% rename from priv/static/adminfe/chunk-23b2.723b6cc5.css rename to priv/static/adminfe/chunk-d01a.03fe0a3f.css index 172bce31757c9858b49e0291c6e5ecb51690ee23..f0b3bf1447fe6b38c52beb47498993a4c9529a6d 100644 GIT binary patch delta 117 zcmdlYxkYlqeNI!e#6)9rBh%Q)H(87)OEXF!iP&ARdrYz6X*CoE%{DY7fZc>b%xyQgJxOYxe&tI!q`& zEbdrCQGwA_|({n6a&U=~#n!f0x>AoVCz6McDU9jsl6IOi&t#u_DYwdc?=>~W% zq`6OpBu2KxBR=V`5r=@4&R{1LiOtoRlY^RCJGrZOe5^s4rd#MN9zGBUKkj%*U#|__ z>aHhfUbC_UaZ0c1^x=|(Hk{TA{z%rHu-&5Zl?7KeS40R+W;wj)Ft9~GqN9UzfUIk! zX0CbCjh1|8U}bTbKO)(#sd8x@jg75a6|&8@eIu=GEj+=mMd*o*{LDhH>4IO8utT)1 zBkIm92cm` zoiLnIoNI`{mz>!y3>u52J!S`?p?$c&k-U%E$tkt-Sa-?lF+qH_?^=A`4rTw3VI#QX zN1z*shW(16gzpsp_R(m>u=8LvTKy{TPH8C*!eK~NQL{aSGMhg-0-bktO%;?l4-~KO z^ZFJ&Z&|p zZ6Wy1;!4Q^graX~NexYIcB@f^)>M<>>%v{Q?f7sgzpIw^hfPAh<8D=*Qcd^Za(RJv zaLvVYHOKo5;zvuC*l&kq?ihm2ni_BHeZ+_-{Sk5BMd!$!i`XJ2S!Id7rB*?xvhV?y z&oDadVXRE<0HD{M0FI3v5%kla9;CX2-MC%PzH~2l KXj|Bw=KTYI%6T#X literal 0 HcmV?d00001 diff --git a/priv/static/adminfe/index.html b/priv/static/adminfe/index.html index 70bb8bd3b..d238accb5 100644 --- a/priv/static/adminfe/index.html +++ b/priv/static/adminfe/index.html @@ -1 +1 @@ -Admin FE
\ No newline at end of file +Admin FE
\ No newline at end of file diff --git a/priv/static/adminfe/static/js/ZhIB.861df339.js b/priv/static/adminfe/static/js/ZhIB.861df339.js new file mode 100644 index 0000000000000000000000000000000000000000..aeec873c88b1e79bf36f41693392ce8a7042fa8a GIT binary patch literal 11328 zcmcIqYjfL1mi?Yz0by+wT4;ixoOP`sL!5XgnaWO_No71+H6a-nNH(R8L4ef&C2L5| zZ=ZAffd@%B`(dX_1rgnS`*EM=-bU~!i)Hy3J&O4zTYUPTsw_6?nYR6YGGozZTipkf ziT}&}_wW1}-_ieMwJjF4EQ>&J#cQ^E$dp%bdGpoq>cacOMu|$O2l176xluIv8(=?E&?{@2_Y5lYjvN>L z1254T#tt`*C0<5iWxM)b zR?$aUY_@eOxOscX@~ueKL5X^+ApVw*g<+)RIp6Z?My>)8$;yf)C>WDz5oz*(-H05L z#SoHEvEMh+DljT;dg*{>fTF3gAMG?~ZpQdktz>c6h}OdttDx*K?eem~B;A(YB4vk< zZfMKd{r;kel-T6iLIlIfukql`EF9i(-)B)Sio5!r&gC|=Ta_xvJz01)?INho=$=QQd4o4FF%r-Mv`ZMxm>Mf5Q; zO!`nl9CM)<_;@fPNKFeG_IrH{BUrBxk5B`cG&``!T5alUqxE@5u&w+B+gVyjwy5f? zSisV}?114PQYK=nFVl+= zpVMQL`aZv-#{*tT^;D|%`ei?vUwFKM*E0lvBv)x@tUZ&akmMi$zXK6hj2d~ zxILWgJo9O>EWNnEqqdqb%z{t*^?tsu*?361$tI)O38`mj!7|Thy+ujnKa0C>KW~Cf z=x38p>v=sJV-R$m=eOxdjIJ*)FS7mKuVHR!HpaM=xC?!IkeLw@tTE3aF0O`}f+hJ| zL#d~DV`rE@Kf}B`!5c#lGEM&4lCPg9Uw6r0Lw>YA=rsK+OaDpJ6BExJ;lA8fS+O)n zc*RaNXA;e{1fYF=oBWmSxEo)!|{9MR& zmWMsk(JX=om2^4BwFKFE1_KmXSt7A2t{1vr5dW zM@de0n_4k8#6!?xO14{Mh-4>5U^OV&oJ?)i)A;C{v($FT8DeW1Um~_c&Ja7xJitHp zywsNa@IkNzgo}oo@ixBsI2f>P>c@V#y|E!+bvqc@ z2|Fgx#L-~7WWhMazul_?3xn|m8W?%p7b8FHDFVQuHb%rbPGavE36w^!LH=o-Zo|7U z*uXjAZ8=`2!SeF*VGJh^onhfRwC_y6CyU!O#vqz;F%B?pfOa_NaGhXi`<^^tiFJzZ zaE(1w;56Ote_Rk}k`UuG=jbzdh*(alAT5t9J!WrM9x7H zMgn?jfno53$LKg{}pWuj)GAB>aflHfG}& zP=nmMNC}KoUl}a0DIbGT%(s}2S)hb27=-o4umtDuG{?_yvG&|9H)_kjvPx`S0b3j(q4_cgFtRir;I)Mg~m<@S|)$-#O0*{#31L+k#-0P zz8liP2$BO}vj9Zt@yO2V-e}6<9D1`Z0D+B#D#R(Zf2~2N#B#e3JwmpF3`xnzG?6c` z`d*cf8a{qNs`af>@EZO%N@Y*cMwN9*2Y|ZJL>_XZTr550gEWJOEO3i$u4EyWJ|pFC zGq($Oa&>kE75bj6yrQhV%yY;RbCAEPk<zW;nr=vjcftTI>dI%&Z1`t5XH^iXk&za^zLc=R$whNIxZT zhEN?LS;qt==(yq*dpL4skNuYPSF64?71fFK2fiqg7wg>ryKGUHorM|=I+nIppooMW zGz?`{2LdN7q@1pf0Nm6bB&eGMirZo=lH$Ne&_#<;I_IQH1o)v72?^HdRN}c)1AwVP zfYdW?rkCk8w7^BSabx}FNA@|`wv6BvgNY>-Jbjl{Kp8nuTOdLof1zZ~1( z(fmG}pwSlk;Ysu9hw@RV{|2B55NwYI(5hzkKW25-n-%tRA((X^r-6@5gPdVqcIM|7 z$n0hvYq6fjY73GIW*OLe;irmd1poia~PvJ&G`X$QB}la>Ox zYB!3eCq)E7iUyxVtFT3GorigoAwGI>q#7tbt5$zb{9p)8@75KDk7=NbAlLC@f61eJ2+GOIfkpl&75 z*&na?LVI$9n*bwh1XIvK6;jT=KyF{=B6`eJae3K&3w#7gTRlv?|E1EC^YUi8$oszb z?z7693sHEBazKT)rB|2G8`vk~hA&F4q0ug1`gOVtUm!s?3VWgbzfbv~a!sR~T0QM5 z=xnR;@I!}%uDBqy9(B2KSi}YM5{cFkK7nhf`j`fRi87tzqz5qyfD0SybvoQwjeR%_ zKRMsYFLX3U%ttK(epK&x`0E6R2Kdl95~)hHor8l&n)m`49!8h8{&PfV=!jb+E+W)m zAm)RXG>_cYAU!i1fS1X_J1rLCc3K?N#nMPUIxtX%{_gq!0M{IV_7QQ^LuhP9`_S1m zqMr-q*VNI5u#2pdA(OnNtTene>Xe*No?r1i zHG}5Brd9<&b_0`8B+kpCgSVOpD*}zR4&3o&AXd{v`P;C5|M;!|(e z_PiawfY~PD2)@8AKu6*hNvAv`9Su)iK=ic0Cm`&h4n@}bool-S+<4$~mjfr-F3VHD*? zx<=cz5ie<~AytB2Tx02~UxM%l!|B$Q+mVU?$!{tr{u=c_6KWEF?zhzwl&Epdrk1kE z@}7E-fK@79j$$WGgn)nkGoY4uIEvZWukU>w7t|Pd%ugsQz|UgoCzK}Qr@~Jfg=|RQ z5-y&SovA@2{tN#AeiBACND4WuzTm;}=9>zqM+e>fBm`R5xJn!b?|%9#8*+Dh;~LI6 z_ilazbke_<@GuWbAb@nJc)7!=D^xkZ?$!B+(`t;NLtWRGdYZlm)i*N5QPo?b=u8C` z6l`#LXlmK6c#QHZ3LZ$NLtpO!07gc?m8hJc^u`jrg|>^!LwnH1S2a2o1|u=}`if50 zud_k7+C*yfpU^MXx;mHO?&e5c5)@|P&Q>c_u&ZR^zXRj{Opo{U_!B)o(Bmh1d`pjC z=<#>lZ6m2n?Huv-*C&%m8uwm5|D>NE^z$eE{8m4I(a*o*iOC9rDRv|9_65)*74%)p zO6I`wo&2|P)g3Zdwh`=Yd;kj!Q}@8;MT*j9*xpU*gT6y070QG%CWQ5j57T^_oQZqle#Q&=sAjcA2) z)kM-=Y|-d$hT2W`7AU#lsky1R$xto%di-5xEQ9nD1Bc(@e3CGAx zek;4`WDYvOT_@_;HxdQ-9Y*#%0&d_M#sEG6N0sf-mEnhU`#&XaHT8tJJ|aG3S82K5 zk6OfaV29utC+IxpSx-29!G@<=c3js1t4u&ae(NeB{b~k81WOjs9+5QHqrV1|Dc!2> z>3+A`PhZTg*ic`_Wh+=>JFpqYnsLYikR;#}3Fu?*vKaa}eSydCds9U>4nZE7ZK1GZ zejre|j|--QX)rSr<&oYKM(O|kLAaaNwII4kMjf&zbvLcO(s`~Eg=}cB8?3K4+zVYi z#;7{Fcbik`8l=4rb6qGwnhP*QJ$QMu5vnZ*lAo04;V3%yR%G8t6lg*OIia)bBjMEb z(6Zr4V*@4GKnWXYwx^kPw7U{^?bzu^$MST&@2}3ERnyR9PXI~>Eyj7 z=-lTgJ>6W(j3&R4&*BZh45W_*)t1j%n}9ipLD$_61K0LtD@+4-Bhl7#tS8r>=_tUE zScI{kP*nlaFp(hTrokvVZ&~JxCufGw=30pCE31?aD8OGzSHB*yr3P+ zt8_3*B*O4VoQ^I$f=R)f=z~ir$!0Bz(O#}iL5S)&MI>O>tg5ccD=|G7l^m~MJ~CWjPgUqVFHg?X2A`9U>8M%;@#v~f>Sv`=%k_DABz<|{^;90+FG~$@S>+|duNc0L@kfW% zwq#lZF?bzc6<6rcofhlpf>zr{V}N~=Fi3w`qxAQb&|W5>eKcFYTwPf~gYnr6 zY+)BKlwe%-M`!by02}s_2`*+Qeb(4_H}=oFgL#4y_kG$Qk&gLDao?Pe4hR-SljJ!0 zc!rDq)azjSqWeN_o(;MO1T;uS$Fq~PK6`V(a0zxC@aA+j>5q=zi~&gzOD~*wK~V1l zYL2AFn~X7v8178Q;5?vsI1Z97OWL?Yvb(+B1UwVGx;>)Om-4eoa?lqg0w(=q72WJ) zJfC()hpaZ9&y;a0H8vYdljLx&yq`?Egaxe~^kAg8d~#KH&&D(K!)oZ6AP)}aLwt{t zkL+v6u7=%@5Gkc7k{2t|>mC&>Ieh79iy9ejr@{nV1SyfnZ)Q#4k_Qt#pr6x(ESQa- zu|c~#6=M<8-{{V|xaHU}Nu~+01O8`VE?dF&`cp`j0F)rQFZ_bp_7A%=yF|eO7&V{p z_rB@o>x4$T@3hE%Qo^sDXz2wU*Y>F!kcQtzh(*CH84}h3+{}2uJ}$ZwvJh%TuCfGr z04>AC5pNs}#v_)ByGiw<=vv5VHWs?OhrFg>Q;`x|mf>p~-#e^3@69Fka$#x6}0gjDkeZdwUNsReu zFc!^*TFni90u;nm&HBT{$qxQ`tY)#lZ}D({lKeUEPZEh#8W9=&hTXF>I7&p$+r!N` zjvL$fh#MVzwEv}luj04U^>*VaiZwkFQvkBsml)H#+E>0>t_w#)ZW{;CTs%P6nr|kb|sIcWXnp}>WP$WTdPw- zekMgvNyqEB@fsx^DIxNp8yze1yxu_xf#RRApp%yLe_W68y5NxgABWF+v9`hR2tQS-U{QNp|f^8Cv5bodTf{YA^qYOjM`li zyj3^`AGYGrX53mA!$Fl!JXU1iNy#6=F(cB1p^*9;LZFpFChhol2e%D=kkh{jhK)YZ z@18pzkQ0T*ar{BSmI4Q-dOL!@F`%>-y`Q>8XsehbO9on3TALi)ZGfdNdzeCVw&O*Z z?m%{1;0*k*KvYS*5alJEo%pqsgA-fv*(UYMGxZZMADiZ7-~w&MwET>2LJcMY2Faig*&_^+%187PPj9{ckbYxD_s62+?m3i=HbHl%JAJ5 zK0tG};=cs$j~nqKRVLRMB1FXPSWUoG`5p8l5t&R>pD#7awN(Si7>gPzxfSvmfK1gW z?_Z_4(?LdCxGMIhzCCNfcGftKPpk*Jt;bM}4$z=00=v|;cxI)}2yIb@N*nm3N{L6r zHR&50IClSsKxp}Z?rn$x-Pf|Ik}60ZiCQWPyn?p^!-5`(IYKv`c&;t4hOJ=~@mW06TlL4cOF>EMOg1Bo#~P~?itp%Bfq29(l)!wHh>Kj;^;R|*xJnQvUEC|I;Z z^=?1hSk-F6WqRie$^8&Jx17Y=;2q@-CrJGmwWXKF3eqmfl)SazX9@&Ueiv`u*W@|0 zTau(q1w^N_n#Zct%#&_Ib~NlZKB*NQ`BW8`-d~gzci<&NOW9BpUqM`9yb?x$^Ktw_ zuzv0e+H{a$#)owa9Hf@w!8bms{0R%#^iMiOFnT*pL_Sc0Ncr&j-_CR`;A-^MT;oR- zv{Lnl0HGQJEU{J3QM{B2+Y}ZF`(DMTsV#U>)fJ9NHfgB`ya3r{^(C#)-~eebB6w-?V`&V~)Lvw&L{*9`!gWysAwNe32x@uv zSUF`m3!A|X=xxv|f5F?cnkrDSmM{oWk9;b*`#UJdU7uruzpA5?bEE`}=NtBI&^7k8{yKM{^8Y(EUK_2C-4b|5voW__F2*5|p!u*zt#|r3sb?Rwi18gYX@6@CLhrfVLs_4G8AbtxQNiV9tMwiDO zCiG}Rl}2MMGl-HJgEf#LLJ?ukvCwBr7sN3?;&Z6}pb{K!3ON?k*-B|pSFd##DIhPn z=N{4l8P5>1&` zSrFz}?IZ6zWk4YKw=fBpz|hVdL0hPXwQKY&xM6Xs62DHsUl) zOc}_q``l*4E;(<8hExU855pvy8hb_m&J;oxvC$cjiUFB53Aqzbw9=TVk=gc^x?q`u zVpK5$G;pX%w{WiQ?pb9{>Ir|;fqdlMw#XHJK+`TMi{lHn-;Tf6_RljSM9LV)r~xwg zgBY1y#2v9ohE$s!*vZ- zq>X&2xb^cP)mWV5W(-RtUta=JCtlrD)l@R&+K&(FI}ojhyjEP0ZQN32<72WFbYR{?g9V+`sj~S{;GAthRPET(SPlQkN2z#B9Xj!!gd;Nr zu}$pRZ6JbqS~`dh!vm4Z?!2P}U!fiR67MK!vrQpHlmpaL3>cm5x6A&SpTWF=OUnaU~Qy5vx7kTj{{-E$5i!?P*TZ#h+H0pD$K>vsJ zl`SNlaGT^qBA=&ZJ{%mmJ5ZEGaGGK35DjTOH3>6Q{CdJ^cjPPNvP}e&61OM;(t5+7 z!D9p>)Q5-CG1>3ZvCMK zOr>nb7wVn|%3ehWy~y%mZVRuR? zQW|m({vtz!4W4LnVHpqnD?4n?8~_n@rJpf20xA{{u4;TFiE+|I zO`zY#q_osxzAl7g~PnZkl=Pf%9y>>al%7<8!#7y5FO)rgql^C<)5ZXyHrYB@V&^GpX- z7~RlG-~*PTMMj%g5Ngr2gBv1Ke$35OySPT4mHAkVdWbUyvh2$C73?5_L0L_FJrOlV zWYQ<~M#mM6A2C^sYx)4Sa1puP#6%P#3wnC&Msa`Y=w?gg#k_%}5xMP5f}TfNRcTo( zB=}?q$fQRR%3mr%WP)m@kf6%edaV4zNUP%pZBC^yW0Y4Z>aM92*?`=HnkGY73|>7a zRH0#8oPPr^V$2HhOHzp^81D;}s|ys45+AFNh1l5kJEi;nf<{Dc(E>Id1(?BAUYb^nTV8Ub!ByGm$ zzSdV;>LopMhkmbMJJkGBnlNUeh{R@mU-d3n-OUV&182b)+!3moI0~i6Us!;VA-_z+ zmGz>&mmA=}=sE6Vy^BI53*Z}BI#NCd-XsxFwpGU2jLspm}%SG zxC_FI`Z1x}o{X!y!LWmLs*iBJ?0`Ea{d){DpfH_O249~dG?06zSkzEg>&*AhqWLoM zrIhhOsZ|*1Qwam`t%Ub7o+wzG;8gdbhUo8fxzPD=4NyPmeOrfs0byl(Tb%a8;LdmJ zSR0|(4iSDm3*;o*O#%nzHFW#|M!+$swkayAI(mJ+t;Plxte=@gfqUy%X+=Jd z`B@rmPy-Mo(`WpLFJjXm{9fqjrlq&kI)bGwTk7FN1{ii7AVP)hM=*T}AYxb|T8BkA zwNhIrHoTSd5RX&>!aufA!5Oyr=q>mwk5X-c@phh4TPCXl>^2pYP;Cj7-dV^a+76OG zGB(9rvJjR4aY^9v+`A@X!pQ~V5`7)26_s{Qt}PP5wmN!5ZB8ugIMr}o12dlapw9%y zg8n-=kgM}ZjIJ46FjXfjH8b_bfd#>_GKlH+OA))O`tmm0V0)C!Q$AH}pTw&{W|<)N z(wlZpHq1Ax*eqUYB4P_G!chy8R4SXC0*VwDfpC4VB#OzJE9;y)`{NI@eT^eRgko;a zj#TNoe_GIXJl18_3@z?wB1kZZil5zhODFxS-K~yd(mNupggpWxCa=PLM&HnP>)oB~Cte-5oPC{F7z zn8<4#)A*}n6*uD$orTe<6JIKV4}IKoBbqKJ+;+eTbx<~h;n)k4K3+C4)Ch zM60tU)%lg|DLCsUc+pS47l3XZZKx+BBGPT(Yi~DHGpq$gEQfX!{v6{a2Q9U zzezOyI>R^)SCHvb@fHl@=>AcQq9_~2v9)E7Jx^`@+!Ha5#%F^)#}OI(Nas`;Y9i&Q zFr4_rQH4Y98;cLKscKdyI)l~c+!e6VJOY>@mDX!a_i|i`3WBaxpvaExnxR@V5=>}% zlVfN}6A|&cy6l8**@hSD;G~h}pwfx1Mo6g|u$6KqbhR|`#~4j9u;oeffeeUPMG`Eq zL?c?*Q#(_@KQQU)Fmn?fc@aW9P~H zQFJH5bAEhE6Ak32Jq8%9MfaL3%?DD}o#P><35w#w?l9Vzbn$qU+~9LOcn}SDo^QXA zl9So&43C0#QSstXDmIV%vy*wRc`zQf@US8Kx(jSs8z1jS^&@-KF{&RA#=Y*K5go}B zWtU6QC)C1U|0t^S@vHGsq)&WCKmYu5RC5h$(SQAy-Dq})hiNl5)%tY@@m~t?bCe8# zwg*?A&{4Q}{t-{M2~;g^+VedDWY59YEw>x)?E(~$O@?pjn!Mt(1>kv+M)27CttuQ%S#p5-mQ9X#C& z_$!T-dN zk-6%uw3a@pU#sk9#n@E|n#xS*`$WC_;wrRRy&Pl_J!l!8$t>bVewj@-Gs|PN$!IIy zFOcYS(+S*2o7Usc{FKdg`w&yRQ69OIE$cr>ZvHp+#j5VyL8{F zA8`fzb{%<`6pElE&gl1>s_O3ihf@5U;pYLF#pN*eFtx1eQ;53*_d>j?0TG-+19fha zWK9x0OFxQ6$%S*_+ODBqDb27_{Nd|e;tE>fy%h&Xx4ZXqWNC}v5Q!-cHAozjf)L>V zq~6l^-iRfEni$q7hH)S-$20KASV1@4zOn7I-}6Z_Kqj4r0}FDl&wcd8|DQ5`f=p91 zi!sw^ngIDOLu3QVt{$fli7!9_&)C41f9iyFLN22taLS$V0b8;_QWudS1r?ocMEUbR z0-_0sK}(Z6pley)t|P;_wIJ8o%LOYz2Pb`yy*T6{DY*?eIND)B1^)~ZR9h*5K}O(K zEuZDFd&B@a3%yt32K&9vy()!Y9)2t=@3?_q=pF<6-v>@`6#$m#WWJU z$%9xLk?PJt5@%thD>Fq*M;(C@IGgFeRnwkuW9q^oW>}8;)yb zsOk|ishKHN$UR%puzQ+BQykI}UBKECI0aMjs0&*_b|HEnvc!2W{f66O01vBxV~_w3 zplRY#jfWm8tcCuki3~Z>`4k6C3=X3?#-MaPquB`tx@u@V3oB2 zzi>)Mbb&!N&?bip{h)qt%-f(;hWeu!0a&&k&BlJGVQa=kXShhT>@=Oa^8~eetvoXF zlpZjFLnBb`c<#QnIn`7=3L?KtR^Gp_|NZ{`(w?O&xwhUgeGyX2$lQV;SH$~n1>UM* z!0amcE*Sw&2nwg<0|oXE7+8}kxYh~Ew#G!)4zv*_x_$OJ!%z|o}~J1v+GA$`R?i-6FIv5g4GZJ zEa%`PIXD&bkTYbWo*dwEg(tg;*rQ}j`-R53}S57nH8;0E)LBjJiYi=M#BKTKEhzwJ0tPtUt zK8syBB9LPItbU;B&dRGVHRWu!Iy>53*~_s#P5iQ$;#FVGS9j82u4QRN8j2eQLULmP zOJG8L+%L2Sb~gNPt+b9Mo7mlRlPADCM=V@(ihW@)L_Su?G;IXt?^Xzv&C`0IHN8%= zrkV(^Kr$QhFKrMsHrArooEWqb7fNL8h(=i|WJesj@nA+Y`K2>U2xn=B2;^?QsW@nC zl-4Uj#$3s#kmol*GsVY{DdJMbgM#X_M+{lO5MzzgmsFdJ7*@#L0I128DhxZzB%nj+ zW_GS5E>$|qu9lBX!Bzkn)U?#G14I-;LCi3pD znh2FlrK+x6E%buGQ>X3cK2i6$iX3hcH$kWPy7vB-_b|-$H%` zJ=IBrgRp2Kv>Gjg`-NClXKnusD}(!mSY4546SXR4L7g4u{1)vFi=f{&MuFL+Q>3m@ zh3kTH!EIFypH&_4*_2~%VgDr#U1z9Da+HDv5(fga(DGB{hw)O)1wj$Q6Xr@r^e^201v58M|L)yVFl2(#Qd_AZq~W@RkTMoK_6Y=_pQS0kVB}xgnRZ0%yi6&b z5~;s`f4EeC^fSLd-T%C_Tz_)J@So%psLBECpPbX(l*Us??(hvr!5TeOMiImaDU5 zb#n4t5LqBan-+r4u8w3WR&EZ7aSvD#0U~!-I_4}W>71KAN9#_G~Sf5hSgF; z*rn^M3|z?ag8ba_g&e`{wfA>8z=ybHp{b1t{?QUdxE$pzLFkPF&7~_1I>4HSyD7?6 z7!;^GZX24a1tk;AK}bf$Pa*HfcK$iDTZ`_O0AURmrJ@HF69`&)j4MaQn-nV)#7S)3>n=TVwU-_(J^H33$dD-hQ4-2e=L&AH_OC*|V+%f5>#DC}u;F8#qaz=Y z6$kySO6GVMGV!ZgQL!)`woqP(J^t!Zv382Salnluk>R28-& zyGtpy;gW*OOQm1|77e8};5uY{e=GZtyqG0*3l?mEYzvHHBeV5NuxvBc9ZlN4sxw=Q z^ZHXtAP0FVL{-NoB{x~-D%0TvOH%=5@^*=`uH4%uD1E#M;)+gnQ~>+1%F9@Owbxa^ zWDPT|q%|st*B4rN_;mTIi3SF(g8|=bft`g7c&9#(@BfXP@b0td5^Yiw(d6W=tLB9kY-P4>> zAwj_H!YGdgW%#A-;RU(!HyY2dG@QY?3Kb?*oafq+eb05_?NUiHfMFSGxj|L*%P$yH zl~g-LrLuBSZ{U&{6B$E3q%RzC?uGVozf?YEKUmwgca-ej%g+>7r}h^o{Xv55Ygl$7 zEnJAe4@9+D`KmVYJ6&GR@sFhiak7x4oN}TEz)O*5r7{85epQ9SZXvwrV|EGZpx(O1 zKN>s5)=a5%0=E<$$Ew*bXecl#l!C?nG_u>f$>)!0;r>-eLpO#(S%#c)JMqLCB7fk1{vs zEApKv!Anf=a+~vHaEWcMc)QOCVRdwjbe>zC@vayA#O`Fc0PNv7jCz+r-*_n%UQ3cq zcsKpKeDcBmGPE++7S%r%I8OtW_wK;5KKio;*@8*Iu8d{U%$e0sX+)OYDlU1EI^N{H zCmug2Zg2&LNTj7{Wn>?jiDF70>hUE%G^E%6QPiHK%+G zkf_z9l4tI#IIm$+APkEd>EkYSc_$Kfo8y&8g^sBpl=_e!cfXl239}2R?n%h$lIw{t zTEb)(8e+mleyrg~uEFO<6in4E67%-(Y>@DEP|A3+SKoYv{kI~q8TPTxuz6`P=GPE! z)`ji1qlCd&6`O(T8RPdc}b05+YYxSkw0qdJ~ITK#?rDlhNas^8Ojsv3(C zAEkRN_e_CZpa%%mc<^PYJF371zLpHSa#z+kUUCKP@UXz7rwz-|YsAfJ zWnn!3cCC^gQq*>=7ndd?S9HeN-`Z!Ljc~5oeWz^Fi*!yWhYV(I3Q1of;WPK2nxE^#5~JaL zXk_R1oon}ud3IIaRu>U+ZLJV(?dk??b=r#k+Lhn-4M%QnNP`hCA`P91Z7O#wB2lz` zr1lLmkvXS%2(Gx*cFLpzTbZLicEn;+9rn*6x?Q^b(ub(eYNo&xr3mj5v@n6uJGb>W z?FI;TyXR3Sa5IuK=gKG^<8C-@4eHs4_qM|Dp)219IGOAFM?*N^9s3C69`X<|lCSKW z(%6v^)9aFcL~*=&(HI)x6^GsC*W+r+PUWuaMIXv5W$XqJpf)XH?yWq>Nx3n{RB5-A z+5Vs?uqXhm5QI{|4k*@J4m=v{1wv?Ug>O0(@kEC*ufvxh03a?)lB2CXq01?LtT7O_+Ot>ndl3A=ZLCij+eR zD{pnclcG9@pj#)Ppn*=B+3qdN{dCr-hV)jUV1Chif-O^chk>^ykYNy|Fh6AybtnWa zz6J`$x=q>r?u%#vAM$Oki3d{S6}M7bD(pZ5CO@n{*Np%B%#t}7?k%;LWS$8 zVp2`btkOG~>W0e8I0+9(`cnslGR!;b%U%Norg3?%e$C~~{%c_Kw;06qm9Y6c=uF0) z`Eo8dV74w_?3N=XiOJnmPepf{yTNzM2Ha`24#s@LHPdoN1vR*g(;{n~xnvEQv^1j( z7(%YH0ydS)Uat%S?+^-JrONl&Hu*N&LQ_`qE7WT*EaZNeuHkS4K5A`~oi>piZO@{U zE=H6IGS-9fT@t}L1o&BbaM0HsLbu$=E;IC6Ei7ydBg__J{)q3awIN7?XKwIB&XiFz zt4$JR>nnF|4JBGx$r`})17|i4@v3L!E~bIp6q8jZOK#mlZMPTof3Wdm`I}a8 za?)#FWey$Kpo}j4{%5GGkMQ48OquA$#gaza{`XDPm+*PvoHhBry=Eq@67d}#bKrx zc9CI1PZuP1G?=@?4D2NOratR0d&g{WG%l zPrgD~Rw4}5mLidnxt}>1(3qiUS!24A>XGPPNFCM>RyzIKCVRZ*I&6?(?sl@db!BDU3eAc(I6lag-nvFVhEOFiUop@R3AO6h&?iJj8> zZgbAp;&7^*62hT5-ZVsdk0yp>>C?T>xlIn_gC9SH=gkJ5B`k)J^wsmGp1PFaB`{@7 zP8+#NwO>l7)D|mOsZ}~L7J9&%MJaRHz*M*%-MST}4f$+>J)9`l9kAjIo{%M)(E9gKBz!xKukttt1eeG}4=8T~78UNT0bA!QA04H6{Wp~pR zDAz7*l%L54HH9LOBr67TqpVwFM4GJ$!@_>(O^mDXAM(72W0N;EQkf@Yd;x;RhB@;$ zvk&`Z9HwQu*4}i;&AQDRmUsIpq@1XOLDxTCh-fQE&CThLY3{rrEiFbs_UHbzj*~oC z7lP7X1&O-W{QeqS`6{qT(Sm+CU$M_q1ijN+N^wgU7(&2q{ZdOyEIlu-yaAbyDK0j6 zwkSI6`IdtFFd&}i@VfQglsJuRLz3DRkT9I`;0T5Vv^;#E2JpsUvNDWrG}Ks+go+-! zhlf}8NI%M537c~2B|6>5S(%dw_n(YMTB!RBXir>^%UrGc-y&j1=K7)o?v8Ui4LC~S z+|>kcoQie|lv9h1BHcJ(8$rdJVXh{{@Q%RnqY9e_!y^`Whze1$_l(n7ppqQHf;TqgWZZ|#{D;D@eJht& z3ge3v_8P|HtAAc;si&s%9z#_916F`BUIp4v$SQEkzn^MwSj+Pg?D?yhjEshF>X&pq z|NjHAglsQ}-TvP{Jw^3)pI?|p&p*2cG5?m9Y4iX7eHL zyd|y*`lsJ_@SsUBIFUSQ!DxdXescYQ29yeS{V*xA&>oG#%SGQRhcQ zS27h^Sp>^5Wd<=lNKShLHh9V@P&5+l{!*0m!aW$3o1}HM`R!H2=^1zNOckG%;gTaK zctvofSY~a(I|K3o`y%VbonoxIcy=#`HhK;)@QjzR_*ckVSZFRtq2(Io4Is0Z)C6=5 zx0JUAdD#<;)^^9(+H-14NVc&3VP$1C+dYWg!+BgXp^Gp1yku*(mCl5g=2kX?$vBeD zxg0#2i(YenVLCs;L5&Gq%5H6)e&?xtK9|oe`FtavFXgi%pMS{byY(`ckz0t3i`qj~|KlJMzzI<#_g(O?BmG`yL2~BidLV}{SV<0=i zI%!?e&WexvgBga^PWU+W0jnzo*_+&r^;v88D4RCI*^8cpKL=CXMA7LQruUPNK+;3G zsW$U>u&V?vEFKK-5ZH8nb~c{O=$q(6qq@D@;^N{0r$C-#3o|xH9rnBOW($xroVI!` zs6&4$Ta=sK>Df{y#XK53ykI#*E_HvZFY7q@0r3XD8@^zP3!4hfxu??Dc8M(uVRWmC zY%U13>bom&w|o6Zy+UkrNMnmtc8Z}Z6oz^f`bO@6I|*OujPntWl2ccbQ)sa{QVCHy zPLzecK2HaH zUuA^NRE^aXDG;R>scK1Ng{UhcD{A9xGj6~}QyiG0mp}cC4HjT|W`erJW%nzd6$4EG zBBNq}sVfA!OB@9zTf9B9CCdB*#S7cO6iUXsY*{jg4RKlKA~WgfPduSV+%Trm+dr|} zg7+a%{+W7U7gNZCT_IwxARuj}2(cSHBv}cYb;INeOB4P4(>e}fY1rk>s<~%0aBcZr zXrD&@%#d*9%_?7Mwgu{gEP$zmL;_FJzL#*G#2W!sqycmXj!_x1 zK$zVK6yI7@4%bXnon@3|25zu@)8yd&`)`)KM^=6F)zIP@uVC+X5tpnhl?8d73xY;E zHJJKWwTRv5IE@4BN}P*vJp~rF~lbI z=6vA0JSdwEH4QFKh`Jp1#)GL9WN8$1<}TD_FJ+Arp!;?W6|D@A{LaI{0r+mF!?ovk zVR@eC%p+~7-*U#{WxlRMMIGR9qzfxwt1i#=FO6{_RH%s_G8Ptl4olE>BNayrWHuO& zxserXiN_~V(#3I<#$tsO3JO| zlnW$8PNKw&WXaU1qq9Wa@gkUHGOL?e+s51crdvFZmY=0?Ao;y0;wo0rKf?+G7NCG^ zO?_`3J-68psmzz4iCE`TQwV*f+M#BnA_?24%HgD|7Uic~IPFn&ry|YLmY#wLi2sNB z0o}%sq>2CxTTPRyBqGl|=?phLz_nEDZoQ(v`YI&fhyCZw*caN@Qo-vJu7hab&jnP9 z%3ej%wKV!m$SzaqP)mbxkjGeS`4tj6zLRu>gAZ`z(M@x@gb7b_cfoW|tDe@$=<@Rw z4gCKw_cO1U_25*_qYYNdZI#GjG{dIh)HK(SqqF?*ChrB5YMlC!3p}&d$rvTCjH|6A z%Z2xRvS)Y1ir2fF!%s>k@h$f$W=@GqDEB_CeqKo^jDPW*}FoRmc*+zHJi?5GfMtQU0xZ{>@q6dRHo{h5Uc%&eB$e)tXVP48h zeREE(5ZQ`{icy3MuNCJsvduODLM-<8e+64uO4vmDK@MPO&2ZNVe&G?Ok{v6#-9_o9l`@xT z8iigw?P0%#c%IdiCD?r=yF4zfzAM6PWHW+1I8k`Ilx<52GK{b1EN!X%rjBy_A2Rf< zbfiXipj;&r*JIIkH*Z}~ab#%atGc0KRXRkDA;(pIP&Yw%uO2d-b?&ifzE?X=)3Y!4 zmWF|+=wDA0@jN}=nLg_tCpT5bZx_aE+|8u%)g$$_HX3{zXi<@oHt;NUa=(VV)^A8){KMqj-r zA+;2}aPhY!l_9-erINh}kZnP3Su2oh%`0{xRPR8@A&D#{-F@(_|1c*zs|qy=!;`$P z|2NBOR91obG*QX#HSl^PYQBtaro&md#_nlt zp?kvJ%)ET@sG0r4fdt-Af^))bu@MK+jcv<5_P;&gCb$Qs2cco5O-_gm+G*I~Noor8 z4k7SEc0-o~nkLxiaF`;X8|Kv~mB1lv*R=vJSe8jxF+ldd#_T<9^b@w5AUV}nRX9jE z1s;T1G+g=;uPNznGQJzp|6GaRanoK*oagsJ^nX=d-x;6zP6Lx+!+_dhHB zoV0dS;^;RVNMY>e4Lo3hX~ks7Z5@~x&UB21LZeLaae-$*GHHm;}K;bYO&hX&0;3g)cLOEzk|S>p?Hze{U}j)|-G%$@KYH z{%Gz$C)053&pOFb3=44auUyVQ3nct?=(Pmzm!yfdYTaj+>HEvJgo(_^<$K%=U JwcEEp|6jHB(Lw+K literal 0 HcmV?d00001 diff --git a/priv/static/adminfe/static/js/app.19b7049e.js b/priv/static/adminfe/static/js/app.19b7049e.js new file mode 100644 index 0000000000000000000000000000000000000000..d33589df49a42c384c9ebc07f898dffacf47ccdb GIT binary patch literal 180590 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