Backend settings sync (#226)
authorfloatingghost <hannah@coffee-and-dreams.uk>
Thu, 6 Oct 2022 16:22:15 +0000 (16:22 +0000)
committerfloatingghost <hannah@coffee-and-dreams.uk>
Thu, 6 Oct 2022 16:22:15 +0000 (16:22 +0000)
Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/226

12 files changed:
CHANGELOG.md
config/config.exs
lib/pleroma/akkoma/frontend_setting_profile.ex [new file with mode: 0644]
lib/pleroma/user.ex
lib/pleroma/web/akkoma_api/controllers/frontend_settings_controller.ex [new file with mode: 0644]
lib/pleroma/web/api_spec/operations/frontend_settings_operation.ex [new file with mode: 0644]
lib/pleroma/web/router.ex
mix.exs
priv/repo/migrations/20220911195347_add_user_frontend_profiles.exs [new file with mode: 0644]
test/pleroma/akkoma/frontend_setting_profile_test.exs [new file with mode: 0644]
test/pleroma/web/akkoma_api/frontend_settings_controller_test.exs [new file with mode: 0644]
test/support/factory.ex

index 104164dec089b53bfab8c68b719d20b0fc2ef63c..ece6af0d2883cec5c0fd01f1948980541b513958 100644 (file)
@@ -4,12 +4,19 @@ All notable changes to this project will be documented in this file.
 
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
-## Unreleased
+## 2022.10
+
+### Added
+- Ability to sync frontend profiles between clients, with a name attached
+- Status card generation will now use the media summary if it is available
 
 ### Changed
+- Emoji updated to latest 15.0 draft
 - **Breaking**: `/api/v1/pleroma/backups` endpoints now requires `read:backups` scope instead of `read:accounts`
 
 ### Fixed
+- OAuthPlug no longer joins with the database every call and uses the user cache
+- Undo activities no longer try to look up by ID, and render correctly
 - prevent false-errors from meilisearch
 
 ## 2022.09
index 7fbfb9ad29ca94d7648c9006ec47baf5e3b49fdc..d7005770eda673edcce61d6bf48326e8285a7503 100644 (file)
@@ -261,7 +261,8 @@ config :pleroma, :instance,
   password_reset_token_validity: 60 * 60 * 24,
   profile_directory: true,
   privileged_staff: false,
-  local_bubble: []
+  local_bubble: [],
+  max_frontend_settings_json_chars: 100_000
 
 config :pleroma, :welcome,
   direct_message: [
diff --git a/lib/pleroma/akkoma/frontend_setting_profile.ex b/lib/pleroma/akkoma/frontend_setting_profile.ex
new file mode 100644 (file)
index 0000000..18208a7
--- /dev/null
@@ -0,0 +1,100 @@
+defmodule Pleroma.Akkoma.FrontendSettingsProfile do
+  use Ecto.Schema
+
+  import Ecto.Changeset
+  import Ecto.Query
+  alias Pleroma.Repo
+  alias Pleroma.Config
+  alias Pleroma.User
+
+  @primary_key false
+  schema "user_frontend_setting_profiles" do
+    belongs_to(:user, Pleroma.User, primary_key: true, type: FlakeId.Ecto.CompatType)
+    field(:frontend_name, :string, primary_key: true)
+    field(:profile_name, :string, primary_key: true)
+    field(:settings, :map)
+    field(:version, :integer)
+    timestamps()
+  end
+
+  def changeset(%__MODULE__{} = struct, attrs) do
+    struct
+    |> cast(attrs, [:user_id, :frontend_name, :profile_name, :settings, :version])
+    |> validate_required([:user_id, :frontend_name, :profile_name, :settings, :version])
+    |> validate_length(:frontend_name, min: 1, max: 255)
+    |> validate_length(:profile_name, min: 1, max: 255)
+    |> validate_version(struct)
+    |> validate_number(:version, greater_than: 0)
+    |> validate_settings_length(Config.get([:instance, :max_frontend_settings_json_chars]))
+  end
+
+  def create_or_update(%User{} = user, frontend_name, profile_name, settings, version) do
+    struct =
+      case get_by_user_and_frontend_name_and_profile_name(user, frontend_name, profile_name) do
+        nil ->
+          %__MODULE__{}
+
+        %__MODULE__{} = profile ->
+          profile
+      end
+
+    struct
+    |> changeset(%{
+      user_id: user.id,
+      frontend_name: frontend_name,
+      profile_name: profile_name,
+      settings: settings,
+      version: version
+    })
+    |> Repo.insert_or_update()
+  end
+
+  def get_all_by_user_and_frontend_name(%User{id: user_id}, frontend_name) do
+    Repo.all(
+      from(p in __MODULE__, where: p.user_id == ^user_id and p.frontend_name == ^frontend_name)
+    )
+  end
+
+  def get_by_user_and_frontend_name_and_profile_name(
+        %User{id: user_id},
+        frontend_name,
+        profile_name
+      ) do
+    Repo.one(
+      from(p in __MODULE__,
+        where:
+          p.user_id == ^user_id and p.frontend_name == ^frontend_name and
+            p.profile_name == ^profile_name
+      )
+    )
+  end
+
+  def delete_profile(profile) do
+    Repo.delete(profile)
+  end
+
+  defp validate_settings_length(
+         %Ecto.Changeset{changes: %{settings: settings}} = changeset,
+         max_length
+       ) do
+    settings_json = Jason.encode!(settings)
+
+    if String.length(settings_json) > max_length do
+      add_error(changeset, :settings, "is too long")
+    else
+      changeset
+    end
+  end
+
+  defp validate_version(changeset, %{version: nil}), do: changeset
+
+  defp validate_version(%Ecto.Changeset{changes: %{version: version}} = changeset, %{
+         version: prev_version
+       }) do
+    if version != prev_version + 1 do
+      add_error(changeset, :version, "must be incremented by 1")
+    else
+      changeset
+    end
+  end
+end
index a36c1c330b050d6a063b81f4fdbdfe74d8d8d96d..700cab2b591ea00af408092ba7e45fa060c8ac39 100644 (file)
@@ -165,6 +165,8 @@ defmodule Pleroma.User do
     has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id)
     has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id)
 
