diff --git a/apps/astarte_appengine_api/mix.exs b/apps/astarte_appengine_api/mix.exs index d88c59ecf..00b29f599 100644 --- a/apps/astarte_appengine_api/mix.exs +++ b/apps/astarte_appengine_api/mix.exs @@ -48,7 +48,7 @@ defmodule Astarte.AppEngine.API.Mixfile do end # Specifies which paths to compile per environment. - defp elixirc_paths(:test), do: ["test/support", "lib"] + defp elixirc_paths(:test), do: ["test/support", "test/support_v2", "lib"] defp elixirc_paths(_), do: ["lib"] defp dialyzer_cache_directory(:ci) do diff --git a/apps/astarte_appengine_api/mix.lock b/apps/astarte_appengine_api/mix.lock index 7423d03f5..bb2e7f1f4 100644 --- a/apps/astarte_appengine_api/mix.lock +++ b/apps/astarte_appengine_api/mix.lock @@ -15,16 +15,18 @@ "cqex": {:hex, :cqex, "1.0.1", "bc9980ac3b82d039879f8d6ca589deab799fe08f80ff449d60ad709f2524718f", [:mix], [{:cqerl, "~> 2.0.1", [hex: :cqerl, repo: "hexpm", optional: false]}], "hexpm", "1bbf2079c044cbf0f747f60dcf0409a951eaa8f1a2447cd6d80d6ff1b7c4dc6b"}, "credentials_obfuscation": {:hex, :credentials_obfuscation, "3.4.0", "34e18b126b3aefd6e8143776fbe1ceceea6792307c99ac5ee8687911f048cfd7", [:rebar3], [], "hexpm", "738ace0ed5545d2710d3f7383906fc6f6b582d019036e5269c4dbd85dbced566"}, "cyanide": {:hex, :cyanide, "2.0.0", "f97b700b87f9b0679ae812f0c4b7fe35ea6541a4121a096cf10287941b7a6d55", [:mix], [], "hexpm", "7f9748251804c2a2115b539202568e1117ab2f0ae09875853fb89cc94ae19dd1"}, - "db_connection": {:hex, :db_connection, "2.3.1", "4c9f3ed1ef37471cbdd2762d6655be11e38193904d9c5c1c9389f1b891a3088e", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "abaab61780dde30301d840417890bd9f74131041afd02174cf4e10635b3a63f5"}, - "decimal": {:hex, :decimal, "1.9.0", "83e8daf59631d632b171faabafb4a9f4242c514b0a06ba3df493951c08f64d07", [:mix], [], "hexpm", "b1f2343568eed6928f3e751cf2dffde95bfaa19dd95d09e8a9ea92ccfd6f7d85"}, + "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, + "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "dialyxir": {:hex, :dialyxir, "1.4.2", "764a6e8e7a354f0ba95d58418178d486065ead1f69ad89782817c296d0d746a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "516603d8067b2fd585319e4b13d3674ad4f314a5902ba8130cd97dc902ce6bbd"}, "dialyzex": {:git, "https://github.com/Comcast/dialyzex.git", "cdc7cf71fe6df0ce4cf59e3f497579697a05c989", []}, "ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"}, "ecto_morph": {:hex, :ecto_morph, "0.1.28", "073c3faf4ff7c496fc2ae8352ea7d8c9b89ae7b2e6995da3027454d44cb547fb", [:mix], [{:ecto, ">= 3.0.3", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "ce39a1252a5b7d58c601beb702eed4303b11016705f02fbae9b78f8fb971b0bc"}, + "ecto_sql": {:hex, :ecto_sql, "3.10.2", "6b98b46534b5c2f8b8b5f03f126e75e2a73c64f3c071149d32987a5378b0fdbd", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "68c018debca57cb9235e3889affdaec7a10616a4e3a80c99fa1d01fdafaa9007"}, "elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_json_schema": {:hex, :ex_json_schema, "0.7.4", "09eb5b0c8184e5702bc89625a9d0c05c7a0a845d382e9f6f406a0fc1c9a8cc3f", [:mix], [], "hexpm", "45c67fa840f0d719a2b5578126dc29bcdc1f92499c0f61bcb8a3bcb5935f9684"}, "ex_lttb": {:hex, :ex_lttb, "0.3.0", "aec7aab96be6535c4c8f143c2b5f2191a9d1a6f512690ec6d6f4f6bce0223b0f", [:mix], [], "hexpm", "6937bf70307d85781200912c3dcf5e32efdcbdf958e9107ee0a61d4fefc1fddb"}, + "exandra": {:hex, :exandra, "0.10.2", "e95dca77501df9ae48f23854224e91712e64d65cd7157e2fe46232ea97918ec6", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:xandra, "~> 0.18.0", [hex: :xandra, repo: "hexpm", optional: false]}], "hexpm", "334616b170233828f2acac0b060c3c6c91051848972218d2b159e3a455b07c84"}, "excoveralls": {:hex, :excoveralls, "0.15.0", "ac941bf85f9f201a9626cc42b2232b251ad8738da993cf406a4290cacf562ea4", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9631912006b27eca30a2f3c93562bc7ae15980afb014ceb8147dc5cdd8f376f1"}, "expo": {:hex, :expo, "0.5.2", "beba786aab8e3c5431813d7a44b828e7b922bfa431d6bfbada0904535342efe2", [:mix], [], "hexpm", "8c9bfa06ca017c9cb4020fabe980bc7fdb1aaec059fd004c2ab3bff03b1c599c"}, "gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"}, @@ -41,6 +43,7 @@ "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, "mox": {:hex, :mox, "0.5.2", "55a0a5ba9ccc671518d068c8dddd20eeb436909ea79d1799e2209df7eaa98b6c", [:mix], [], "hexpm", "df4310628cd628ee181df93f50ddfd07be3e5ecc30232d3b6aadf30bdfe6092b"}, + "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"}, "observer_cli": {:hex, :observer_cli, "1.6.1", "d176f967c978ab8b8a29c35c12524f78b7bb36fd4e9b8276dd75c9cb56e07e42", [:mix, :rebar3], [{:recon, "~>2.5.1", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm", "3418e319764b9dff1f469e43cbdffd7fd54ea47cbf765027c557abd146a19fb3"}, "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, "phoenix": {:hex, :phoenix, "1.7.2", "c375ffb482beb4e3d20894f84dd7920442884f5f5b70b9f4528cbe0cedefec63", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.4", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "1ebca94b32b4d0e097ab2444a9742ed8ff3361acad17365e4e6b2e79b4792159"}, @@ -74,5 +77,5 @@ "uuid": {:hex, :uuid_erl, "2.0.1", "1fd9079c544d521063897887a1c5b3302dca98f9bb06aadcdc6fb0663f256797", [:rebar3], [{:quickrand, "~> 2.0.1", [hex: :quickrand, repo: "hexpm", optional: false]}], "hexpm", "ab57caccd51f170011e5f444ce865f84b41605e483a9efcc468c1afaec87553b"}, "websock": {:hex, :websock, "0.5.1", "c496036ce95bc26d08ba086b2a827b212c67e7cabaa1c06473cd26b40ed8cf10", [:mix], [], "hexpm", "b9f785108b81cd457b06e5f5dabe5f65453d86a99118b2c0a515e1e296dc2d2c"}, "websock_adapter": {:hex, :websock_adapter, "0.5.1", "292e6c56724e3457e808e525af0e9bcfa088cc7b9c798218e78658c7f9b85066", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "8e2e1544bfde5f9d0442f9cec2f5235398b224f75c9e06b60557debf64248ec1"}, - "xandra": {:hex, :xandra, "0.13.1", "f82866e6c47527f74f35dd3007b5311121852dd861a29ed1613e27ccfaba0102", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.7", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "a2efdb8921e3b694bf3505e40c5ec9d353d8fa3755cec946be7c18b8236d7230"}, + "xandra": {:hex, :xandra, "0.18.1", "6ac8794161f69a5ada6e8c197e5e3472f44c94f7b3add208cd3abc8ee135f852", [:mix], [{:decimal, "~> 1.7 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "25d74d8101ca303b7be102da14a37629ae94c1bd21827f9d199a27f5e89b785f"}, } diff --git a/apps/astarte_appengine_api/test/astarte_appengine_api/v2_device_test.exs b/apps/astarte_appengine_api/test/astarte_appengine_api/v2_device_test.exs new file mode 100644 index 000000000..119981482 --- /dev/null +++ b/apps/astarte_appengine_api/test/astarte_appengine_api/v2_device_test.exs @@ -0,0 +1,69 @@ +# +# This file is part of Astarte. +# +# Copyright 2024 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +defmodule Astarte.AppEngine.API.V2DeviceTest do + use ExUnit.Case, async: true + use ExUnitProperties + use Astarte.Test.Cases.Device + + alias Ecto.Changeset + alias StreamData + alias Astarte.Core.Mapping + alias Astarte.Core.Interface + alias Astarte.AppEngine.API.Stats + alias Astarte.AppEngine.API.Stats.DevicesStats + alias Astarte.Test.Setups.Database, as: DatabaseSetup + alias Astarte.Test.Setups.Interface, as: InterfaceSetup + alias Astarte.Test.Generators.String, as: StringGenerator + alias Astarte.Test.Generators.Interface, as: InterfaceGenerator + alias Astarte.Test.Generators.Mapping, as: MappingGenerator + alias Astarte.Test.Generators.Device, as: DeviceGenerator + alias Astarte.Test.Helpers.Database, as: DatabaseHelper + + @moduletag :v2 + @moduletag :device + @moduletag interface_count: 10 + @moduletag device_count: 100 + + describe "device generator testing" do + property "validate device with pre-generated interfaces", %{interfaces: interfaces} do + check all device <- DeviceGenerator.device(interfaces: interfaces) do + :ok + end + end + end + + describe "devices fixtures testing" do + test "validate inserted devices", %{ + cluster: cluster, + keyspace: keyspace, + devices: devices + } do + list = DatabaseHelper.select!(:device, cluster, keyspace, devices) + fields = [:device_id, :encoded_id] + + for field <- fields do + f = fn l -> Enum.map(l, fn d -> d[field] end) end + devices_ids_a = f.(devices) + devices_ids_b = f.(list) + assert [] === devices_ids_a -- devices_ids_b + assert [] === devices_ids_b -- devices_ids_a + end + end + end +end diff --git a/apps/astarte_appengine_api/test/astarte_appengine_api/v2_group_test.exs b/apps/astarte_appengine_api/test/astarte_appengine_api/v2_group_test.exs new file mode 100644 index 000000000..7aadc3a9d --- /dev/null +++ b/apps/astarte_appengine_api/test/astarte_appengine_api/v2_group_test.exs @@ -0,0 +1,147 @@ +# +# This file is part of Astarte. +# +# Copyright 2024 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +defmodule Astarte.AppEngine.API.V2GroupTest do + use ExUnit.Case, async: true + use ExUnitProperties + use Astarte.Test.Cases.Group + use Astarte.Test.Cases.Conn + + alias Astarte.AppEngine.API.Device + alias Astarte.AppEngine.API.Device.DevicesList + alias Astarte.AppEngine.API.Device.DeviceStatus + alias Astarte.Test.Generators.Group, as: GroupGenerator + alias Astarte.Test.Generators.Device, as: DeviceGenerator + + @moduletag :v2 + @moduletag :group + @moduletag interface_count: 10 + @moduletag device_count: 100 + @moduletag group_count: 2 + + describe "create" do + @tag :unit + property "fails when group name is not valid", %{auth_conn: auth_conn, keyspace: keyspace} do + check all {{group_name, error}, devices} <- + tuple({ + bind(GroupGenerator.name(), fn name -> + bind(integer(0..2), fn num -> + constant( + case num do + 0 -> {"", "can't be blank"} + 1 -> {"~" <> name, "is not valid"} + 2 -> {"@" <> name, "is not valid"} + end + ) + end) + end), + list_of(DeviceGenerator.encoded_id(), min_length: 0, max_length: 1) + }) do + params = %{ + "group_name" => group_name, + "devices" => devices + } + + response = post(auth_conn, groups_path(auth_conn, :create, keyspace), data: params) + assert [error] === json_response(response, 422)["errors"]["group_name"] + end + end + + property "fails when devices list empty", %{auth_conn: auth_conn, keyspace: keyspace} do + @tag :unit + check all group_name <- GroupGenerator.name() do + params = %{ + "group_name" => group_name, + "devices" => [] + } + + response = post(auth_conn, groups_path(auth_conn, :create, keyspace), data: params) + + assert ["should have at least 1 item(s)"] === + json_response(response, 422)["errors"]["devices"] + end + end + + property "fails when device does not exist", %{auth_conn: auth_conn, keyspace: keyspace} do + check all group_name <- GroupGenerator.name(), + devices <- list_of(DeviceGenerator.encoded_id(), min_length: 1) do + params = %{ + "group_name" => group_name, + "devices" => devices + } + + response = post(auth_conn, groups_path(auth_conn, :create, keyspace), data: params) + + assert ["must exist (#{Enum.at(devices, 0)} not found)"] === + json_response(response, 422)["errors"]["devices"] + end + end + + test "fails when the group already exists", %{ + auth_conn: auth_conn, + cluster: cluster, + keyspace: keyspace, + interfaces: interfaces, + devices: devices, + groups: groups + } do + device_ids = Enum.map(devices, & &1.encoded_id) + existing_group_name = Enum.at(groups, 0).name + + params = %{ + "group_name" => existing_group_name, + "devices" => device_ids + } + + response = post(auth_conn, groups_path(auth_conn, :create, keyspace), data: params) + assert "Group already exists" === json_response(response, 409)["errors"]["detail"] + end + + property "success creates groups with valid parameters", %{ + auth_conn: auth_conn, + cluster: cluster, + keyspace: keyspace, + interfaces: interfaces, + devices: devices, + groups: groups + } do + device_ids = Enum.map(devices, & &1.encoded_id) + old_group_names = Enum.map(groups, & &1.name) + + check all group_name <- + filter(GroupGenerator.name(), fn name -> name not in old_group_names end) do + params = %{ + "group_name" => group_name, + "devices" => device_ids + } + + response = post(auth_conn, groups_path(auth_conn, :create, keyspace), data: params) + assert params === json_response(response, 201)["data"] + response = get(auth_conn, groups_path(auth_conn, :show, keyspace, group_name)) + assert group_name === json_response(response, 200)["data"]["group_name"] + + for device <- device_ids do + {:ok, %DeviceStatus{groups: groups}} = Device.get_device_status!(keyspace, device) + assert group_name in groups + # TODO right way + # assert [group_name] === groups -- old_group_names + end + end + end + end +end diff --git a/apps/astarte_appengine_api/test/astarte_appengine_api/v2_group_without_cases_test.exs b/apps/astarte_appengine_api/test/astarte_appengine_api/v2_group_without_cases_test.exs new file mode 100644 index 000000000..068d91a56 --- /dev/null +++ b/apps/astarte_appengine_api/test/astarte_appengine_api/v2_group_without_cases_test.exs @@ -0,0 +1,237 @@ +# +# This file is part of Astarte. +# +# Copyright 2024 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +defmodule Astarte.AppEngine.API.V2GroupTest do + use ExUnit.Case, async: true + use ExUnitProperties + + import Plug.Conn + import Phoenix.ConnTest + import Astarte.AppEngine.APIWeb.Router.Helpers + + # The default endpoint for testing + @endpoint Astarte.AppEngine.APIWeb.Endpoint + + alias Astarte.AppEngine.API.Device + alias Astarte.AppEngine.API.Device.DevicesList + alias Astarte.AppEngine.API.Device.DeviceStatus + alias Astarte.Test.Generators.Group, as: GroupGenerator + alias Astarte.Test.Generators.Device, as: DeviceGenerator + + describe "create" do + @tag :unit + @tag :v2 + @tag :group + @tag interface_count: 10 + @tag device_count: 100 + @tag group_count: 2 + setup [ + {DatabaseSetup, :connect}, + {DatabaseSetup, :keyspace}, + {DatabaseSetup, :setup}, + {DatabaseSetup, :setup_auth}, + {ConnSetup, :create_conn}, + {ConnSetup, :jwt}, + {ConnSetup, :auth_conn} + ] + + property "fails when group name is not valid", %{auth_conn: auth_conn, keyspace: keyspace} do + check all {{group_name, error}, devices} <- + tuple({ + bind(GroupGenerator.name(), fn name -> + bind(integer(0..2), fn num -> + constant( + case num do + 0 -> {"", "can't be blank"} + 1 -> {"~" <> name, "is not valid"} + 2 -> {"@" <> name, "is not valid"} + end + ) + end) + end), + list_of(DeviceGenerator.encoded_id(), min_length: 0, max_length: 1) + }) do + params = %{ + "group_name" => group_name, + "devices" => devices + } + + response = post(auth_conn, groups_path(auth_conn, :create, keyspace), data: params) + assert [error] === json_response(response, 422)["errors"]["group_name"] + end + end + + @tag :unit + @tag :v2 + @tag :group + @tag interface_count: 10 + @tag device_count: 100 + @tag group_count: 2 + setup [ + {DatabaseSetup, :connect}, + {DatabaseSetup, :keyspace}, + {DatabaseSetup, :setup}, + {DatabaseSetup, :setup_auth}, + {ConnSetup, :create_conn}, + {ConnSetup, :jwt}, + {ConnSetup, :auth_conn} + ] + + property "fails when devices list empty", %{auth_conn: auth_conn, keyspace: keyspace} do + @tag :unit + check all group_name <- GroupGenerator.name() do + params = %{ + "group_name" => group_name, + "devices" => [] + } + + response = post(auth_conn, groups_path(auth_conn, :create, keyspace), data: params) + + assert ["should have at least 1 item(s)"] === + json_response(response, 422)["errors"]["devices"] + end + end + + @tag :unit + @tag :v2 + @tag :group + @tag interface_count: 10 + @tag device_count: 100 + @tag group_count: 2 + setup [ + {DatabaseSetup, :connect}, + {DatabaseSetup, :keyspace}, + {DatabaseSetup, :setup}, + {DatabaseSetup, :setup_auth}, + {ConnSetup, :create_conn}, + {ConnSetup, :jwt}, + {ConnSetup, :auth_conn} + ] + + property "fails when device does not exist", %{auth_conn: auth_conn, keyspace: keyspace} do + check all group_name <- GroupGenerator.name(), + devices <- list_of(DeviceGenerator.encoded_id(), min_length: 1) do + params = %{ + "group_name" => group_name, + "devices" => devices + } + + response = post(auth_conn, groups_path(auth_conn, :create, keyspace), data: params) + + assert ["must exist (#{Enum.at(devices, 0)} not found)"] === + json_response(response, 422)["errors"]["devices"] + end + end + + @tag :unit + @tag :v2 + @tag :group + @tag interface_count: 10 + @tag device_count: 100 + @tag group_count: 2 + setup [ + {DatabaseSetup, :connect}, + {DatabaseSetup, :keyspace}, + {DatabaseSetup, :setup}, + {DatabaseSetup, :setup_auth}, + {InterfaceSetup, :init}, + {InterfaceSetup, :setup}, + {DeviceSetup, :init}, + {DeviceSetup, :setup}, + {GroupSetup, :init}, + {GroupSetup, :setup}, + {ConnSetup, :create_conn}, + {ConnSetup, :jwt}, + {ConnSetup, :auth_conn} + ] + + test "fails when the group already exists", %{ + auth_conn: auth_conn, + cluster: cluster, + keyspace: keyspace, + interfaces: interfaces, + devices: devices, + groups: groups + } do + device_ids = Enum.map(devices, & &1.encoded_id) + existing_group_name = Enum.at(groups, 0).name + + params = %{ + "group_name" => existing_group_name, + "devices" => device_ids + } + + response = post(auth_conn, groups_path(auth_conn, :create, keyspace), data: params) + assert "Group already exists" === json_response(response, 409)["errors"]["detail"] + end + + setup [ + {DatabaseSetup, :connect}, + {DatabaseSetup, :keyspace}, + {DatabaseSetup, :setup}, + {DatabaseSetup, :setup_auth}, + {InterfaceSetup, :init}, + {InterfaceSetup, :setup}, + {DeviceSetup, :init}, + {DeviceSetup, :setup}, + {GroupSetup, :init}, + {GroupSetup, :setup}, + {ConnSetup, :create_conn}, + {ConnSetup, :jwt}, + {ConnSetup, :auth_conn} + ] + + @tag :unit + @tag :v2 + @tag :group + @tag interface_count: 10 + @tag device_count: 100 + @tag group_count: 2 + test "success creates groups with valid parameters", %{ + auth_conn: auth_conn, + cluster: cluster, + keyspace: keyspace, + interfaces: interfaces, + devices: devices, + groups: groups + } do + device_ids = Enum.map(devices, & &1.encoded_id) + old_group_names = Enum.map(groups, & &1.name) + + check all group_name <- + filter(GroupGenerator.name(), fn name -> name not in old_group_names end) do + params = %{ + "group_name" => group_name, + "devices" => device_ids + } + + response = post(auth_conn, groups_path(auth_conn, :create, keyspace), data: params) + assert params === json_response(response, 201)["data"] + response = get(auth_conn, groups_path(auth_conn, :show, keyspace, group_name)) + assert group_name === json_response(response, 200)["data"]["group_name"] + + for device <- device_ids do + {:ok, %DeviceStatus{groups: groups}} = Device.get_device_status!(keyspace, device) + assert group_name in groups + # TODO right way + # assert [group_name] === groups -- old_group_names + end + end + end + end +end diff --git a/apps/astarte_appengine_api/test/astarte_appengine_api/v2_interface_test.exs b/apps/astarte_appengine_api/test/astarte_appengine_api/v2_interface_test.exs new file mode 100644 index 000000000..154420bae --- /dev/null +++ b/apps/astarte_appengine_api/test/astarte_appengine_api/v2_interface_test.exs @@ -0,0 +1,50 @@ +defmodule Astarte.AppEngine.API.V2InterfaceTest do + use ExUnit.Case, async: true + use ExUnitProperties + use Astarte.Test.Cases.Interface + + alias Ecto.Changeset + alias StreamData + alias Astarte.Core.Mapping + alias Astarte.Core.Interface + alias Astarte.AppEngine.API.Stats + alias Astarte.AppEngine.API.Stats.DevicesStats + alias Astarte.Test.Setups.Database, as: DatabaseSetup + alias Astarte.Test.Setups.Interface, as: InterfaceSetup + alias Astarte.Test.Generators.String, as: StringGenerator + alias Astarte.Test.Generators.Interface, as: InterfaceGenerator + alias Astarte.Test.Generators.Mapping, as: MappingGenerator + alias Astarte.Test.Generators.Device, as: DeviceGenerator + alias Astarte.Test.Helpers.Database, as: DatabaseHelper + + @moduletag :v2 + @moduletag :interface + @moduletag interface_count: 10 + + describe "interface generator" do + @tag timeout: :infinity + property "validate interface" do + check all interface <- InterfaceGenerator.interface() do + %Changeset{valid?: valid, errors: errors} = Interface.changeset(interface) + + assert valid, "Invalid interface: " <> (errors |> Enum.join(", ")) + end + end + end + + describe "interfaces db" do + test "validate inserted interfaces names", %{ + cluster: cluster, + keyspace: keyspace, + interfaces: interfaces + } do + list = DatabaseHelper.select!(:interface, cluster, keyspace, interfaces) + f = fn l -> Enum.map(l, fn i -> i.name end) end + interfaces_names_a = f.(interfaces) + interfaces_names_b = f.(list) + + assert [] === interfaces_names_a -- interfaces_names_b + assert [] === interfaces_names_b -- interfaces_names_a + end + end +end diff --git a/apps/astarte_appengine_api/test/support_v2/cases/conn.ex b/apps/astarte_appengine_api/test/support_v2/cases/conn.ex new file mode 100644 index 000000000..619dfd4ec --- /dev/null +++ b/apps/astarte_appengine_api/test/support_v2/cases/conn.ex @@ -0,0 +1,40 @@ +# +# This file is part of Astarte. +# +# Copyright 2024 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +defmodule Astarte.Test.Cases.Conn do + use ExUnit.CaseTemplate + + using do + quote do + import Plug.Conn + import Phoenix.ConnTest + import Astarte.AppEngine.APIWeb.Router.Helpers + + # The default endpoint for testing + @endpoint Astarte.AppEngine.APIWeb.Endpoint + end + end + + alias Astarte.Test.Setups.Conn, as: ConnSetup + + setup_all [ + {ConnSetup, :create_conn}, + {ConnSetup, :jwt}, + {ConnSetup, :auth_conn} + ] +end diff --git a/apps/astarte_appengine_api/test/support_v2/cases/database.ex b/apps/astarte_appengine_api/test/support_v2/cases/database.ex new file mode 100644 index 000000000..1ebe755ee --- /dev/null +++ b/apps/astarte_appengine_api/test/support_v2/cases/database.ex @@ -0,0 +1,29 @@ +# +# This file is part of Astarte. +# +# Copyright 2024 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +defmodule Astarte.Test.Cases.Database do + use ExUnit.CaseTemplate + alias Astarte.Test.Setups.Database, as: DatabaseSetup + + setup_all [ + {DatabaseSetup, :connect}, + {DatabaseSetup, :keyspace}, + {DatabaseSetup, :setup}, + {DatabaseSetup, :setup_auth} + ] +end diff --git a/apps/astarte_appengine_api/test/support_v2/cases/device.ex b/apps/astarte_appengine_api/test/support_v2/cases/device.ex new file mode 100644 index 000000000..14b869006 --- /dev/null +++ b/apps/astarte_appengine_api/test/support_v2/cases/device.ex @@ -0,0 +1,28 @@ +# +# This file is part of Astarte. +# +# Copyright 2024 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +defmodule Astarte.Test.Cases.Device do + use ExUnit.CaseTemplate + use Astarte.Test.Cases.Interface + alias Astarte.Test.Setups.Device, as: DeviceSetup + + setup_all [ + {DeviceSetup, :init}, + {DeviceSetup, :setup} + ] +end diff --git a/apps/astarte_appengine_api/test/support_v2/cases/group.ex b/apps/astarte_appengine_api/test/support_v2/cases/group.ex new file mode 100644 index 000000000..a122e9427 --- /dev/null +++ b/apps/astarte_appengine_api/test/support_v2/cases/group.ex @@ -0,0 +1,28 @@ +# +# This file is part of Astarte. +# +# Copyright 2024 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +defmodule Astarte.Test.Cases.Group do + use ExUnit.CaseTemplate + use Astarte.Test.Cases.Device + alias Astarte.Test.Setups.Group, as: GroupSetup + + setup_all [ + {GroupSetup, :init}, + {GroupSetup, :setup} + ] +end diff --git a/apps/astarte_appengine_api/test/support_v2/cases/interface.ex b/apps/astarte_appengine_api/test/support_v2/cases/interface.ex new file mode 100644 index 000000000..8fbb2d203 --- /dev/null +++ b/apps/astarte_appengine_api/test/support_v2/cases/interface.ex @@ -0,0 +1,28 @@ +# +# This file is part of Astarte. +# +# Copyright 2024 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +defmodule Astarte.Test.Cases.Interface do + use ExUnit.CaseTemplate + use Astarte.Test.Cases.Database + alias Astarte.Test.Setups.Interface, as: InterfaceSetup + + setup_all [ + {InterfaceSetup, :init}, + {InterfaceSetup, :setup} + ] +end diff --git a/apps/astarte_appengine_api/test/support_v2/generators/common.ex b/apps/astarte_appengine_api/test/support_v2/generators/common.ex new file mode 100644 index 000000000..449894e7e --- /dev/null +++ b/apps/astarte_appengine_api/test/support_v2/generators/common.ex @@ -0,0 +1,34 @@ +# +# This file is part of Astarte. +# +# Copyright 2024 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +defmodule Astarte.Test.Generators.Common do + use ExUnitProperties + + def keyspace_name() do + repeatedly(fn -> "realm#{System.unique_integer([:positive, :monotonic])}" end) + end + + def ip() do + gen all a <- integer(0..255), + b <- integer(0..255), + c <- integer(0..255), + d <- integer(0..255) do + {a, b, c, d} + end + end +end diff --git a/apps/astarte_appengine_api/test/support_v2/generators/datetime.ex b/apps/astarte_appengine_api/test/support_v2/generators/datetime.ex new file mode 100644 index 000000000..053f72b33 --- /dev/null +++ b/apps/astarte_appengine_api/test/support_v2/generators/datetime.ex @@ -0,0 +1,32 @@ +# +# This file is part of Astarte. +# +# Copyright 2024 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +defmodule Astarte.Test.Generators.DateTime do + use ExUnitProperties + + @min_default 0 + @max_default 2_556_143_999 + + def date_time!(opts \\ []) do + [min: min, max: max] = Keyword.validate!(opts, min: @min_default, max: @max_default) + date_time!(min, max) + end + + defp date_time!(min, max) when min < max, do: integer(min..max) |> map(&DateTime.from_unix!(&1)) + defp date_time!(min, max), do: raise("Datetime generator, received min: #{min} >= max: #{max}") +end diff --git a/apps/astarte_appengine_api/test/support_v2/generators/device.ex b/apps/astarte_appengine_api/test/support_v2/generators/device.ex new file mode 100644 index 000000000..7d6fde889 --- /dev/null +++ b/apps/astarte_appengine_api/test/support_v2/generators/device.ex @@ -0,0 +1,132 @@ +# +# This file is part of Astarte. +# +# Copyright 2024 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +defmodule Astarte.Test.Generators.Device do + use ExUnitProperties + alias Astarte.Core.Device + alias Astarte.Test.Generators.Common, as: CommonGenerator + alias Astarte.Test.Generators.DateTime, as: DateTimeGenerator + + def id() do + gen all seq <- binary(length: 16) do + <> = seq + <> + end + end + + def encoded_id() do + gen all id <- id() do + Base.url_encode64(id, padding: false) + end + end + + defp received do + gen all msgs <- integer(1..10_000), + bytes <- integer(10..10_000) do + {msgs, msgs * bytes} + end + end + + defp aliases do + one_of([ + map_of(string(:alphanumeric, min_length: 1), string(:alphanumeric, min_length: 1)), + constant(nil) + ]) + end + + defp attributes do + one_of([ + map_of(string(:alphanumeric, min_length: 1), string(:alphanumeric, min_length: 1)), + constant(nil) + ]) + end + + defp interface_maps(interface_names) do + one_of([ + member_of(interface_names) + |> Enum.take(length(interface_names)) + |> Enum.uniq() + |> Enum.map(fn name -> {name, 0..1 |> Enum.random()} end) + |> Enum.reduce({%{}, %{}}, fn i, {msgs, bytes} -> + {m, b} = received() |> Enum.at(0) + + { + Map.merge(msgs, %{i => m}), + Map.merge(bytes, %{i => b}) + } + end) + |> constant(), + constant(nil) + ]) + end + + defp dates() do + time_zone = "Etc/UTC" + + gen all last_disconnection <- + DateTimeGenerator.date_time!(max: DateTime.now!(time_zone) |> DateTime.to_unix()), + last_connection <- + DateTimeGenerator.date_time!(max: DateTime.now!(time_zone) |> DateTime.to_unix()), + first_credentials_request <- + DateTimeGenerator.date_time!(max: last_connection |> DateTime.to_unix()), + first_registration <- + DateTimeGenerator.date_time!(max: first_credentials_request |> DateTime.to_unix()) do + {first_registration, first_credentials_request, last_connection, last_disconnection} + end + end + + def device(interfaces: interfaces) do + gen all id <- id(), + last_seen_ip <- CommonGenerator.ip(), + last_credentials_request_ip <- CommonGenerator.ip(), + inhibit_credentials_request <- boolean(), + { + first_registration, + first_credentials_request, + last_connection, + last_disconnection + } <- dates(), + {interfaces_msgs, interfaces_bytes} <- + interfaces + |> Enum.map(fn i -> i.name end) + |> interface_maps(), + aliases <- aliases(), + attributes <- attributes() do + %{ + id: id, + device_id: id, + encoded_id: Device.encode_device_id(id), + connected: DateTime.after?(last_connection, last_disconnection), + first_registration: first_registration, + first_credentials_request: first_credentials_request, + last_connection: last_connection, + last_disconnection: last_disconnection, + last_seen_ip: last_seen_ip, + inhibit_credentials_request: inhibit_credentials_request, + last_credentials_request_ip: last_credentials_request_ip, + interfaces_msgs: interfaces_msgs, + interfaces_bytes: interfaces_bytes, + aliases: aliases, + attributes: attributes, + total_received_msgs: Enum.reduce(interfaces_msgs, 0, fn {_, msgs}, acc -> acc + msgs end), + total_received_bytes: + Enum.reduce(interfaces_bytes, 0, fn {_, bytes}, acc -> acc + bytes end) + } + end + end +end diff --git a/apps/astarte_appengine_api/test/support_v2/generators/group.ex b/apps/astarte_appengine_api/test/support_v2/generators/group.ex new file mode 100644 index 000000000..cb1927980 --- /dev/null +++ b/apps/astarte_appengine_api/test/support_v2/generators/group.ex @@ -0,0 +1,48 @@ +# +# This file is part of Astarte. +# +# Copyright 2024 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +defmodule Astarte.Test.Generators.Group do + use ExUnitProperties + + @max_subpath_count 10 + @max_subpath_length 20 + + def name() do + string(:ascii, min_length: 1, max_length: @max_subpath_length) + |> uniq_list_of( + min_length: 1, + max_length: @max_subpath_count + ) + |> map(&Enum.join(&1, "/")) + |> filter(fn name -> + String.first(name) not in ["@", "~", "\s"] + end) + end + + def group(devices: devices) do + gen all name <- name(), + device_ids <- + devices + |> Enum.map(fn i -> i.device_id end) + |> constant() do + %{ + name: name, + device_ids: device_ids + } + end + end +end diff --git a/apps/astarte_appengine_api/test/support_v2/generators/interface.ex b/apps/astarte_appengine_api/test/support_v2/generators/interface.ex new file mode 100644 index 000000000..19042bc10 --- /dev/null +++ b/apps/astarte_appengine_api/test/support_v2/generators/interface.ex @@ -0,0 +1,126 @@ +# +# This file is part of Astarte. +# +# Copyright 2024 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +defmodule Astarte.Test.Generators.Interface do + use ExUnitProperties + + alias Ecto.UUID + alias Astarte.Core.Interface + alias Astarte.Test.Generators.String, as: StringGenerator + alias Astarte.Test.Generators.Mapping, as: MappingGenerator + + defp id() do + repeatedly(&UUID.generate/0) + end + + defp name() do + string(:alphanumeric, min_length: 1, max_length: 16) + |> list_of( + min_length: 2, + max_length: 5 + ) + |> filter(fn [<> | _] -> + first < 48 or first > 57 + end) + |> map(&Enum.join(&1, ".")) + end + + defp versions() do + gen all major_version <- integer(0..9), + minor_version <- integer(0..255) do + case {major_version, minor_version} do + {0, 0} -> {0, 1} + valid -> valid + end + end + end + + defp type(), do: member_of([:datastream, :properties]) + + defp ownership(), do: member_of([:device, :server]) + + defp mappings(config) do + uniq_list_of(MappingGenerator.mapping(config), min_length: 1, max_length: 1000) + end + + defp aggregation(%{type: :properties}), do: constant(:individual) + defp aggregation(_), do: member_of([:individual, :object]) + + defp description() do + string(:ascii, min_length: 1, max_length: 1000) + end + + defp doc() do + string(:ascii, min_length: 1, max_length: 100_000) + end + + defp required_fields() do + gen all id <- id(), + name <- name(), + {major_version, minor_version} <- versions(), + type <- type(), + aggregation <- aggregation(%{type: type}), + ownership <- ownership(), + prefix <- StringGenerator.endpoint_prefix(), + retention <- MappingGenerator.retention(), + reliability <- MappingGenerator.reliability(), + expiry <- MappingGenerator.expiry(), + allow_unset <- MappingGenerator.allow_unset(), + explicit_timestamp <- MappingGenerator.explicit_timestamp(), + mappings <- + mappings(%{ + aggregation: aggregation, + prefix: prefix, + retention: retention, + reliability: reliability, + expiry: expiry, + allow_unset: allow_unset, + explicit_timestamp: explicit_timestamp + }) do + %{ + id: id, + interface_id: id, + name: name, + interface_name: name, + major_version: major_version, + minor_version: minor_version, + version_major: major_version, + version_minor: minor_version, + type: type, + interface_type: type, + ownership: ownership, + aggregation: aggregation, + mappings: mappings + } + end + end + + defp optional_fields() do + optional_map(%{ + description: description(), + doc: doc() + }) + end + + def interface() do + gen all required <- required_fields(), + optional <- optional_fields() do + struct(Interface, Map.merge(required, optional)) + end + end +end diff --git a/apps/astarte_appengine_api/test/support_v2/generators/mapping.ex b/apps/astarte_appengine_api/test/support_v2/generators/mapping.ex new file mode 100644 index 000000000..7d4455c4e --- /dev/null +++ b/apps/astarte_appengine_api/test/support_v2/generators/mapping.ex @@ -0,0 +1,108 @@ +# +# This file is part of Astarte. +# +# Copyright 2024 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +defmodule Astarte.Test.Generators.Mapping do + use ExUnitProperties + + alias Astarte.Core.Mapping + + defp endpoint(%{aggregation: aggregation, prefix: prefix}) do + generator = + case aggregation do + :individual -> repeatedly(fn -> "/individual_#{System.unique_integer([:positive])}" end) + :object -> repeatedly(fn -> "/object_#{System.unique_integer([:positive])}" end) + end + + gen all postfix <- generator do + prefix <> postfix + end + end + + defp type() do + member_of([ + :double, + :integer, + :boolean, + :longinteger, + :string, + :binaryblob, + :datetime, + :doublearray, + :integerarray, + :booleanarray, + :longintegerarray, + :stringarray, + :binaryblobarray, + :datetimearray + ]) + end + + def reliability(), do: member_of([:unreliable, :guaranteed, :unique]) + + def explicit_timestamp(), do: boolean() + + def retention(), do: member_of([:discard, :volatile, :stored]) + + def expiry(), do: one_of([constant(0), integer(1..10_000)]) + + def database_retention_policy(), do: member_of([:no_ttl, :use_ttl]) + + def database_retention_ttl(), do: integer(0..10_1000) + + def allow_unset(), do: boolean() + + defp description(), do: string(:ascii, min_length: 1, max_length: 1000) + + defp doc(), do: string(:ascii, min_length: 1, max_length: 100_000) + + defp required_fields(%{ + aggregation: aggregation, + prefix: prefix, + retention: retention, + reliability: reliability, + explicit_timestamp: explicit_timestamp, + allow_unset: allow_unset, + expiry: expiry + }) do + fixed_map(%{ + endpoint: endpoint(%{aggregation: aggregation, prefix: prefix}), + type: type(), + retention: constant(retention), + reliability: constant(reliability), + explicit_timestamp: constant(explicit_timestamp), + allow_unset: constant(allow_unset), + expiry: constant(expiry) + }) + end + + defp optional_fields(_config) do + optional_map(%{ + database_retention_policy: database_retention_policy(), + database_retention_ttl: database_retention_ttl(), + description: description(), + doc: doc() + }) + end + + def mapping(config) do + gen all required <- required_fields(config), + optional <- optional_fields(config) do + struct(Mapping, Map.merge(required, optional)) + end + end +end diff --git a/apps/astarte_appengine_api/test/support_v2/generators/number.ex b/apps/astarte_appengine_api/test/support_v2/generators/number.ex new file mode 100644 index 000000000..b23c794f4 --- /dev/null +++ b/apps/astarte_appengine_api/test/support_v2/generators/number.ex @@ -0,0 +1,29 @@ +# +# This file is part of Astarte. +# +# Copyright 2024 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +defmodule Astarte.Test.Generators.Number do + use ExUnitProperties + + def random_numbers(max) do + gen all first <- StreamData.integer(0..max), + second <- StreamData.integer(0..max), + third <- StreamData.integer(0..max) do + {first, second, third} + end + end +end diff --git a/apps/astarte_appengine_api/test/support_v2/generators/string.ex b/apps/astarte_appengine_api/test/support_v2/generators/string.ex new file mode 100644 index 000000000..5793e2837 --- /dev/null +++ b/apps/astarte_appengine_api/test/support_v2/generators/string.ex @@ -0,0 +1,50 @@ +# +# This file is part of Astarte. +# +# Copyright 2024 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +defmodule Astarte.Test.Generators.String do + use ExUnitProperties + + def random_string(max) do + string(:alphanumeric, length: max) + end + + def endpoint_subpath() do + string([?a..?z, ?_], min_length: 1, max_length: 5) + end + + def endpoint_parametric_subpath() do + gen all subpath <- endpoint_subpath() do + "%{" <> subpath <> "}" + end + end + + def endpoint_prefix() do + gen all prefix <- + frequency([ + {1, endpoint_subpath()}, + {1, endpoint_parametric_subpath()} + ]) + |> list_of( + min_length: 1, + max_length: 5 + ) + |> map(&Enum.join(&1, "/")) do + "/" <> prefix + end + end +end diff --git a/apps/astarte_appengine_api/test/support_v2/helpers/database.ex b/apps/astarte_appengine_api/test/support_v2/helpers/database.ex new file mode 100644 index 000000000..168e62c6a --- /dev/null +++ b/apps/astarte_appengine_api/test/support_v2/helpers/database.ex @@ -0,0 +1,640 @@ +# +# This file is part of Astarte. +# +# Copyright 2024 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +defmodule Astarte.Test.Helpers.Database do + alias Astarte.Core.Device + alias Astarte.Core.Interface + alias Astarte.Core.Interface.Type, as: InterfaceType + alias Astarte.Core.Interface.Aggregation, as: AggregationType + alias Astarte.Core.Interface.Ownership, as: OwnershipType + + ### + ### Keyspace + @create_keyspace """ + CREATE KEYSPACE :keyspace + WITH + replication = {'class': 'SimpleStrategy', 'replication_factor': '1'} AND + durable_writes = true; + """ + + @drop_keyspace """ + DROP KEYSPACE :keyspace + """ + + ### + ### Tables + @create_kv_store """ + CREATE TABLE :keyspace.kv_store ( + group varchar, + key varchar, + value blob, + + PRIMARY KEY ((group), key) + ) + """ + + @create_names_table """ + CREATE TABLE :keyspace.names ( + object_name varchar, + object_type int, + object_uuid uuid, + + PRIMARY KEY ((object_name), object_type) + ) + """ + + @create_devices_table """ + CREATE TABLE :keyspace.devices ( + device_id uuid, + aliases map, + introspection map, + introspection_minor map, + old_introspection map>, int>, + protocol_revision int, + first_registration timestamp, + credentials_secret ascii, + inhibit_credentials_request boolean, + cert_serial ascii, + cert_aki ascii, + first_credentials_request timestamp, + last_connection timestamp, + last_disconnection timestamp, + connected boolean, + pending_empty_cache boolean, + total_received_msgs bigint, + total_received_bytes bigint, + exchanged_msgs_by_interface map>, bigint>, + exchanged_bytes_by_interface map>, bigint>, + last_credentials_request_ip inet, + last_seen_ip inet, + groups map, + attributes map, + + PRIMARY KEY (device_id) + ) + """ + + @create_interfaces_table """ + CREATE TABLE :keyspace.interfaces ( + name ascii, + major_version int, + minor_version int, + interface_id uuid, + storage_type int, + storage ascii, + type int, + ownership int, + aggregation int, + automaton_transitions blob, + automaton_accepting_states blob, + description varchar, + doc varchar, + + PRIMARY KEY (name, major_version) + ) + """ + + @create_endpoints_table """ + CREATE TABLE :keyspace.endpoints ( + interface_id uuid, + endpoint_id uuid, + interface_name ascii, + interface_major_version int, + interface_minor_version int, + interface_type int, + endpoint ascii, + value_type int, + reliability int, + retention int, + database_retention_policy int, + database_retention_ttl int, + expiry int, + allow_unset boolean, + explicit_timestamp boolean, + description varchar, + doc varchar, + + PRIMARY KEY ((interface_id), endpoint_id) + ) + """ + @create_individual_properties_table """ + CREATE TABLE :keyspace.individual_properties ( + device_id uuid, + interface_id uuid, + endpoint_id uuid, + path varchar, + reception_timestamp timestamp, + reception_timestamp_submillis smallint, + + double_value double, + integer_value int, + boolean_value boolean, + longinteger_value bigint, + string_value varchar, + binaryblob_value blob, + datetime_value timestamp, + doublearray_value list, + integerarray_value list, + booleanarray_value list, + longintegerarray_value list, + stringarray_value list, + binaryblobarray_value list, + datetimearray_value list, + + PRIMARY KEY((device_id, interface_id), endpoint_id, path) + ) + """ + + @create_individual_datastreams_table """ + CREATE TABLE :keyspace.individual_datastreams ( + device_id uuid, + interface_id uuid, + endpoint_id uuid, + path varchar, + value_timestamp timestamp, + reception_timestamp timestamp, + reception_timestamp_submillis smallint, + + double_value double, + integer_value int, + boolean_value boolean, + longinteger_value bigint, + string_value varchar, + binaryblob_value blob, + datetime_value timestamp, + doublearray_value list, + integerarray_value list, + booleanarray_value list, + longintegerarray_value list, + stringarray_value list, + binaryblobarray_value list, + datetimearray_value list, + + PRIMARY KEY((device_id, interface_id, endpoint_id, path), value_timestamp, reception_timestamp, reception_timestamp_submillis) + ) + """ + + @create_groups_table """ + CREATE TABLE :keyspace.grouped_devices ( + group_name varchar, + insertion_uuid timeuuid, + device_id uuid, + PRIMARY KEY ((group_name), insertion_uuid, device_id) + ) + """ + + ### + ### Insert + @insert_interface """ + INSERT INTO :keyspace.interfaces ( + name, + interface_id, + major_version, + minor_version, + type, + automaton_accepting_states, + automaton_transitions, + aggregation, + ownership, + storage, + storage_type + ) VALUES ( + :name, + :interface_id, + :major_version, + :minor_version, + :type, + :automaton_accepting_states, + :automaton_transitions, + :aggregation, + :ownership, + :storage, + :storage_type + ) + """ + + @insert_device """ + INSERT INTO :keyspace.devices + ( + device_id, + aliases, + attributes, + connected, + last_connection, + last_disconnection, + first_registration, + first_credentials_request, + last_seen_ip, + last_credentials_request_ip, + total_received_msgs, + total_received_bytes, + inhibit_credentials_request, + introspection, + introspection_minor, + exchanged_msgs_by_interface, + exchanged_bytes_by_interface + ) + VALUES + ( + :device_id, + :aliases, + :attributes, + :connected, + :last_connection, + :last_disconnection, + :first_registration, + :first_credentials_request, + :last_seen_ip, + :last_credentials_request_ip, + :total_received_msgs, + :total_received_bytes, + :inhibit_credentials_request, + :introspection, + :introspection_minor, + :exchanged_msgs_by_interface, + :exchanged_bytes_by_interface + ) + """ + + @insert_alias """ + INSERT INTO :keyspace.names ( + object_name, + object_type, + object_uuid + ) + VALUES ( + :object_name, + 1, + :object_uuid + ) + """ + + @insert_group """ + INSERT INTO :keyspace.grouped_devices ( + group_name, + insertion_uuid, + device_id + ) + VALUES + ( + :group_name, + :insertion_uuid, + :device_id + ) + """ + + @insert_pubkeypem """ + INSERT INTO :keyspace.kv_store (group, key, value) + VALUES ('auth', 'jwt_public_key_pem', varcharAsBlob(:pem)); + """ + + ### + ### Update + @update_device """ + UPDATE :keyspace.devices + SET :field = :field + :data + WHERE device_id = :device_id + """ + + ### + ### Select + @select_interfaces """ + SELECT * FROM :keyspace.interfaces WHERE name IN :names + """ + + @select_devices """ + SELECT * FROM :keyspace.devices WHERE device_id IN :device_ids + """ + + ### + ### Delete + @delete_interface """ + DELETE FROM :keyspace.interfaces WHERE name = :name + """ + + @delete_device """ + DELETE FROM :keyspace.devices WHERE device_id = :device_id + """ + + @delete_alias """ + DELETE FROM :keyspace.names + WHERE object_name = :object_name + AND object_type = 1 + """ + + @delete_group """ + DELETE FROM :keyspace.grouped_devices WHERE group_name = :group_name + """ + + @delete_pubkeypem """ + DELETE FROM :keyspace.kv_store WHERE group = 'auth' AND key = 'jwt_public_key_pem' + """ + + ### + ### Keyspace + def create_test_keyspace!(cluster, keyspace) do + Xandra.Cluster.execute!(cluster, String.replace(@create_keyspace, ":keyspace", keyspace)) + + Xandra.Cluster.execute!(cluster, String.replace(@create_devices_table, ":keyspace", keyspace)) + Xandra.Cluster.execute!(cluster, String.replace(@create_groups_table, ":keyspace", keyspace)) + Xandra.Cluster.execute!(cluster, String.replace(@create_names_table, ":keyspace", keyspace)) + Xandra.Cluster.execute!(cluster, String.replace(@create_kv_store, ":keyspace", keyspace)) + + Xandra.Cluster.execute!( + cluster, + String.replace(@create_endpoints_table, ":keyspace", keyspace) + ) + + Xandra.Cluster.execute!( + cluster, + String.replace(@create_individual_properties_table, ":keyspace", keyspace) + ) + + Xandra.Cluster.execute!( + cluster, + String.replace(@create_individual_datastreams_table, ":keyspace", keyspace) + ) + + Xandra.Cluster.execute!( + cluster, + String.replace(@create_interfaces_table, ":keyspace", keyspace) + ) + end + + def destroy_test_keyspace!(cluster, keyspace) do + Xandra.Cluster.execute!(cluster, String.replace(@drop_keyspace, ":keyspace", keyspace)) + end + + ### + ### Insert + def insert!(:pubkeypem, cluster, keyspace, pub_key_pem) do + query = + Xandra.Cluster.prepare!(cluster, String.replace(@insert_pubkeypem, ":keyspace", keyspace)) + + Xandra.Cluster.execute!(cluster, query, %{ + "pem" => pub_key_pem + }) + end + + def insert!(:interface, cluster, keyspace, interfaces) do + prepared = + Xandra.Cluster.prepare!(cluster, String.replace(@insert_interface, ":keyspace", keyspace)) + + batch = + Enum.reduce(interfaces, Xandra.Batch.new(), fn interface, acc -> + Xandra.Batch.add(acc, prepared, %{ + "name" => interface.name, + "interface_id" => interface.interface_id, + "major_version" => interface.major_version, + "minor_version" => interface.minor_version, + "type" => InterfaceType.to_int(interface.type), + "automaton_accepting_states" => :erlang.term_to_binary(:automaton_accepting_states), + "automaton_transitions" => :erlang.term_to_binary(:automaton_transitions), + "aggregation" => AggregationType.to_int(interface.aggregation), + # TODO Mapping to the other table + "ownership" => OwnershipType.to_int(interface.ownership), + "storage" => "individual_properties", + "storage_type" => 1 + }) + end) + + Xandra.Cluster.execute!(cluster, batch) + end + + def insert!(:device, cluster, keyspace, devices) do + prepared_device = + Xandra.Cluster.prepare!(cluster, String.replace(@insert_device, ":keyspace", keyspace)) + + prepared_aliases = + Xandra.Cluster.prepare!(cluster, String.replace(@insert_alias, ":keyspace", keyspace)) + + batch = + Enum.reduce(devices, Xandra.Batch.new(), fn device, acc -> + acc = + Xandra.Batch.add(acc, prepared_device, %{ + "device_id" => device.id, + "aliases" => device.aliases, + "attributes" => device.attributes, + "connected" => device.connected, + "last_connection" => device.last_connection, + "last_disconnection" => device.last_disconnection, + "first_registration" => device.first_registration, + "first_credentials_request" => device.first_credentials_request, + "last_seen_ip" => device.last_seen_ip, + "last_credentials_request_ip" => device.last_credentials_request_ip, + "total_received_msgs" => device.total_received_msgs, + "total_received_bytes" => device.total_received_bytes, + "inhibit_credentials_request" => device.inhibit_credentials_request, + "introspection" => %{ + "org.astarte-platform.genericsensors.ServerOwnedAggregateObj" => 0 + }, + "introspection_minor" => %{ + "org.astarte-platform.genericsensors.ServerOwnedAggregateObj" => 1 + }, + "exchanged_msgs_by_interface" => device.interfaces_msgs, + "exchanged_bytes_by_interface" => device.interfaces_bytes + }) + + aliases = if device.aliases != nil, do: device.aliases, else: [] + + Enum.reduce(aliases, acc, fn {_, name}, acc -> + Xandra.Batch.add(acc, prepared_aliases, %{ + "object_name" => name, + "object_type" => 1, + "object_uuid" => device.id + }) + end) + end) + + Xandra.Cluster.execute!(cluster, batch) + end + + def insert!(:group, cluster, keyspace, groups) do + prepared_group = + Xandra.Cluster.prepare!(cluster, String.replace(@insert_group, ":keyspace", keyspace)) + + prepared_device = + Xandra.Cluster.prepare!( + cluster, + String.replace(@update_device, ":keyspace", keyspace) + |> String.replace(":field", "groups") + ) + + batch = + Enum.reduce(groups, Xandra.Batch.new(), fn group, acc -> + Enum.reduce(group.device_ids, acc, fn device_id, acc -> + uuid = UUID.uuid1() + + acc = + Xandra.Batch.add(acc, prepared_group, %{ + "group_name" => group.name, + "insertion_uuid" => uuid, + "device_id" => device_id + }) + + Xandra.Batch.add(acc, prepared_device, %{ + "device_id" => device_id, + "data" => %{group.name => uuid} + }) + end) + end) + + Xandra.Cluster.execute!(cluster, batch) + end + + ### + ### Select + def select!(:interface, cluster, keyspace, interfaces) do + prepared = + Xandra.Cluster.prepare!(cluster, String.replace(@select_interfaces, ":keyspace", keyspace)) + + list = + interfaces + |> Stream.map(fn %Interface{} = interface -> interface.name end) + |> Enum.to_list() + + %Xandra.Page{} = + page = + Xandra.Cluster.execute!( + cluster, + prepared, + %{ + "names" => list + } + ) + + Stream.map(page, fn record -> + %Interface{ + name: record["name"], + interface_id: record["interface_id"], + major_version: record["major_version"], + minor_version: record["minor_version"], + type: InterfaceType.from_int(record["type"]), + aggregation: AggregationType.from_int(record["aggregation"]), + # TODO Mapping from the other table + ownership: OwnershipType.from_int(record["ownership"]) + } + end) + |> Enum.to_list() + end + + def select!(:device, cluster, keyspace, devices) do + prepared = + Xandra.Cluster.prepare!(cluster, String.replace(@select_devices, ":keyspace", keyspace)) + + list = + devices + |> Stream.map(fn device -> device.device_id end) + |> Enum.to_list() + + %Xandra.Page{} = + page = + Xandra.Cluster.execute!( + cluster, + prepared, + %{ + "device_ids" => list + }, + uuid_format: :binary + ) + + Stream.map(page, fn record -> + %{ + id: record["device_id"], + device_id: record["device_id"], + encoded_id: record["device_id"] |> Device.encode_device_id(), + aliases: record["aliases"], + attributes: record["attributes"], + connected: record["connected"], + last_connection: record["last_connection"], + last_disconnection: record["last_disconnection"], + first_registration: record["first_registration"], + first_credentials_request: record["first_credentials_request"], + last_seen_ip: record["last_seen_ip"], + last_credentials_request_ip: record["last_credentials_request_ip"], + total_received_msgs: record["total_received_msgs"], + total_received_bytes: record["total_received_bytes"], + inhibit_credentials_request: record["inhibit_credentials_request"], + exchanged_msgs_by_interface: record["exchanged_msgs_by_interface"], + exchanged_bytes_by_interface: record["exchanged_bytes_by_interface"] + } + end) + |> Enum.to_list() + end + + ### + ### Delete + def delete!(:pubkeypem, cluster, keyspace) do + Xandra.Cluster.execute!(cluster, String.replace(@delete_pubkeypem, ":keyspace", keyspace)) + end + + def delete!(:interface, cluster, keyspace, interfaces) do + prepared = + Xandra.Cluster.prepare!(cluster, String.replace(@delete_interface, ":keyspace", keyspace)) + + batch = + Enum.reduce(interfaces, Xandra.Batch.new(), fn interface, acc -> + Xandra.Batch.add(acc, prepared, %{ + "name" => interface.name + }) + end) + + Xandra.Cluster.execute!(cluster, batch) + end + + def delete!(:device, cluster, keyspace, devices) do + prepared_alias = + Xandra.Cluster.prepare!(cluster, String.replace(@delete_alias, ":keyspace", keyspace)) + + prepared_device = + Xandra.Cluster.prepare!(cluster, String.replace(@delete_device, ":keyspace", keyspace)) + + batch = + Enum.reduce(devices, Xandra.Batch.new(), fn device, acc -> + aliases = if device.aliases != nil, do: device.aliases, else: [] + + acc = + Enum.reduce(aliases, acc, fn {_, name}, acc -> + Xandra.Batch.add(acc, prepared_alias, %{ + "object_name" => name, + "object_type" => 1 + }) + end) + + Xandra.Batch.add(acc, prepared_device, %{ + "device_id" => device.device_id + }) + end) + + Xandra.Cluster.execute!(cluster, batch) + end + + def delete!(:group, cluster, keyspace, groups) do + prepared_group = + Xandra.Cluster.prepare!(cluster, String.replace(@delete_group, ":keyspace", keyspace)) + + batch = + Enum.reduce(groups, Xandra.Batch.new(), fn group, acc -> + Xandra.Batch.add(acc, prepared_group, %{ + "group_name" => group.name + }) + end) + + Xandra.Cluster.execute!(cluster, batch) + end +end diff --git a/apps/astarte_appengine_api/test/support_v2/helpers/jwt.ex b/apps/astarte_appengine_api/test/support_v2/helpers/jwt.ex new file mode 100644 index 000000000..fb2c4d49f --- /dev/null +++ b/apps/astarte_appengine_api/test/support_v2/helpers/jwt.ex @@ -0,0 +1,66 @@ +# +# This file is part of Astarte. +# +# Copyright 2024 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +defmodule Astarte.Test.Helpers.JWT do + alias Astarte.AppEngine.API.Auth.User + alias Astarte.AppEngine.APIWeb.AuthGuardian + + def public_key_pem do + Application.get_env(:astarte_appengine_api, :test_pub_key_pem) + end + + def gen_jwt_token(authorization_paths) do + jwk = + Application.get_env(:astarte_appengine_api, :test_priv_key) + |> JOSE.JWK.from_map() + + {:ok, jwt, claims} = + %User{id: "testuser"} + |> AuthGuardian.encode_and_sign( + %{a_aea: authorization_paths}, + secret: jwk, + allowed_algos: ["RS256"] + ) + + {jwt, claims} + end + + def gen_jwt_all_access_token do + gen_jwt_token([".*::.*"]) + end + + def gen_channels_jwt_token(authorization_paths) do + jwk = + Application.get_env(:astarte_appengine_api, :test_priv_key) + |> JOSE.JWK.from_map() + + {:ok, jwt, _claims} = + %User{id: "testuser"} + |> AuthGuardian.encode_and_sign( + %{a_ch: authorization_paths}, + secret: jwk, + allowed_algos: ["RS256"] + ) + + jwt + end + + def gen_channels_jwt_all_access_token do + gen_channels_jwt_token(["JOIN::.*", "WATCH::.*"]) + end +end diff --git a/apps/astarte_appengine_api/test/support_v2/setups/conn.ex b/apps/astarte_appengine_api/test/support_v2/setups/conn.ex new file mode 100644 index 000000000..1ae4ab747 --- /dev/null +++ b/apps/astarte_appengine_api/test/support_v2/setups/conn.ex @@ -0,0 +1,40 @@ +# +# This file is part of Astarte. +# +# Copyright 2024 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +defmodule Astarte.Test.Setups.Conn do + import Plug.Conn + alias Phoenix.ConnTest + alias Astarte.Test.Helpers.JWT, as: JWTHelper + + def create_conn(_context) do + {:ok, conn: ConnTest.build_conn()} + end + + def jwt(_context) do + {:ok, jwt: JWTHelper.gen_jwt_all_access_token()} + end + + def auth_conn(%{conn: conn, jwt: {jwt, _claims}}) do + auth_conn = + conn + |> put_req_header("accept", "application/json") + |> put_req_header("authorization", "bearer #{jwt}") + + {:ok, auth_conn: auth_conn} + end +end diff --git a/apps/astarte_appengine_api/test/support_v2/setups/database.ex b/apps/astarte_appengine_api/test/support_v2/setups/database.ex new file mode 100644 index 000000000..c43893888 --- /dev/null +++ b/apps/astarte_appengine_api/test/support_v2/setups/database.ex @@ -0,0 +1,50 @@ +# +# This file is part of Astarte. +# +# Copyright 2024 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +defmodule Astarte.Test.Setups.Database do + use ExUnit.Case, async: false + alias Astarte.Test.Generators.Common, as: CommonGenerator + alias Astarte.Test.Helpers.Database, as: DatabaseHelper + alias Astarte.Test.Helpers.JWT, as: JWTHelper + + def connect(_context) do + {:ok, cluster: :xandra} + end + + def keyspace(_context) do + {:ok, keyspace: CommonGenerator.keyspace_name() |> Enum.at(0)} + end + + def setup(%{cluster: cluster, keyspace: keyspace}) do + on_exit(fn -> + DatabaseHelper.destroy_test_keyspace!(cluster, keyspace) + end) + + DatabaseHelper.create_test_keyspace!(cluster, keyspace) + {:ok, keyspace: keyspace} + end + + def setup_auth(%{cluster: cluster, keyspace: keyspace}) do + on_exit(fn -> + DatabaseHelper.delete!(:pubkeypem, cluster, keyspace) + end) + + DatabaseHelper.insert!(:pubkeypem, cluster, keyspace, JWTHelper.public_key_pem()) + {:ok, keyspace: keyspace} + end +end diff --git a/apps/astarte_appengine_api/test/support_v2/setups/device.ex b/apps/astarte_appengine_api/test/support_v2/setups/device.ex new file mode 100644 index 000000000..0c8812e26 --- /dev/null +++ b/apps/astarte_appengine_api/test/support_v2/setups/device.ex @@ -0,0 +1,36 @@ +# +# This file is part of Astarte. +# +# Copyright 2024 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +defmodule Astarte.Test.Setups.Device do + use ExUnit.Case, async: false + alias Astarte.Test.Helpers.Database, as: DatabaseHelper + alias Astarte.Test.Generators.Device, as: DeviceGenerator + + def init(%{device_count: device_count, interfaces: interfaces}) do + {:ok, devices: DeviceGenerator.device(interfaces: interfaces) |> Enum.take(device_count)} + end + + def setup(%{cluster: cluster, keyspace: keyspace, devices: devices}) do + on_exit(fn -> + DatabaseHelper.delete!(:device, cluster, keyspace, devices) + end) + + DatabaseHelper.insert!(:device, cluster, keyspace, devices) + :ok + end +end diff --git a/apps/astarte_appengine_api/test/support_v2/setups/group.ex b/apps/astarte_appengine_api/test/support_v2/setups/group.ex new file mode 100644 index 000000000..204b04dc2 --- /dev/null +++ b/apps/astarte_appengine_api/test/support_v2/setups/group.ex @@ -0,0 +1,36 @@ +# +# This file is part of Astarte. +# +# Copyright 2024 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +defmodule Astarte.Test.Setups.Group do + use ExUnit.Case, async: false + alias Astarte.Test.Helpers.Database, as: DatabaseHelper + alias Astarte.Test.Generators.Group, as: GroupGenerator + + def init(%{group_count: group_count, devices: devices}) do + {:ok, groups: GroupGenerator.group(devices: devices) |> Enum.take(group_count)} + end + + def setup(%{cluster: cluster, keyspace: keyspace, groups: groups}) do + on_exit(fn -> + DatabaseHelper.delete!(:group, cluster, keyspace, groups) + end) + + DatabaseHelper.insert!(:group, cluster, keyspace, groups) + :ok + end +end diff --git a/apps/astarte_appengine_api/test/support_v2/setups/interface.ex b/apps/astarte_appengine_api/test/support_v2/setups/interface.ex new file mode 100644 index 000000000..f29c32392 --- /dev/null +++ b/apps/astarte_appengine_api/test/support_v2/setups/interface.ex @@ -0,0 +1,36 @@ +# +# This file is part of Astarte. +# +# Copyright 2024 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +defmodule Astarte.Test.Setups.Interface do + use ExUnit.Case, async: false + alias Astarte.Test.Helpers.Database, as: DatabaseHelper + alias Astarte.Test.Generators.Interface, as: InterfaceGenerator + + def init(%{interface_count: interface_count}) do + {:ok, interfaces: InterfaceGenerator.interface() |> Enum.take(interface_count)} + end + + def setup(%{cluster: cluster, keyspace: keyspace, interfaces: interfaces}) do + on_exit(fn -> + DatabaseHelper.delete!(:interface, cluster, keyspace, interfaces) + end) + + DatabaseHelper.insert!(:interface, cluster, keyspace, interfaces) + :ok + end +end