diff --git a/SELFHOSTING.md b/SELFHOSTING.md index 73fab0905..d9f0e9e5e 100644 --- a/SELFHOSTING.md +++ b/SELFHOSTING.md @@ -132,6 +132,7 @@ Required environment variables: | STATIC_PATH | Path to folder where uploaded files can be stored | "/tmp" | | UNSPLASH_ACCESS_KEY | Application access key registered on [Unsplash](https://unsplash.com/) (Image Catalog) | "hcejpnHRuFWL-fKXLYqhGBt1Dz0_tTjeNifgD01VkGE" | | UNSPLASH_APP_NAME | Application name registered on [Unsplash](https://unsplash.com/) (Image Catalog) | "Self" | +| STORAGE_SERVICES | Comma seperated list of storage services | "builtin yoda aws azure" | Optional environment variables: @@ -144,8 +145,9 @@ Optional environment variables: | SURFCONEXT_SITE | SURFconext site | "https://connect.test.surfconext.nl" | | SURFCONEXT_CLIENT_ID | SURFconext client ID | "self.com" | | SURFCONEXT_CLIENT_SECRET | SURFconext client secret | "12343HieOjb1234hcBpL" | -| CONTENT_S3_PREFIX | Prefix for S3 content storage | "content" | -| FELDSPAR_S3_PREFIX | Prefix for S3 feldspar storage | "feldspar" | +| STORAGE_S3_PREFIX | Prefix for S3 builtin storage objects. Without this variable "builtin" storage service will default to local filesystem | "storage" | +| CONTENT_S3_PREFIX | Prefix for S3 content objects | "content" | +| FELDSPAR_S3_PREFIX | Prefix for S3 feldspar objects | "feldspar" | | PUBLIC_S3_URL | Public accessable url of an S3 service | "https://self-public.s3.eu-central-1.amazonaws.com" | | PUBLIC_S3_BUCKET | Name of the bucket on the S3 service | "self-prod" | | DIST_HOSTS | Comma seperated list of hosts in the cluster, see: [OTP Distribution](https://elixirschool.com/en/lessons/advanced/otp_distribution) | "one, two" | \ No newline at end of file diff --git a/core/config/config.exs b/core/config/config.exs index e86ced1bf..ffd74c20a 100644 --- a/core/config/config.exs +++ b/core/config/config.exs @@ -131,7 +131,7 @@ config :core, :version, System.get_env("VERSION", "dev") config :core, :assignment, external_panels: ~w(liss ioresearch generic) -config :core, :storage, services: ~w(azure aws yoda) +config :core, :storage, services: ~w(builtin yoda) config :core, BankingClient, host: 'localhost', diff --git a/core/config/dev.exs b/core/config/dev.exs index abce09535..6b44575d4 100644 --- a/core/config/dev.exs +++ b/core/config/dev.exs @@ -51,6 +51,8 @@ config :core, "*@eyra.co" ] +config :core, Systems.Storage.BuiltIn, special: Systems.Storage.BuiltIn.LocalFS + config :core, :rate, prune_interval: 5 * 60 * 1000, quotas: [ diff --git a/core/config/runtime.exs b/core/config/runtime.exs index ee3e2ac5f..7d9a82e42 100644 --- a/core/config/runtime.exs +++ b/core/config/runtime.exs @@ -42,28 +42,17 @@ if config_env() == :prod do hackney_opts: [recv_timeout: :timer.minutes(1)] end - # AWS - - if bucket = System.get_env("AWS_S3_BUCKET") do - config :core, :s3, bucket: bucket - end - - if aws_access_key_id = System.get_env("AWS_ACCESS_KEY_ID") do - config :ex_aws, access_key_id: aws_access_key_id - - config :core, Systems.Email.Mailer, - adapter: Bamboo.SesAdapter, - domain: app_domain, - default_from_email: {app_name, app_mail_noreply} - end - - if secret_access_key = System.get_env("AWS_SECRET_ACCESS_KEY") do - config :ex_aws, secret_access_key: secret_access_key - end - - if aws_region = System.get_env("AWS_REGION") do - config :ex_aws, region: aws_region - end + # EX AWS + config :ex_aws, + access_key_id: System.get_env("AWS_ACCESS_KEY_ID"), + secret_access_key: System.get_env("AWS_SECRET_ACCESS_KEY"), + region: System.get_env("AWS_REGION") + + # AWS SES + config :core, Systems.Email.Mailer, + adapter: Bamboo.SesAdapter, + domain: app_domain, + default_from_email: {app_name, app_mail_noreply} # AZURE BLOB @@ -126,6 +115,23 @@ if config_env() == :prod do environment_name: System.get_env("RELEASE_ENV") || "prod" end + config :core, :storage, + services: + System.get_env("STORAGE_SERVICES", "builtin, yoda") + |> String.split(",", trim: true) + |> Enum.map(&String.trim/1) + |> Enum.map(&String.to_atom/1) + + if storage_s3_prefix = System.get_env("STORAGE_S3_PREFIX") do + config :core, Systems.Storage.BuiltIn, special: Systems.Storage.BuiltIn.S3 + + config :core, Systems.Storage.BuiltIn.S3, + bucket: System.get_env("AWS_S3_BUCKET"), + prefix: storage_s3_prefix + else + config :core, Systems.Storage.BuiltIn, special: Systems.Storage.BuiltIn.LocalFS + end + if content_s3_prefix = System.get_env("CONTENT_S3_PREFIX") do config :core, :content, backend: Systems.Content.S3, diff --git a/core/frameworks/fabric/live_component.ex b/core/frameworks/fabric/live_component.ex index b50d9676d..be302a7ef 100644 --- a/core/frameworks/fabric/live_component.ex +++ b/core/frameworks/fabric/live_component.ex @@ -38,7 +38,7 @@ defmodule Fabric.LiveComponent do @impl true def handle_event(_name, _payload, socket) do - Logger.error("handle_event/3 not implemented") + Logger.error("[#{__MODULE__}] handle_event/3 not implemented") {:noreply, socket} end diff --git a/core/frameworks/utililty/ecto_helper.ex b/core/frameworks/utililty/ecto_helper.ex index cf28fcdd0..a63d3721c 100644 --- a/core/frameworks/utililty/ecto_helper.ex +++ b/core/frameworks/utililty/ecto_helper.ex @@ -1,5 +1,6 @@ defmodule Frameworks.Utility.EctoHelper do import Ecto.Query, only: [from: 2] + require Logger alias Ecto.{Multi, Changeset} alias Core.Repo alias Frameworks.Signal @@ -20,11 +21,16 @@ defmodule Frameworks.Utility.EctoHelper do Repo.insert(changeset) end - def update_and_dispatch(changeset, key) do + def update_and_dispatch(%Changeset{} = changeset, key) do Multi.new() + |> update_and_dispatch(changeset, key) + |> Repo.transaction() + end + + def update_and_dispatch(%Multi{} = multi, %Changeset{} = changeset, key) do + multi |> Repo.multi_update(key, changeset) |> Signal.Public.multi_dispatch({key, :update_and_dispatch}, %{changeset: changeset}) - |> Repo.transaction() end def delete(multi, name, %table{id: id}) do diff --git a/core/priv/gettext/en/LC_MESSAGES/eyra-account.po b/core/priv/gettext/en/LC_MESSAGES/eyra-account.po index 5d8814a5c..061033525 100644 --- a/core/priv/gettext/en/LC_MESSAGES/eyra-account.po +++ b/core/priv/gettext/en/LC_MESSAGES/eyra-account.po @@ -58,30 +58,6 @@ msgstr "Change" msgid "professionaltitle.label" msgstr "Professional title" -#, elixir-autogen, elixir-format -msgid "push.registration.button" -msgstr "Activate" - -#, elixir-autogen, elixir-format -msgid "push.registration.title" -msgstr "Push notifications" - -#, elixir-autogen, elixir-format -msgid "push.registration.activated" -msgstr "Push notifications are activated on this browser." - -#, elixir-autogen, elixir-format -msgid "push.registration.denied" -msgstr "Push notifications are deactivated on this browser" - -#, elixir-autogen, elixir-format -msgid "push.registration.label" -msgstr "Activate to receive notifications from the platform in your browser. After activation, your browser might ask for permission to show notifications." - -#, elixir-autogen, elixir-format -msgid "push.registration.pending" -msgstr "Determining state.." - #, elixir-autogen, elixir-format msgid "feature.study.description" msgstr "To earn credits for research participation (OP / RPR) for bedrijfskunde (BK) or international business administration (IBA), select one or more courses below. If you did not finish a course last year, make sure to select that course as well." @@ -110,10 +86,6 @@ msgstr "Inclusion" msgid "features.content.description" msgstr "Select the inclusion criteria for your study. If nothing is selected, everyone in the pool is eligible to participate." -#, elixir-autogen, elixir-format -msgid "push.unavailable.label" -msgstr "Registering for push notifications is currently available on Chrome and Firefox browsers. Please open the website on one of these browsers to make use of push notifications." - #, elixir-autogen, elixir-format msgid "login.google.button" msgstr "Sign in with Google" @@ -157,7 +129,3 @@ msgstr "Sign in" #, elixir-autogen, elixir-format, fuzzy msgid "await.confirmation.description" msgstr "Your account has been created. We send you an activation email. Please click the link in the email to activate your account." - -#, elixir-autogen, elixir-format -msgid "account.created.info.flash" -msgstr "Account created successfully" diff --git a/core/priv/gettext/en/LC_MESSAGES/eyra-enums.po b/core/priv/gettext/en/LC_MESSAGES/eyra-enums.po index 874145fc9..6b48dc820 100644 --- a/core/priv/gettext/en/LC_MESSAGES/eyra-enums.po +++ b/core/priv/gettext/en/LC_MESSAGES/eyra-enums.po @@ -377,3 +377,7 @@ msgstr "Benchmark" #, elixir-autogen, elixir-format, fuzzy msgid "platforms.netflix" msgstr "Netflix" + +#, elixir-autogen, elixir-format, fuzzy +msgid "storage_service_ids.builtin" +msgstr "Internal" diff --git a/core/priv/gettext/en/LC_MESSAGES/eyra-storage.po b/core/priv/gettext/en/LC_MESSAGES/eyra-storage.po index fbaaddef7..17d026659 100644 --- a/core/priv/gettext/en/LC_MESSAGES/eyra-storage.po +++ b/core/priv/gettext/en/LC_MESSAGES/eyra-storage.po @@ -57,4 +57,24 @@ msgstr "Folder url" #, elixir-autogen, elixir-format msgid "yoda.user.label" -msgstr "Email" +msgstr "E-mail" + +#, elixir-autogen, elixir-format +msgid "aws.annotation" +msgstr "
Use Amazon S3 storage. More information: https://aws.amazon.com/s3
" + +#, elixir-autogen, elixir-format +msgid "azure.annotation" +msgstr "
Use Microsoft Azure blob storage. More information: https://azure.microsoft.com/en-us/products/storage/blobs
" + +#, elixir-autogen, elixir-format +msgid "builtin.annotation" +msgstr "
Use the internal data storage if you do not wish to connect to a third-party data storage service.
Once your data donation flow is published, a data tab will appear with instructions on how to download the donated data.
" + +#, elixir-autogen, elixir-format +msgid "centerdata.annotation" +msgstr "
More information: https://www.centerdata.nl
" + +#, elixir-autogen, elixir-format +msgid "yoda.annotation" +msgstr "
Use Yoda, a research data management service developed at Utrecht University.
More information: https://www.uu.nl/en/research/yoda
" diff --git a/core/priv/gettext/en/LC_MESSAGES/eyra-ui.po b/core/priv/gettext/en/LC_MESSAGES/eyra-ui.po index 3f01497b4..2436444d6 100644 --- a/core/priv/gettext/en/LC_MESSAGES/eyra-ui.po +++ b/core/priv/gettext/en/LC_MESSAGES/eyra-ui.po @@ -26,10 +26,6 @@ msgstr "My console" msgid "menu.item.profile" msgstr "My profile" -#, elixir-autogen, elixir-format -msgid "menu.item.settings" -msgstr "Settings" - #, elixir-autogen, elixir-format msgid "menu.item.signin" msgstr "Sign in" @@ -58,26 +54,10 @@ msgstr "To-do" msgid "tabbar.item.features" msgstr "Characteristics" -#, elixir-autogen, elixir-format -msgid "tabbar.item.profile" -msgstr "My profile" - #, elixir-autogen, elixir-format msgid "tabbar.item.student" msgstr "Study program" -#, elixir-autogen, elixir-format -msgid "tabbar.item.features.forward" -msgstr "Update characteristics" - -#, elixir-autogen, elixir-format -msgid "tabbar.item.profile.forward" -msgstr "Update profile" - -#, elixir-autogen, elixir-format -msgid "tabbar.item.student.forward" -msgstr "Select study program" - #, elixir-autogen, elixir-format msgid "menu.item.helpdesk" msgstr "Helpdesk" @@ -194,14 +174,6 @@ msgstr "Collapse" msgid "expand.button" msgstr "Expand" -#, elixir-autogen, elixir-format, fuzzy -msgid "tabbar.item.settings" -msgstr "Settings" - -#, elixir-autogen, elixir-format, fuzzy -msgid "tabbar.item.settings.forward" -msgstr "Settings" - #, elixir-autogen, elixir-format msgid "privacy.link" msgstr "Privacy" diff --git a/core/priv/gettext/eyra-account.pot b/core/priv/gettext/eyra-account.pot index 6cffd3530..10397f2f9 100644 --- a/core/priv/gettext/eyra-account.pot +++ b/core/priv/gettext/eyra-account.pot @@ -58,30 +58,6 @@ msgstr "" msgid "professionaltitle.label" msgstr "" -#, elixir-autogen, elixir-format -msgid "push.registration.button" -msgstr "" - -#, elixir-autogen, elixir-format -msgid "push.registration.title" -msgstr "" - -#, elixir-autogen, elixir-format -msgid "push.registration.activated" -msgstr "" - -#, elixir-autogen, elixir-format -msgid "push.registration.denied" -msgstr "" - -#, elixir-autogen, elixir-format -msgid "push.registration.label" -msgstr "" - -#, elixir-autogen, elixir-format -msgid "push.registration.pending" -msgstr "" - #, elixir-autogen, elixir-format msgid "feature.study.description" msgstr "" @@ -110,10 +86,6 @@ msgstr "" msgid "features.content.description" msgstr "" -#, elixir-autogen, elixir-format -msgid "push.unavailable.label" -msgstr "" - #, elixir-autogen, elixir-format msgid "login.google.button" msgstr "" @@ -157,7 +129,3 @@ msgstr "" #, elixir-autogen, elixir-format msgid "await.confirmation.description" msgstr "" - -#, elixir-autogen, elixir-format -msgid "account.created.info.flash" -msgstr "" diff --git a/core/priv/gettext/eyra-enums.pot b/core/priv/gettext/eyra-enums.pot index 2af2d058b..13ca3611d 100644 --- a/core/priv/gettext/eyra-enums.pot +++ b/core/priv/gettext/eyra-enums.pot @@ -377,3 +377,7 @@ msgstr "" #, elixir-autogen, elixir-format msgid "platforms.netflix" msgstr "" + +#, elixir-autogen, elixir-format +msgid "storage_service_ids.builtin" +msgstr "" diff --git a/core/priv/gettext/eyra-storage.pot b/core/priv/gettext/eyra-storage.pot index 5cc416175..36c3d2c49 100644 --- a/core/priv/gettext/eyra-storage.pot +++ b/core/priv/gettext/eyra-storage.pot @@ -58,3 +58,23 @@ msgstr "" #, elixir-autogen, elixir-format msgid "yoda.user.label" msgstr "" + +#, elixir-autogen, elixir-format +msgid "aws.annotation" +msgstr "" + +#, elixir-autogen, elixir-format +msgid "azure.annotation" +msgstr "" + +#, elixir-autogen, elixir-format +msgid "builtin.annotation" +msgstr "" + +#, elixir-autogen, elixir-format +msgid "centerdata.annotation" +msgstr "" + +#, elixir-autogen, elixir-format +msgid "yoda.annotation" +msgstr "" diff --git a/core/priv/gettext/eyra-ui.pot b/core/priv/gettext/eyra-ui.pot index 65fd00b16..ba4dbbf48 100644 --- a/core/priv/gettext/eyra-ui.pot +++ b/core/priv/gettext/eyra-ui.pot @@ -26,10 +26,6 @@ msgstr "" msgid "menu.item.profile" msgstr "" -#, elixir-autogen, elixir-format -msgid "menu.item.settings" -msgstr "" - #, elixir-autogen, elixir-format msgid "menu.item.signin" msgstr "" @@ -58,26 +54,10 @@ msgstr "" msgid "tabbar.item.features" msgstr "" -#, elixir-autogen, elixir-format -msgid "tabbar.item.profile" -msgstr "" - #, elixir-autogen, elixir-format msgid "tabbar.item.student" msgstr "" -#, elixir-autogen, elixir-format -msgid "tabbar.item.features.forward" -msgstr "" - -#, elixir-autogen, elixir-format -msgid "tabbar.item.profile.forward" -msgstr "" - -#, elixir-autogen, elixir-format -msgid "tabbar.item.student.forward" -msgstr "" - #, elixir-autogen, elixir-format msgid "menu.item.helpdesk" msgstr "" @@ -194,14 +174,6 @@ msgstr "" msgid "expand.button" msgstr "" -#, elixir-autogen, elixir-format -msgid "tabbar.item.settings" -msgstr "" - -#, elixir-autogen, elixir-format -msgid "tabbar.item.settings.forward" -msgstr "" - #, elixir-autogen, elixir-format msgid "privacy.link" msgstr "" diff --git a/core/priv/gettext/nl/LC_MESSAGES/errors.po b/core/priv/gettext/nl/LC_MESSAGES/errors.po index 40429b4be..95ca5a146 100644 --- a/core/priv/gettext/nl/LC_MESSAGES/errors.po +++ b/core/priv/gettext/nl/LC_MESSAGES/errors.po @@ -84,9 +84,3 @@ msgstr "moet groter of gelijk aan %{number} zijn" msgid "must be equal to %{number}" msgstr "moet gelijk aan %{number} zijn" - -msgid "at least one digit or punctuation character" -msgstr "gebruik minimaal 1 cijfer of interpunctie teken" - -msgid "at least one upper case character" -msgstr "gebruik minimaal 1 hoofdletter" diff --git a/core/priv/gettext/nl/LC_MESSAGES/eyra-account.po b/core/priv/gettext/nl/LC_MESSAGES/eyra-account.po index 6f5ca924c..4ac3e4b7e 100644 --- a/core/priv/gettext/nl/LC_MESSAGES/eyra-account.po +++ b/core/priv/gettext/nl/LC_MESSAGES/eyra-account.po @@ -58,30 +58,6 @@ msgstr "Aanpassen" msgid "professionaltitle.label" msgstr "Functietitel" -#, elixir-autogen, elixir-format -msgid "push.registration.button" -msgstr "Activeren" - -#, elixir-autogen, elixir-format -msgid "push.registration.title" -msgstr "Push notificaties" - -#, elixir-autogen, elixir-format -msgid "push.registration.activated" -msgstr "Push notificaties staan ingeschakeld op deze browser." - -#, elixir-autogen, elixir-format -msgid "push.registration.denied" -msgstr "Push notificaties zijn gedeactiveerd op deze browser" - -#, elixir-autogen, elixir-format -msgid "push.registration.label" -msgstr "Activeer om meldingen van het platform in je browser te ontvangen. Na het activeren kan het zijn dat je browser vraagt om toestemming voor het tonen van notificaties." - -#, elixir-autogen, elixir-format -msgid "push.registration.pending" -msgstr "Push notificaties zijn gedeactiveerd op deze browser" - #, elixir-autogen, elixir-format msgid "feature.study.description" msgstr "Om credits te kunnen behalen voor onderzoeksparticipatie (OP / RPR) voor bedrijfskunde (BK) of international business administration (IBA), selecteer je één of meerdere cursussen hieronder. Als je een cursus vorig jaar niet hebt afgerond, zorg dan dat je die cursus ook selecteert." @@ -110,10 +86,6 @@ msgstr "Inclusie" msgid "features.content.description" msgstr "Selecteer hieronder de inclusiecriteria voor je studie. Geen selectie betekend dat iedereen in de pool mee mag doen." -#, elixir-autogen, elixir-format -msgid "push.unavailable.label" -msgstr "Registereren voor push notificaties is op dit moment alleen beschikbaar op Chrome en Firefox browsers. Open deze website in een van deze browser om gebruik te maken van push notificaties." - #, elixir-autogen, elixir-format msgid "login.google.button" msgstr "Inloggen met Google" @@ -157,7 +129,3 @@ msgstr "Inloggen" #, elixir-autogen, elixir-format, fuzzy msgid "await.confirmation.description" msgstr "Je account is aangemaakt. We hebben je een e-mail gestuurd ter bevestiging. Klik op de link in de e-mail om je account te activeren." - -#, elixir-autogen, elixir-format -msgid "account.created.info.flash" -msgstr "Account succesvol aangemaakt" diff --git a/core/priv/gettext/nl/LC_MESSAGES/eyra-enums.po b/core/priv/gettext/nl/LC_MESSAGES/eyra-enums.po index 725a72663..d0233c3af 100644 --- a/core/priv/gettext/nl/LC_MESSAGES/eyra-enums.po +++ b/core/priv/gettext/nl/LC_MESSAGES/eyra-enums.po @@ -377,3 +377,7 @@ msgstr "Benchmark" #, elixir-autogen, elixir-format, fuzzy msgid "platforms.netflix" msgstr "Netflix" + +#, elixir-autogen, elixir-format, fuzzy +msgid "storage_service_ids.builtin" +msgstr "Built-in" diff --git a/core/priv/gettext/nl/LC_MESSAGES/eyra-storage.po b/core/priv/gettext/nl/LC_MESSAGES/eyra-storage.po index 95fc2e556..f7d74bc4f 100644 --- a/core/priv/gettext/nl/LC_MESSAGES/eyra-storage.po +++ b/core/priv/gettext/nl/LC_MESSAGES/eyra-storage.po @@ -58,3 +58,23 @@ msgstr "Portal URL" #, elixir-autogen, elixir-format msgid "yoda.user.label" msgstr "E-mail" + +#, elixir-autogen, elixir-format +msgid "aws.annotation" +msgstr "" + +#, elixir-autogen, elixir-format +msgid "azure.annotation" +msgstr "" + +#, elixir-autogen, elixir-format +msgid "builtin.annotation" +msgstr "" + +#, elixir-autogen, elixir-format +msgid "centerdata.annotation" +msgstr "" + +#, elixir-autogen, elixir-format +msgid "yoda.annotation" +msgstr "" diff --git a/core/priv/gettext/nl/LC_MESSAGES/eyra-ui.po b/core/priv/gettext/nl/LC_MESSAGES/eyra-ui.po index f3600ccd4..0023ffbd8 100644 --- a/core/priv/gettext/nl/LC_MESSAGES/eyra-ui.po +++ b/core/priv/gettext/nl/LC_MESSAGES/eyra-ui.po @@ -26,10 +26,6 @@ msgstr "Mijn console" msgid "menu.item.profile" msgstr "Mijn profiel" -#, elixir-autogen, elixir-format -msgid "menu.item.settings" -msgstr "Instellingen" - #, elixir-autogen, elixir-format msgid "menu.item.signin" msgstr "Inloggen" @@ -58,26 +54,10 @@ msgstr "To-do" msgid "tabbar.item.features" msgstr "Kenmerken" -#, elixir-autogen, elixir-format -msgid "tabbar.item.profile" -msgstr "Mijn profiel" - #, elixir-autogen, elixir-format msgid "tabbar.item.student" msgstr "Opleiding" -#, elixir-autogen, elixir-format -msgid "tabbar.item.features.forward" -msgstr "Pas kenmerken aan" - -#, elixir-autogen, elixir-format -msgid "tabbar.item.profile.forward" -msgstr "Pas profiel aan" - -#, elixir-autogen, elixir-format -msgid "tabbar.item.student.forward" -msgstr "Selecteer opleiding" - #, elixir-autogen, elixir-format msgid "menu.item.helpdesk" msgstr "Helpdesk" @@ -194,14 +174,6 @@ msgstr "Inklappen" msgid "expand.button" msgstr "Uitklappen" -#, elixir-autogen, elixir-format, fuzzy -msgid "tabbar.item.settings" -msgstr "Instellingen" - -#, elixir-autogen, elixir-format, fuzzy -msgid "tabbar.item.settings.forward" -msgstr "Instellingen" - #, elixir-autogen, elixir-format msgid "privacy.link" msgstr "Privacy" diff --git a/core/priv/repo/migrations/20240211150908_add_storage_built_in.exs b/core/priv/repo/migrations/20240211150908_add_storage_built_in.exs new file mode 100644 index 000000000..f9f7d52ab --- /dev/null +++ b/core/priv/repo/migrations/20240211150908_add_storage_built_in.exs @@ -0,0 +1,52 @@ +defmodule Core.Repo.Migrations.AddStorageBuiltIn do + use Ecto.Migration + + def up do + create table(:storage_endpoints_builtin) do + add(:key, :string, null: false) + timestamps() + end + + create(unique_index(:storage_endpoints_builtin, [:key])) + + alter table(:storage_endpoints) do + add(:builtin_id, references(:storage_endpoints_builtin, on_delete: :nilify_all), null: true) + end + + drop(constraint(:storage_endpoints, :must_have_at_most_one_special)) + + create( + constraint(:storage_endpoints, :must_have_at_most_one_special, + check: """ + 2 > CASE WHEN aws_id IS NULL THEN 0 ELSE 1 END + + CASE WHEN azure_id IS NULL THEN 0 ELSE 1 END + + CASE WHEN centerdata_id IS NULL THEN 0 ELSE 1 END + + CASE WHEN yoda_id IS NULL THEN 0 ELSE 1 END + + CASE WHEN builtin_id IS NULL THEN 0 ELSE 1 END + """ + ) + ) + end + + def down do + drop(constraint(:storage_endpoints, :must_have_at_most_one_special)) + + alter table(:storage_endpoints) do + remove(:builtin_id) + end + + create( + constraint(:storage_endpoints, :must_have_at_most_one_special, + check: """ + 2 > CASE WHEN aws_id IS NULL THEN 0 ELSE 1 END + + CASE WHEN azure_id IS NULL THEN 0 ELSE 1 END + + CASE WHEN centerdata_id IS NULL THEN 0 ELSE 1 END + + CASE WHEN yoda_id IS NULL THEN 0 ELSE 1 END + """ + ) + ) + + drop(index(:storage_endpoints_builtin, [:key])) + drop(table(:storage_endpoints_builtin)) + end +end diff --git a/core/systems/assignment/_private.ex b/core/systems/assignment/_private.ex index 09a51f41a..440fb5e44 100644 --- a/core/systems/assignment/_private.ex +++ b/core/systems/assignment/_private.ex @@ -12,6 +12,10 @@ defmodule Systems.Assignment.Private do Storage } + def storage_endpoint_key(%Assignment.Model{id: id}) do + "assignment=#{id}" + end + def get_panel_url(%Assignment.Model{id: id, external_panel: external_panel}) do case external_panel do :liss -> ~p"/assignment/#{id}/liss" diff --git a/core/systems/assignment/_public.ex b/core/systems/assignment/_public.ex index c1c03835a..80af8a40c 100644 --- a/core/systems/assignment/_public.ex +++ b/core/systems/assignment/_public.ex @@ -11,6 +11,7 @@ defmodule Systems.Assignment.Public do alias CoreWeb.UI.Timestamp alias Core.Authorization alias Core.Accounts.User + alias Frameworks.Utility.EctoHelper alias Frameworks.Concept alias Frameworks.Signal @@ -199,32 +200,27 @@ defmodule Systems.Assignment.Public do assignment end - def delete_storage_endpoint!(assignment) do + def delete_storage_endpoint!(%{storage_endpoint: %Ecto.Association.NotLoaded{}} = assignment) do + Repo.preload(assignment, :storage_endpoint, Storage.EndpointModel.preload_graph(:down)) + |> delete_storage_endpoint!() + end + + def delete_storage_endpoint!(%{storage_endpoint: storage_endpoint} = assignment) do changeset = Assignment.Model.changeset(assignment, %{}) |> Ecto.Changeset.put_assoc(:storage_endpoint, nil) - {:ok, assignment} = Core.Persister.save(changeset.data, changeset) - - assignment - end - - def create_storage_endpoint!(%{storage_endpoint_id: nil} = assignment) do - storage_endpoint = - %Storage.EndpointModel{} - |> Storage.EndpointModel.changeset(%{}) + storage_endpoint_special = Storage.EndpointModel.special(storage_endpoint) - {:ok, assignment} = - Assignment.Model.changeset(assignment, %{}) - |> Ecto.Changeset.put_assoc(:storage_endpoint, storage_endpoint) - |> Repo.update() + {:ok, %{assignment: assignment}} = + Multi.new() + |> EctoHelper.update_and_dispatch(changeset, :assignment) + |> Multi.delete(:storage_endpoint_special, storage_endpoint_special) + |> Repo.transaction() assignment - |> Repo.preload(Assignment.Model.preload_graph(:down)) end - def create_storage_endpoint!(assignment), do: assignment - def copy( %Assignment.Model{} = assignment, %Assignment.InfoModel{} = info, diff --git a/core/systems/assignment/connection_view.ex b/core/systems/assignment/connection_view.ex index 1f27da136..b53d46d6d 100644 --- a/core/systems/assignment/connection_view.ex +++ b/core/systems/assignment/connection_view.ex @@ -67,11 +67,18 @@ defmodule Systems.Assignment.ConnectionView do end defp update_special_view( - %{assigns: %{type: type, assignment: assignment, myself: myself, uri_origin: uri_origin}} = - socket + %{ + assigns: %{ + id: id, + type: type, + assignment: assignment, + myself: myself, + uri_origin: uri_origin + } + } = socket ) do special_view = %{ - id: "connection_view_#{type}", + id: "#{id}_connection_#{type}", module: Assignment.Private.connection_view_module(type), assignment: assignment, uri_origin: uri_origin, @@ -98,15 +105,13 @@ defmodule Systems.Assignment.ConnectionView do @impl true def handle_event("change", _payload, socket) do - # TODO - Logger.error("Ghost event?!!") {:noreply, socket} end @impl true def render(assigns) do ~H""" -
+
<:title>
diff --git a/core/systems/assignment/connection_view_storage.ex b/core/systems/assignment/connection_view_storage.ex index 054803713..030fb1bc8 100644 --- a/core/systems/assignment/connection_view_storage.ex +++ b/core/systems/assignment/connection_view_storage.ex @@ -1,9 +1,7 @@ defmodule Systems.Assignment.ConnectionViewStorage do use CoreWeb, :live_component - alias Systems.{ - Assignment - } + alias Systems.Assignment @impl true def update(%{event: :disconnect}, %{assigns: %{assignment: assignment}} = socket) do @@ -28,8 +26,7 @@ defmodule Systems.Assignment.ConnectionViewStorage do @impl true def render(assigns) do ~H""" -
-
+
""" end end diff --git a/core/systems/assignment/connector_popup_storage.ex b/core/systems/assignment/connector_popup_storage.ex index 8b7257b4f..e0816cc7d 100644 --- a/core/systems/assignment/connector_popup_storage.ex +++ b/core/systems/assignment/connector_popup_storage.ex @@ -4,6 +4,8 @@ defmodule Systems.Assignment.ConnectorPopupStorage do import CoreWeb.UI.Dialog + require Logger + alias Systems.{ Assignment, Storage @@ -62,11 +64,12 @@ defmodule Systems.Assignment.ConnectorPopupStorage do end @impl true - def compose(:storage_endpoint_form, %{storage_endpoint: storage_endpoint}) do + def compose(:storage_endpoint_form, %{storage_endpoint: storage_endpoint, entity: entity}) do %{ module: Storage.EndpointForm, params: %{ - endpoint: storage_endpoint + endpoint: storage_endpoint, + key: Assignment.Private.storage_endpoint_key(entity) } } end diff --git a/core/systems/assignment/crew_page.ex b/core/systems/assignment/crew_page.ex index 677c88b38..f155b571d 100644 --- a/core/systems/assignment/crew_page.ex +++ b/core/systems/assignment/crew_page.ex @@ -114,7 +114,7 @@ defmodule Systems.Assignment.CrewPage do socket = socket |> decline_member() - |> store("onboarding", "{\"status\":\"consent declined\"}") + |> store("onboarding", nil, "{\"status\":\"consent declined\"}") socket = if embedded? do @@ -130,8 +130,8 @@ defmodule Systems.Assignment.CrewPage do end @impl true - def handle_event("store", %{key: key, data: data}, socket) do - {:noreply, socket |> store(key, data)} + def handle_event("store", %{key: key, group: group, data: data}, socket) do + {:noreply, socket |> store(key, group, data)} end @impl true @@ -161,12 +161,14 @@ defmodule Systems.Assignment.CrewPage do } } = socket, key, + group, data ) do meta_data = %{ remote_ip: remote_ip, timestamp: Timestamp.now() |> DateTime.to_unix(), - key: key + key: key, + group: group } if storage_info = Storage.Private.storage_info(assignment) do diff --git a/core/systems/assignment/crew_work_view.ex b/core/systems/assignment/crew_work_view.ex index 51a03e165..703e7bbf7 100644 --- a/core/systems/assignment/crew_work_view.ex +++ b/core/systems/assignment/crew_work_view.ex @@ -354,13 +354,13 @@ defmodule Systems.Assignment.CrewWorkView do end end - defp handle_feldspar_event(socket, %{ + defp handle_feldspar_event(%{assigns: %{selected_item: {%{group: group}, _}}} = socket, %{ "__type__" => "CommandSystemDonate", "key" => key, "json_string" => json_string }) do socket - |> send_event(:parent, "store", %{key: key, data: json_string}) + |> send_event(:parent, "store", %{key: key, group: group, data: json_string}) |> Frameworks.Pixel.Flash.put_info("Donated") end diff --git a/core/systems/storage/_private.ex b/core/systems/storage/_private.ex index 977c1dd60..4981dc7ed 100644 --- a/core/systems/storage/_private.ex +++ b/core/systems/storage/_private.ex @@ -13,13 +13,15 @@ defmodule Systems.Storage.Private do Application.get_env(:core, :storage) end + def build_special(:builtin), do: %Storage.BuiltIn.EndpointModel{} + def build_special(:yoda), do: %Storage.Yoda.EndpointModel{} def build_special(:aws), do: %Storage.AWS.EndpointModel{} def build_special(:azure), do: %Storage.Azure.EndpointModel{} - def build_special(:yoda), do: %Storage.Yoda.EndpointModel{} + def backend_info(%Storage.BuiltIn.EndpointModel{}), do: {:builtin, Storage.BuiltIn.Backend} + def backend_info(%Storage.Yoda.EndpointModel{}), do: {:yoda, Storage.Yoda.Backend} def backend_info(%Storage.AWS.EndpointModel{}), do: {:aws, Storage.AWS.Backend} def backend_info(%Storage.Azure.EndpointModel{}), do: {:azure, Storage.Azure.Backend} - def backend_info(%Storage.Yoda.EndpointModel{}), do: {:yoda, Storage.Yoda.Backend} def storage_info(%{storage_endpoint: %{} = storage_endpoint, external_panel: external_panel}) do if endpoint = Storage.EndpointModel.special(storage_endpoint) do diff --git a/core/systems/storage/built_in/backend.ex b/core/systems/storage/built_in/backend.ex new file mode 100644 index 000000000..b6a80a0ae --- /dev/null +++ b/core/systems/storage/built_in/backend.ex @@ -0,0 +1,43 @@ +defmodule Systems.Storage.BuiltIn.Backend do + @behaviour Systems.Storage.Backend + + alias CoreWeb.UI.Timestamp + alias Systems.Storage.BuiltIn + + def store(%{"key" => folder}, panel_info, data, meta_data) do + identifier = identifier(panel_info, meta_data) + special().store(folder, identifier, data) + end + + def store(_, _, _, _) do + {:error, :endpoint_key_missing} + end + + defp identifier(%{"participant" => participant}, %{"key" => meta_key, "group" => group}) + when not is_nil(group) do + ["participant=#{participant}", "source=#{group}", meta_key] + end + + defp identifier(%{"participant" => participant}, %{"key" => meta_key}) do + ["participant=#{participant}", meta_key] + end + + defp identifier(%{"participant" => participant}, _) do + timestamp = Timestamp.now() |> DateTime.to_unix() + ["participant=#{participant}", "#{timestamp}"] + end + + defp identifier(_, _) do + timestamp = Timestamp.now() |> DateTime.to_unix() + ["participant=?", "#{timestamp}"] + end + + defp settings do + Application.fetch_env!(:core, Systems.Storage.BuiltIn) + end + + defp special do + # Allow mocking + Access.get(settings(), :special, BuiltIn.LocalFS) + end +end diff --git a/core/systems/storage/built_in/endpoint_form.ex b/core/systems/storage/built_in/endpoint_form.ex new file mode 100644 index 000000000..156bc8c89 --- /dev/null +++ b/core/systems/storage/built_in/endpoint_form.ex @@ -0,0 +1,47 @@ +defmodule Systems.Storage.BuiltIn.EndpointForm do + use CoreWeb.LiveForm, :fabric + use Fabric.LiveComponent + + require Logger + + alias Systems.Storage.BuiltIn.EndpointModel, as: Model + + @impl true + def update(%{model: model, key: key}, socket) do + attrs = + if Map.get(model, :key) != nil do + %{} + else + %{key: key} + end + + changeset = + Model.changeset(model, attrs) + |> Model.validate() + + changeset = + if model.id do + Map.put(changeset, :action, :update) + else + Map.put(changeset, :action, :insert) + end + + { + :ok, + socket + |> send_event(:parent, "update", %{changeset: changeset}) + } + end + + @impl true + def handle_event(_, _payload, socket) do + {:noreply, socket} + end + + @impl true + def render(assigns) do + ~H""" +
+ """ + end +end diff --git a/core/systems/storage/built_in/endpoint_model.ex b/core/systems/storage/built_in/endpoint_model.ex new file mode 100644 index 000000000..926be1bfb --- /dev/null +++ b/core/systems/storage/built_in/endpoint_model.ex @@ -0,0 +1,43 @@ +defmodule Systems.Storage.BuiltIn.EndpointModel do + use Ecto.Schema + use Frameworks.Utility.Schema + + import Ecto.Changeset + + @fields ~w(key)a + @required_fields @fields + + @derive {Jason.Encoder, only: @fields} + schema "storage_endpoints_builtin" do + field(:key, :string) + timestamps() + end + + def changeset(endpoint, params) do + endpoint + |> cast(params, @fields) + end + + def validate(changeset) do + changeset + |> validate_required(@required_fields) + |> unique_constraint(:key) + end + + def ready?(endpoint) do + changeset = + endpoint + |> changeset(%{}) + |> validate() + + changeset.valid?() + end + + def preload_graph(:down), do: [] + + defimpl Frameworks.Concept.ContentModel do + alias Systems.Storage.BuiltIn + def form(_), do: BuiltIn.EndpointForm + def ready?(endpoint), do: BuiltIn.EndpointModel.ready?(endpoint) + end +end diff --git a/core/systems/storage/built_in/local_fs.ex b/core/systems/storage/built_in/local_fs.ex new file mode 100644 index 000000000..d3f5c830f --- /dev/null +++ b/core/systems/storage/built_in/local_fs.ex @@ -0,0 +1,21 @@ +defmodule Systems.Storage.BuiltIn.LocalFS do + @behaviour Systems.Storage.BuiltIn.Special + use CoreWeb, :verified_routes + + @impl true + def store(folder, identifier, data) do + filename = Enum.join(identifier, "_") <> ".json" + folder_path = get_full_path(folder) + File.mkdir(folder_path) + file_path = Path.join(folder_path, filename) + File.write!(file_path, data) + end + + defp get_full_path(folder) do + Path.join(get_root_path(), folder) + end + + def get_root_path do + Application.get_env(:core, :upload_path) + end +end diff --git a/core/systems/storage/built_in/s3.ex b/core/systems/storage/built_in/s3.ex new file mode 100644 index 000000000..8c314d44e --- /dev/null +++ b/core/systems/storage/built_in/s3.ex @@ -0,0 +1,35 @@ +defmodule Systems.Storage.BuiltIn.S3 do + @behaviour Systems.Storage.BuiltIn.Special + alias ExAws.S3 + + @impl true + def store(folder, identifier, data) do + filename = Enum.join(identifier, "_") <> ".json" + filepath = Path.join(folder, filename) + object_key = object_key(filepath) + content_type = content_type(object_key) + bucket = Access.fetch!(settings(), :bucket) + + S3.put_object(bucket, object_key, data, content_type: content_type) + |> backend().request!() + end + + defp object_key(filepath) do + if prefix = Access.get(settings(), :prefix, nil) do + Path.join(prefix, filepath) + else + filepath + end + end + + defp content_type(name), do: MIME.from_path(name) + + defp settings do + Application.fetch_env!(:core, Systems.Storage.BuiltIn.S3) + end + + defp backend do + # Allow mocking + Access.get(settings(), :s3_backend, ExAws) + end +end diff --git a/core/systems/storage/built_in/special.ex b/core/systems/storage/built_in/special.ex new file mode 100644 index 000000000..45187db42 --- /dev/null +++ b/core/systems/storage/built_in/special.ex @@ -0,0 +1,7 @@ +defmodule Systems.Storage.BuiltIn.Special do + @callback store( + folder :: binary(), + identifier :: list(binary()), + data :: binary() + ) :: any() +end diff --git a/core/systems/storage/delivery.ex b/core/systems/storage/delivery.ex index 1dfc495ad..068ab0992 100644 --- a/core/systems/storage/delivery.ex +++ b/core/systems/storage/delivery.ex @@ -20,14 +20,21 @@ defmodule Systems.Storage.Delivery do {:error, error} _ -> - Logger.debug("Data delivery succeeded") + Logger.info("Data delivery succeeded") :ok end end def deliver(backend, endpoint, panel_info, data, meta_data) do Logger.warn("[Storage.Delivery] deliver") - backend.store(endpoint, panel_info, data, meta_data) + + try do + backend.store(endpoint, panel_info, data, meta_data) + rescue + e -> + Logger.error(Exception.format(:error, e, __STACKTRACE__)) + reraise e, __STACKTRACE__ + end end def deliver( diff --git a/core/systems/storage/endpoint_form.ex b/core/systems/storage/endpoint_form.ex index bc47e9951..d5439541a 100644 --- a/core/systems/storage/endpoint_form.ex +++ b/core/systems/storage/endpoint_form.ex @@ -3,7 +3,9 @@ defmodule Systems.Storage.EndpointForm do use Fabric.LiveComponent alias Frameworks.Concept - alias Frameworks.Pixel + alias Frameworks.Pixel.RadioGroup + alias Frameworks.Pixel.Annotation + alias Frameworks.Pixel.Panel alias Systems.{ Storage @@ -11,7 +13,7 @@ defmodule Systems.Storage.EndpointForm do @impl true def update( - %{id: id, endpoint: endpoint}, + %{id: id, endpoint: endpoint, key: key}, socket ) do { @@ -19,11 +21,12 @@ defmodule Systems.Storage.EndpointForm do socket |> assign( id: id, - endpoint: endpoint + endpoint: endpoint, + key: key ) |> update_special_type() |> update_special() - |> update_special_title() + |> update_special_annotation() |> compose_child(:type_selector) |> compose_child(:special_form) } @@ -39,16 +42,25 @@ defmodule Systems.Storage.EndpointForm do assign(socket, special: special) end - defp update_special_title(%{assigns: %{special_type: nil}} = socket) do - socket - |> assign(special_title: nil) + defp update_special_annotation(%{assigns: %{special_type: nil}} = socket) do + assign(socket, annotation: nil) end - defp update_special_title(%{assigns: %{special_type: special_type}} = socket) do - special_title = Storage.ServiceIds.translate(special_type) + defp update_special_annotation(%{assigns: %{special_type: special_type}} = socket) do + annotation = + case special_type do + :builtin -> dgettext("eyra-storage", "builtin.annotation") + :yoda -> dgettext("eyra-storage", "yoda.annotation") + :centerdata -> dgettext("eyra-storage", "centerdata.annotation") + :aws -> dgettext("eyra-storage", "aws.annotation") + :azure -> dgettext("eyra-storage", "azure.annotation") + end + + annotation_title = Storage.ServiceIds.translate(special_type) socket - |> assign(special_title: special_title) + |> assign(annotation: annotation) + |> assign(annotation_title: annotation_title) end @impl true @@ -56,7 +68,7 @@ defmodule Systems.Storage.EndpointForm do items = Storage.ServiceIds.labels(special_type, Storage.Private.allowed_service_ids()) %{ - module: Pixel.RadioGroup, + module: RadioGroup, params: %{ items: items } @@ -69,11 +81,12 @@ defmodule Systems.Storage.EndpointForm do end @impl true - def compose(:special_form, %{special: special}) do + def compose(:special_form, %{special: special, key: key}) do %{ module: Concept.ContentModel.form(special), params: %{ - model: special + model: special, + key: key } } end @@ -109,7 +122,7 @@ defmodule Systems.Storage.EndpointForm do special_changeset: nil, special: special ) - |> update_special_title() + |> update_special_annotation() |> compose_child(:type_selector) |> compose_child(:special_form) } @@ -139,10 +152,20 @@ defmodule Systems.Storage.EndpointForm do
<.child name={:type_selector} fabric={@fabric} />
+ <%= if @annotation do %> + <.spacing value="M" /> + + <:title> +
+ <%= @annotation_title %> +
+ + <.spacing value="XS" /> + +
+ <% end %> <%= if get_child(@fabric, :special_form) do %> - <.spacing value="L" /> - <%= @special_title %> - <.spacing value="XS" /> + <.spacing value="M" /> <.child name={:special_form} fabric={@fabric} /> <% end %>
diff --git a/core/systems/storage/endpoint_model.ex b/core/systems/storage/endpoint_model.ex index 1e15ac9dd..06a750013 100644 --- a/core/systems/storage/endpoint_model.ex +++ b/core/systems/storage/endpoint_model.ex @@ -13,17 +13,18 @@ defmodule Systems.Storage.EndpointModel do require Storage.ServiceIds schema "storage_endpoints" do + belongs_to(:builtin, Storage.BuiltIn.EndpointModel, on_replace: :delete) + belongs_to(:yoda, Storage.Yoda.EndpointModel, on_replace: :delete) + belongs_to(:centerdata, Storage.Centerdata.EndpointModel, on_replace: :delete) belongs_to(:aws, Storage.AWS.EndpointModel, on_replace: :delete) belongs_to(:azure, Storage.Azure.EndpointModel, on_replace: :delete) - belongs_to(:centerdata, Storage.Centerdata.EndpointModel, on_replace: :delete) - belongs_to(:yoda, Storage.Yoda.EndpointModel, on_replace: :delete) timestamps() end @fields ~w()a @required_fields @fields - @special_fields ~w(aws azure centerdata yoda)a + @special_fields ~w(builtin yoda centerdata aws azure)a def changeset(endpoint, params) do endpoint diff --git a/core/systems/storage/service_ids.ex b/core/systems/storage/service_ids.ex index b72c2c46e..7ef226aaf 100644 --- a/core/systems/storage/service_ids.ex +++ b/core/systems/storage/service_ids.ex @@ -3,5 +3,5 @@ defmodule Systems.Storage.ServiceIds do Defines list of supported storage services """ use Core.Enums.Base, - {:storage_service_ids, [:aws, :azure, :yoda]} + {:storage_service_ids, [:builtin, :yoda, :aws, :azure]} end diff --git a/core/test/core_web/controllers/user_session_controller_test.exs b/core/test/core_web/controllers/user_session_controller_test.exs index f5ba33ad1..0b82cccff 100644 --- a/core/test/core_web/controllers/user_session_controller_test.exs +++ b/core/test/core_web/controllers/user_session_controller_test.exs @@ -25,7 +25,7 @@ defmodule CoreWeb.UserSessionControllerTest do test "redirects if already logged in", %{conn: conn} do conn = get(conn, ~p"/user/signin") - assert redirected_to(conn) == "/project" + assert redirected_to(conn) =~ "/" end end diff --git a/core/test/core_web/live/user/confirm_token_test.exs b/core/test/core_web/live/user/confirm_token_test.exs index abb69c4b4..d68b43e33 100644 --- a/core/test/core_web/live/user/confirm_token_test.exs +++ b/core/test/core_web/live/user/confirm_token_test.exs @@ -103,8 +103,7 @@ defmodule CoreWeb.Live.User.ConfirmToken.Test do setup [:login_as_member] test "opening activation mail with expired (invalid) token should redirect", %{conn: conn} do - {:error, {:redirect, %{to: "/project"}}} = - live(conn, Routes.live_path(conn, ConfirmToken, "abc")) + {:error, {:redirect, %{to: _}}} = live(conn, Routes.live_path(conn, ConfirmToken, "abc")) end end end diff --git a/core/test/systems/storage/builtin/backend_test.exs b/core/test/systems/storage/builtin/backend_test.exs new file mode 100644 index 000000000..3c6f83b2c --- /dev/null +++ b/core/test/systems/storage/builtin/backend_test.exs @@ -0,0 +1,89 @@ +defmodule Systems.Storage.BuiltIn.BackendTest do + use ExUnit.Case, async: true + + import Mox + + alias Systems.Storage.BuiltIn.Backend + alias Systems.Storage.BuiltIn.MockSpecial + + setup :verify_on_exit! + + setup do + initial_config = Application.get_env(:core, Systems.Storage.BuiltIn) + + Application.put_env(:core, Systems.Storage.BuiltIn, special: MockSpecial) + + on_exit(fn -> + Application.put_env(:core, Systems.Storage.BuiltIn, initial_config) + end) + + :ok + end + + describe "store/4" do + test "unknown folder" do + assert {:error, :endpoint_key_missing} = Backend.store(%{}, %{}, "data", %{}) + end + + test "unknown participant" do + expect(MockSpecial, :store, fn _, identifier, data -> + assert ["participant=?", _unix_timestamp] = identifier + assert "data" = data + :ok + end) + + assert :ok = Backend.store(%{"key" => "assignment=1"}, %{}, "data", %{}) + end + + test "folder + participant" do + expect(MockSpecial, :store, fn folder, identifier, _data -> + assert "assignment=1" = folder + assert ["participant=1", _unix_timestamp] = identifier + :ok + end) + + assert :ok = Backend.store(%{"key" => "assignment=1"}, %{"participant" => 1}, "data", %{}) + end + + test "folder + participant + meta key" do + expect(MockSpecial, :store, fn folder, identifier, _data -> + assert "assignment=1" = folder + assert ["participant=1", "session=1"] = identifier + :ok + end) + + assert :ok = + Backend.store(%{"key" => "assignment=1"}, %{"participant" => 1}, "data", %{ + "key" => "session=1" + }) + end + + test "folder + participant + meta key + group" do + expect(MockSpecial, :store, fn folder, identifier, _data -> + assert "assignment=1" = folder + assert ["participant=1", "source=apple", "session=1"] = identifier + :ok + end) + + assert :ok = + Backend.store(%{"key" => "assignment=1"}, %{"participant" => 1}, "data", %{ + "key" => "session=1", + "group" => "apple" + }) + end + + test "folder + participant + meta key + group=nil" do + expect(MockSpecial, :store, fn folder, identifier, _data -> + assert "assignment=1" = folder + assert ["participant=1", "session=1"] = identifier + :ok + end) + + assert :ok = + Backend.store(%{"key" => "assignment=1"}, %{"participant" => 1}, "data", %{ + "key" => "session=1", + "group" => nil + }) + end + end +end diff --git a/core/test/test_helper.exs b/core/test/test_helper.exs index a2462e732..43a493d6b 100644 --- a/core/test/test_helper.exs +++ b/core/test/test_helper.exs @@ -27,3 +27,4 @@ Mox.defmock(BankingClient.MockClient, for: BankingClient.API) Application.put_env(:core, BankingClient, client: BankingClient.MockClient) Mox.defmock(Systems.Storage.MockBackend, for: Systems.Storage.Backend) +Mox.defmock(Systems.Storage.BuiltIn.MockSpecial, for: Systems.Storage.BuiltIn.Special)