+    has_many(:frontend_profiles, Pleroma.Akkoma.FrontendSettingsProfile)
+
     for {relationship_type,
          [
            {outgoing_relation, outgoing_relation_target},
diff --git a/lib/pleroma/web/akkoma_api/controllers/frontend_settings_controller.ex b/lib/pleroma/web/akkoma_api/controllers/frontend_settings_controller.ex
new file mode 100644 (file)
index 0000000..c13ff90
--- /dev/null
@@ -0,0 +1,96 @@
+defmodule Pleroma.Web.AkkomaAPI.FrontendSettingsController do
+  use Pleroma.Web, :controller
+
+  alias Pleroma.Web.Plugs.OAuthScopesPlug
+  alias Pleroma.Akkoma.FrontendSettingsProfile
+
+  @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
+  plug(
+    OAuthScopesPlug,
+    %{@unauthenticated_access | scopes: ["read:accounts"]}
+    when action in [
+           :list_profiles,
+           :get_profile
+         ]
+  )
+
+  plug(
+    OAuthScopesPlug,
+    %{@unauthenticated_access | scopes: ["write:accounts"]}
+    when action in [
+           :update_profile,
+           :delete_profile
+         ]
+  )
+
+  plug(Pleroma.Web.ApiSpec.CastAndValidate)
+  defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.FrontendSettingsOperation
+
+  action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
+
+  @doc "GET /api/v1/akkoma/frontend_settings/:frontend_name/:profile_name"
+  def get_profile(conn, %{frontend_name: frontend_name, profile_name: profile_name}) do
+    with %FrontendSettingsProfile{} = profile <-
+           FrontendSettingsProfile.get_by_user_and_frontend_name_and_profile_name(
+             conn.assigns.user,
+             frontend_name,
+             profile_name
+           ) do
+      conn
+      |> json(%{
+        settings: profile.settings,
+        version: profile.version
+      })
+    else
+      nil -> {:error, :not_found}
+    end
+  end
+
+  @doc "GET /api/v1/akkoma/frontend_settings/:frontend_name"
+  def list_profiles(conn, %{frontend_name: frontend_name}) do
+    with profiles <-
+           FrontendSettingsProfile.get_all_by_user_and_frontend_name(
+             conn.assigns.user,
+             frontend_name
+           ),
+         data <-
+           Enum.map(profiles, fn profile ->
+             %{name: profile.profile_name, version: profile.version}
+           end) do
+      json(conn, data)
+    end
+  end
+
+  @doc "DELETE /api/v1/akkoma/frontend_settings/:frontend_name/:profile_name"
+  def delete_profile(conn, %{frontend_name: frontend_name, profile_name: profile_name}) do
+    with %FrontendSettingsProfile{} = profile <-
+           FrontendSettingsProfile.get_by_user_and_frontend_name_and_profile_name(
+             conn.assigns.user,
+             frontend_name,
+             profile_name
+           ),
+         {:ok, _} <- FrontendSettingsProfile.delete_profile(profile) do
+      json(conn, %{deleted: "ok"})
+    else
+      nil -> {:error, :not_found}
+    end
+  end
+
+  @doc "PUT /api/v1/akkoma/frontend_settings/:frontend_name/:profile_name"
+  def update_profile(%{body_params: %{settings: settings, version: version}} = conn, %{
+        frontend_name: frontend_name,
+        profile_name: profile_name
+      }) do
+    with {:ok, profile} <-
+           FrontendSettingsProfile.create_or_update(
+             conn.assigns.user,
+             frontend_name,
+             profile_name,
+             settings,
+             version
+           ) do
+      conn
+      |> json(profile.settings)
+    end
+  end
+end
diff --git a/lib/pleroma/web/api_spec/operations/frontend_settings_operation.ex b/lib/pleroma/web/api_spec/operations/frontend_settings_operation.ex
new file mode 100644 (file)
index 0000000..40e81ad
--- /dev/null
@@ -0,0 +1,133 @@
+defmodule Pleroma.Web.ApiSpec.FrontendSettingsOperation do
+  alias OpenApiSpex.Operation
+  alias OpenApiSpex.Schema
+  import Pleroma.Web.ApiSpec.Helpers
+
+  @spec open_api_operation(atom) :: Operation.t()
+  def open_api_operation(action) do
+    operation = String.to_existing_atom("#{action}_operation")
+    apply(__MODULE__, operation, [])
+  end
+
+  @spec list_profiles_operation() :: Operation.t()
+  def list_profiles_operation() do
+    %Operation{
+      tags: ["Retrieve frontend setting profiles"],
+      summary: "Frontend Settings Profiles",
+      description: "List frontend setting profiles",
+      operationId: "AkkomaAPI.FrontendSettingsController.list_profiles",
+      parameters: [frontend_name_param()],
+      security: [%{"oAuth" => ["read:accounts"]}],
+      responses: %{
+        200 =>
+          Operation.response("Profiles", "application/json", %Schema{
+            type: :array,
+            items: %Schema{
+              type: :object,
+              properties: %{
+                name: %Schema{type: :string},
+                version: %Schema{type: :integer}
+              }
+            }
+          })
+      }
+    }
+  end
+
+  @spec get_profile_operation() :: Operation.t()
+  def get_profile_operation() do
+    %Operation{
+      tags: ["Retrieve frontend setting profile"],
+      summary: "Frontend Settings Profile",
+      description: "Get frontend setting profile",
+      operationId: "AkkomaAPI.FrontendSettingsController.get_profile",
+      security: [%{"oAuth" => ["read:accounts"]}],
+      parameters: [frontend_name_param(), profile_name_param()],
+      responses: %{
+        200 =>
+          Operation.response("Profile", "application/json", %Schema{
+            type: :object,
+            properties: %{
+              "version" => %Schema{type: :integer},
+              "settings" => %Schema{type: :object, additionalProperties: true}
+            }
+          }),
+        404 => Operation.response("Not Found", "application/json", %Schema{type: :object})
+      }
+    }
+  end
+
+  @spec delete_profile_operation() :: Operation.t()
+  def delete_profile_operation() do
+    %Operation{
+      tags: ["Delete frontend setting profile"],
+      summary: "Delete frontend Settings Profile",
+      description: "Delete  frontend setting profile",
+      operationId: "AkkomaAPI.FrontendSettingsController.delete_profile",
+      security: [%{"oAuth" => ["write:accounts"]}],
+      parameters: [frontend_name_param(), profile_name_param()],
+      responses: %{
+        200 => Operation.response("Empty", "application/json", %Schema{type: :object}),
+        404 => Operation.response("Not Found", "application/json", %Schema{type: :object})
+      }
+    }
+  end
+
+  @spec update_profile_operation() :: Operation.t()
+  def update_profile_operation() do
+    %Operation{
+      tags: ["Update frontend setting profile"],
+      summary: "Frontend Settings Profile",
+      description: "Update frontend setting profile",
+      operationId: "AkkomaAPI.FrontendSettingsController.update_profile_operation",
+      security: [%{"oAuth" => ["write:accounts"]}],
+      parameters: [frontend_name_param(), profile_name_param()],
+      requestBody: profile_body_param(),
+      responses: %{
+        200 => Operation.response("Settings", "application/json", %Schema{type: :object}),
+        422 => Operation.response("Invalid", "application/json", %Schema{type: :object})
+      }
+    }
+  end
+
+  def frontend_name_param do
+    Operation.parameter(:frontend_name, :path, :string, "Frontend name",
+      example: "pleroma-fe",
+      required: true
+    )
+  end
+
+  def profile_name_param do
+    Operation.parameter(:profile_name, :path, :string, "Profile name",
+      example: "mobile",
+      required: true
+    )
+  end
+
+  def profile_body_param do
+    request_body(
+      "Settings",
+      %Schema{
+        title: "Frontend Setting Profile",
+        type: :object,
+        required: [:version, :settings],
+        properties: %{
+          version: %Schema{
+            type: :integer,
+            description: "Version of the profile, must increment by 1 each time",
+            example: 1
+          },
+          settings: %Schema{
+            type: :object,
+            description: "Settings of the profile",
+            example: %{
+              theme: "dark",
+              locale: "en"
+            }
+          }
+        }
+      },
+      required: true
+    )
+  end
+end
index f722d94f72ca08d89d9fe81a42a3810d4a20f19e..838599c4d65158e5b6e0f9aa9e5a87dd5e4c152a 100644 (file)
@@ -466,6 +466,26 @@ defmodule Pleroma.Web.Router do
   scope "/api/v1/akkoma", Pleroma.Web.AkkomaAPI do
     pipe_through(:authenticated_api)
     get("/translation/languages", TranslationController, :languages)
+
+    get("/frontend_settings/:frontend_name", FrontendSettingsController, :list_profiles)
+
+    get(
+      "/frontend_settings/:frontend_name/:profile_name",
+      FrontendSettingsController,
+      :get_profile
+    )
+
+    put(
+      "/frontend_settings/:frontend_name/:profile_name",
+      FrontendSettingsController,
+      :update_profile
+    )
+
+    delete(
+      "/frontend_settings/:frontend_name/:profile_name",
+      FrontendSettingsController,
+      :delete_profile
+    )
   end
 
   scope "/api/v1", Pleroma.Web.MastodonAPI do
diff --git a/mix.exs b/mix.exs
index 19e6fd045684ab36d7140d5334ba797570472ca1..c7e66b158c01f0cf080807e6ef4ae7739bbbd02e 100644 (file)
--- a/mix.exs
+++ b/mix.exs
@@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do
   def project do
     [
       app: :pleroma,
-      version: version("3.2.0"),
+      version: version("3.3.0"),
       elixir: "~> 1.12",
       elixirc_paths: elixirc_paths(Mix.env()),
       compilers: [:phoenix, :gettext] ++ Mix.compilers(),
diff --git a/priv/repo/migrations/20220911195347_add_user_frontend_profiles.exs b/priv/repo/migrations/20220911195347_add_user_frontend_profiles.exs
new file mode 100644 (file)
index 0000000..c1c4497
--- /dev/null
@@ -0,0 +1,29 @@
+defmodule Pleroma.Repo.Migrations.AddUserFrontendProfiles do
+  use Ecto.Migration
+
+  def up do
+    create_if_not_exists table("user_frontend_setting_profiles", primary_key: false) do
+      add(:user_id, references(:users, type: :uuid, on_delete: :delete_all), primary_key: true)
+      add(:frontend_name, :string, primary_key: true)
+      add(:profile_name, :string, primary_key: true)
+      add(:version, :integer)
+      add(:settings, :map)
+      timestamps()
+    end
+
+    create_if_not_exists(index(:user_frontend_setting_profiles, [:user_id, :frontend_name]))
+
+    create_if_not_exists(
+      unique_index(:user_frontend_setting_profiles, [:user_id, :frontend_name, :profile_name])
+    )
+  end
+
+  def down do
+    drop_if_exists(table("user_frontend_setting_profiles"))
+    drop_if_exists(index(:user_frontend_setting_profiles, [:user_id, :frontend_name]))
+
+    drop_if_exists(
+      unique_index(:user_frontend_setting_profiles, [:user_id, :frontend_name, :profile_name])
+    )
+  end
+end
diff --git a/test/pleroma/akkoma/frontend_setting_profile_test.exs b/test/pleroma/akkoma/frontend_setting_profile_test.exs
new file mode 100644 (file)
index 0000000..4bb1139
--- /dev/null
@@ -0,0 +1,196 @@
+defmodule Pleroma.Akkoma.FrontendSettingsProfileTest do
+  use Pleroma.DataCase, async: true
+  use Oban.Testing, repo: Pleroma.Repo
+  alias Pleroma.Akkoma.FrontendSettingsProfile
+
+  import Pleroma.Factory
+
+  describe "changeset/2" do
+    test "valid" do
+      user = insert(:user)
+      frontend_name = "test"
+      profile_name = "test"
+      settings = %{"test" => "test"}
+      struct = %FrontendSettingsProfile{}
+
+      attrs = %{
+        user_id: user.id,
+        frontend_name: frontend_name,
+        profile_name: profile_name,
+        settings: settings,
+        version: 1
+      }
+
+      assert %{valid?: true} = FrontendSettingsProfile.changeset(struct, attrs)
+    end
+
+    test "when settings is too long" do
+      clear_config([:instance, :max_frontend_settings_json_chars], 10)
+      user = insert(:user)
+      frontend_name = "test"
+      profile_name = "test"
+      settings = %{"verylong" => "verylongoops"}
+      struct = %FrontendSettingsProfile{}
+
+      attrs = %{
+        user_id: user.id,
+        frontend_name: frontend_name,
+        profile_name: profile_name,
+        settings: settings,
+        version: 1
+      }
+
+      assert %{valid?: false, errors: [settings: {"is too long", _}]} =
+               FrontendSettingsProfile.changeset(struct, attrs)
+    end
+
+    test "when frontend name is too short" do
+      user = insert(:user)
+      frontend_name = ""
+      profile_name = "test"
+      settings = %{"test" => "test"}
+      struct = %FrontendSettingsProfile{}
+
+      attrs = %{
+        user_id: user.id,
+        frontend_name: frontend_name,
+        profile_name: profile_name,
+        settings: settings,
+        version: 1
+      }
+
+      assert %{valid?: false, errors: [frontend_name: {"can't be blank", _}]} =
+               FrontendSettingsProfile.changeset(struct, attrs)
+    end
+
+    test "when profile name is too short" do
+      user = insert(:user)
+      frontend_name = "test"
+      profile_name = ""
+      settings = %{"test" => "test"}
+      struct = %FrontendSettingsProfile{}
+
+      attrs = %{
+        user_id: user.id,
+        frontend_name: frontend_name,
+        profile_name: profile_name,
+        settings: settings,
+        version: 1
+      }
+
+      assert %{valid?: false, errors: [profile_name: {"can't be blank", _}]} =
+               FrontendSettingsProfile.changeset(struct, attrs)
+    end
+
+    test "when version is negative" do
+      user = insert(:user)
+      frontend_name = "test"
+      profile_name = "test"
+      settings = %{"test" => "test"}
+      struct = %FrontendSettingsProfile{}
+
+      attrs = %{
+        user_id: user.id,
+        frontend_name: frontend_name,
+        profile_name: profile_name,
+        settings: settings,
+        version: -1
+      }
+
+      assert %{valid?: false, errors: [version: {"must be greater than %{number}", _}]} =
+               FrontendSettingsProfile.changeset(struct, attrs)
+    end
+  end
+
+  describe "create_or_update/2" do
+    test "it should create a new record" do
+      user = insert(:user)
+      frontend_name = "test"
+      profile_name = "test"
+      settings = %{"test" => "test"}
+
+      assert {:ok, %FrontendSettingsProfile{}} =
+               FrontendSettingsProfile.create_or_update(
+                 user,
+                 frontend_name,
+                 profile_name,
+                 settings,
+                 1
+               )
+    end
+
+    test "it should update a record" do
+      user = insert(:user)
+      frontend_name = "test"
+      profile_name = "test"
+
+      insert(:frontend_setting_profile,
+        user: user,
+        frontend_name: frontend_name,
+        profile_name: profile_name,
+        settings: %{"test" => "test"},
+        version: 1
+      )
+
+      settings = %{"test" => "test2"}
+
+      assert {:ok, %FrontendSettingsProfile{settings: ^settings}} =
+               FrontendSettingsProfile.create_or_update(
+                 user,
+                 frontend_name,
+                 profile_name,
+                 settings,
+                 2
+               )
+    end
+  end
+
+  describe "get_all_by_user_and_frontend_name/2" do
+    test "it should return all records" do
+      user = insert(:user)
+      frontend_name = "test"
+
+      insert(:frontend_setting_profile,
+        user: user,
+        frontend_name: frontend_name,
+        profile_name: "profileA",
+        settings: %{"test" => "test"},
+        version: 1
+      )
+
+      insert(:frontend_setting_profile,
+        user: user,
+        frontend_name: frontend_name,
+        profile_name: "profileB",
+        settings: %{"test" => "test"},
+        version: 1
+      )
+
+      assert [%FrontendSettingsProfile{profile_name: "profileA"}, %{profile_name: "profileB"}] =
+               FrontendSettingsProfile.get_all_by_user_and_frontend_name(user, frontend_name)
+    end
+  end
+
+  describe "get_by_user_and_frontend_name_and_profile_name/3" do
+    test "it should return a record" do
+      user = insert(:user)
+      frontend_name = "test"
+      profile_name = "profileA"
+
+      insert(:frontend_setting_profile,
+        user: user,
+        frontend_name: frontend_name,
+        profile_name: profile_name,
+        settings: %{"test" => "test"},
+        version: 1
+      )
+
+      assert %FrontendSettingsProfile{profile_name: "profileA"} =
+               FrontendSettingsProfile.get_by_user_and_frontend_name_and_profile_name(
+                 user,
+                 frontend_name,
+                 profile_name
+               )
+    end
+  end
+end
diff --git a/test/pleroma/web/akkoma_api/frontend_settings_controller_test.exs b/test/pleroma/web/akkoma_api/frontend_settings_controller_test.exs
new file mode 100644 (file)
index 0000000..4909ef3
--- /dev/null
@@ -0,0 +1,122 @@
+defmodule Pleroma.Web.AkkomaAPI.FrontendSettingsControllerTest do
+  use Pleroma.Web.ConnCase, async: true
+
+  import Pleroma.Factory
+  alias Pleroma.Akkoma.FrontendSettingsProfile
+
+  describe "GET /api/v1/akkoma/frontend_settings/:frontend_name" do
+    test "it returns a list of profiles" do
+      %{conn: conn, user: user} = oauth_access(["read"])
+
+      insert(:frontend_setting_profile, user: user, frontend_name: "test", profile_name: "test1")
+      insert(:frontend_setting_profile, user: user, frontend_name: "test", profile_name: "test2")
+
+      response =
+        conn
+        |> get("/api/v1/akkoma/frontend_settings/test")
+        |> json_response_and_validate_schema(200)
+
+      assert response == [
+               %{"name" => "test1", "version" => 1},
+               %{"name" => "test2", "version" => 1}
+             ]
+    end
+  end
+
+  describe "GET /api/v1/akkoma/frontend_settings/:frontend_name/:profile_name" do
+    test "it returns 404 if not found" do
+      %{conn: conn} = oauth_access(["read"])
+
+      conn
+      |> get("/api/v1/akkoma/frontend_settings/unknown_frontend/unknown_profile")
+      |> json_response_and_validate_schema(404)
+    end
+
+    test "it returns 200 if found" do
+      %{conn: conn, user: user} = oauth_access(["read"])
+
+      insert(:frontend_setting_profile,
+        user: user,
+        frontend_name: "test",
+        profile_name: "test1",
+        settings: %{"test" => "test"}
+      )
+
+      response =
+        conn
+        |> get("/api/v1/akkoma/frontend_settings/test/test1")
+        |> json_response_and_validate_schema(200)
+
+      assert response == %{"settings" => %{"test" => "test"}, "version" => 1}
+    end
+  end
+
+  describe "PUT /api/v1/akkoma/frontend_settings/:frontend_name/:profile_name" do
+    test "puts a config" do
+      %{conn: conn, user: user} = oauth_access(["write"])
+      settings = %{"test" => "test2"}
+
+      response =
+        conn
+        |> put_req_header("content-type", "application/json")
+        |> put("/api/v1/akkoma/frontend_settings/test/test1", %{
+          "settings" => settings,
+          "version" => 1
+        })
+        |> json_response_and_validate_schema(200)
+
+      assert response == settings
+
+      assert %FrontendSettingsProfile{settings: ^settings} =
+               FrontendSettingsProfile.get_by_user_and_frontend_name_and_profile_name(
+                 user,
+                 "test",
+                 "test1"
+               )
+    end
+
+    test "refuses to overwrite a newer config" do
+      %{conn: conn, user: user} = oauth_access(["write"])
+
+      insert(:frontend_setting_profile,
+        user: user,
+        frontend_name: "test",
+        profile_name: "test1",
+        settings: %{"test" => "test"},
+        version: 2
+      )
+
+      conn
+      |> put_req_header("content-type", "application/json")
+      |> put("/api/v1/akkoma/frontend_settings/test/test1", %{
+        "settings" => %{"test" => "test2"},
+        "version" => 1
+      })
+      |> json_response_and_validate_schema(422)
+    end
+  end
+
+  describe "DELETE /api/v1/akkoma/frontend_settings/:frontend_name/:profile_name" do
+    test "deletes a config" do
+      %{conn: conn, user: user} = oauth_access(["write"])
+
+      insert(:frontend_setting_profile,
+        user: user,
+        frontend_name: "test",
+        profile_name: "test1",
+        settings: %{"test" => "test"},
+        version: 2
+      )
+
+      conn
+      |> delete("/api/v1/akkoma/frontend_settings/test/test1")
+      |> json_response_and_validate_schema(200)
+
+      assert FrontendSettingsProfile.get_by_user_and_frontend_name_and_profile_name(
+               user,
+               "test",
+               "test1"
+             ) == nil
+    end
+  end
+end
index efcd8039e4913565b883e3102e286c0ad7b4c32b..54d385bc44f46278ce34d04232c9c2ee4f9cea88 100644 (file)
@@ -663,4 +663,15 @@ defmodule Pleroma.Factory do
     |> Map.merge(params)
     |> Pleroma.Announcement.add_rendered_properties()
   end
+
+  def frontend_setting_profile_factory(params \\ %{}) do
+    %Pleroma.Akkoma.FrontendSettingsProfile{
+      user: build(:user),
+      frontend_name: "akkoma-fe",
+      profile_name: "default",
+      settings: %{"test" => "test"},
+      version: 1
+    }
+    |> Map.merge(params)
+  end
 end