diff --git a/backend/config/config.exs b/backend/config/config.exs index 5c1ad6bd3..bde78eaa2 100644 --- a/backend/config/config.exs +++ b/backend/config/config.exs @@ -91,6 +91,7 @@ config :edgehog, Edgehog.PromEx, config :edgehog, :ash_apis, [ Edgehog.Astarte, Edgehog.Devices, + Edgehog.Groups, Edgehog.Labeling, Edgehog.Tenants ] diff --git a/backend/lib/edgehog/groups.ex b/backend/lib/edgehog/groups.ex deleted file mode 100644 index b494105f9..000000000 --- a/backend/lib/edgehog/groups.ex +++ /dev/null @@ -1,179 +0,0 @@ -# -# This file is part of Edgehog. -# -# Copyright 2022 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. -# -# SPDX-License-Identifier: Apache-2.0 -# - -defmodule Edgehog.Groups do - @moduledoc """ - The Groups context. - """ - - import Ecto.Query, warn: false - alias Edgehog.Devices - alias Edgehog.Groups.DeviceGroup - alias Edgehog.Repo - alias Edgehog.Selector - - @doc """ - Returns the list of device_groups. - - ## Examples - - iex> list_device_groups() - [%DeviceGroup{}, ...] - - """ - def list_device_groups do - Repo.all(DeviceGroup) - end - - @doc """ - Returns the list of devices belonging to `device_group`. - - ## Examples - - iex> list_devices_in_group(device_group) - [%Devices.Device{}, ...] - - """ - def list_devices_in_group(%DeviceGroup{} = device_group) do - # This gets validated when the DeviceGroup is created, if it fails here then there's a bug - # and it's legitimate we crash - {:ok, device_query} = Selector.to_ecto_query(device_group.selector) - - Repo.all(device_query) - |> Devices.preload_defaults_for_device() - end - - @doc """ - Returns a `device_id -> list_of_groups` map for the passed Device (database) ids. - - This allows retrieving the list for all devices by doing one query for the group list and one - query for each of the groups (so it's independent from the number of devices). - - ## Examples - - iex> get_groups_for_device_ids(device_ids) - %{1 => [%DeviceGroup{}, ...], 2 => []} - """ - def get_groups_for_device_ids(device_ids) when is_list(device_ids) do - initial_acc = Map.new(device_ids, fn id -> {id, []} end) - - list_device_groups() - |> Enum.reduce(initial_acc, fn device_group, acc -> - # This gets validated when the DeviceGroup is created, if it fails here then there's a bug - # and it's legitimate we crash - {:ok, device_query} = Selector.to_ecto_query(device_group.selector) - - # We just need the ids, no need to load the whole device from the DB - # We also additionally filter device ids to the ones provided as arguments - query = - from d in device_query, - where: d.id in ^device_ids, - select: d.id - - Repo.all(query) - |> Enum.reduce(acc, fn device_id, acc -> - Map.update!(acc, device_id, &[device_group | &1]) - end) - end) - end - - @doc """ - Fetches a single device_group. - - Returns `{:ok, device_group}` or `{:error, :not_found}` if the Device group does not exist. - - ## Examples - - iex> fetch_device_group(123) - {:ok, %DeviceGroup{}} - - iex> fetch_device_group(456) - {:error, :not_found} - - """ - def fetch_device_group(id) do - Repo.fetch(DeviceGroup, id) - end - - @doc """ - Creates a device_group. - - ## Examples - - iex> create_device_group(%{field: value}) - {:ok, %DeviceGroup{}} - - iex> create_device_group(%{field: bad_value}) - {:error, %Ecto.Changeset{}} - - """ - def create_device_group(attrs \\ %{}) do - %DeviceGroup{} - |> DeviceGroup.changeset(attrs) - |> Repo.insert() - end - - @doc """ - Updates a device_group. - - ## Examples - - iex> update_device_group(device_group, %{field: new_value}) - {:ok, %DeviceGroup{}} - - iex> update_device_group(device_group, %{field: bad_value}) - {:error, %Ecto.Changeset{}} - - """ - def update_device_group(%DeviceGroup{} = device_group, attrs) do - device_group - |> DeviceGroup.changeset(attrs) - |> Repo.update() - end - - @doc """ - Deletes a device_group. - - ## Examples - - iex> delete_device_group(device_group) - {:ok, %DeviceGroup{}} - - iex> delete_device_group(device_group) - {:error, %Ecto.Changeset{}} - - """ - def delete_device_group(%DeviceGroup{} = device_group) do - Repo.delete(device_group) - end - - @doc """ - Returns an `%Ecto.Changeset{}` for tracking device_group changes. - - ## Examples - - iex> change_device_group(device_group) - %Ecto.Changeset{data: %DeviceGroup{}} - - """ - def change_device_group(%DeviceGroup{} = device_group, attrs \\ %{}) do - DeviceGroup.changeset(device_group, attrs) - end -end diff --git a/backend/lib/edgehog/groups/device_group.ex b/backend/lib/edgehog/groups/device_group.ex deleted file mode 100644 index df9180bfd..000000000 --- a/backend/lib/edgehog/groups/device_group.ex +++ /dev/null @@ -1,61 +0,0 @@ -# -# This file is part of Edgehog. -# -# Copyright 2022 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. -# -# SPDX-License-Identifier: Apache-2.0 -# - -defmodule Edgehog.Groups.DeviceGroup do - use Ecto.Schema - import Ecto.Changeset - - alias Edgehog.Selector - - schema "device_groups" do - field :tenant_id, :integer, autogenerate: {Edgehog.Repo, :get_tenant_id, []} - field :handle, :string - field :name, :string - field :selector, :string - field :update_channel_id, :id - - timestamps() - end - - @doc false - def changeset(device_group, attrs) do - device_group - |> cast(attrs, [:name, :handle, :selector]) - |> validate_required([:name, :handle, :selector]) - |> validate_format(:handle, ~r/^[a-z][a-z\d\-]*$/, - message: - "should start with a lower case ASCII letter and only contain lower case ASCII letters, digits and -" - ) - |> validate_change(:selector, &validate_selector/2) - |> unique_constraint([:name, :tenant_id]) - |> unique_constraint([:handle, :tenant_id]) - end - - defp validate_selector(field, selector) do - case Selector.to_ecto_query(selector) do - {:ok, _ecto_query} -> - [] - - {:error, %Selector.Parser.Error{message: message}} -> - msg = "failed to be parsed with error: " <> message - [{field, msg}] - end - end -end diff --git a/backend/lib/edgehog/groups/device_group/calculations/filter.ex b/backend/lib/edgehog/groups/device_group/calculations/filter.ex new file mode 100644 index 000000000..92df21df1 --- /dev/null +++ b/backend/lib/edgehog/groups/device_group/calculations/filter.ex @@ -0,0 +1,48 @@ +# +# This file is part of Edgehog. +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule Edgehog.Groups.DeviceGroup.Calculations.Filter do + use Ash.Calculation + + alias Edgehog.Selector + + @impl true + def load(_query, _opts, _context) do + [:selector] + end + + @impl true + def calculate(groups, _opts, _context) do + Enum.map(groups, fn group -> + case Selector.to_ash_expr(group.selector) do + {:ok, result} -> + result + + # We validate this during creation, so we raise here if we fail + {:error, error} -> + raise """ + Cannot parse selector: #{group.selector} + + Error: #{inspect(error)} + """ + end + end) + end +end diff --git a/backend/lib/edgehog/groups/device_group/device_group.ex b/backend/lib/edgehog/groups/device_group/device_group.ex new file mode 100644 index 000000000..a227d72af --- /dev/null +++ b/backend/lib/edgehog/groups/device_group/device_group.ex @@ -0,0 +1,138 @@ +# +# This file is part of Edgehog. +# +# Copyright 2022-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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule Edgehog.Groups.DeviceGroup do + use Edgehog.MultitenantResource, + api: Edgehog.Groups, + extensions: [ + AshGraphql.Resource + ] + + alias Edgehog.Groups.DeviceGroup.{Calculations, ManualRelationships, Validations} + + graphql do + type :device_group + + queries do + get :device_group, :get + list :device_groups, :list + end + + mutations do + update :update_device_group, :update + destroy :delete_device_group, :destroy + end + end + + actions do + create :create do + description "Creates a new device group." + primary? true + + accept [:name, :handle, :selector] + end + + read :get do + description "Returns a single device group." + get? true + end + + read :list do + description "Returns the list of all device groups." + primary? true + end + + update :update do + description "Updates a device group." + primary? true + + accept [:name, :handle, :selector] + end + + destroy :destroy do + description "Deletes a device group." + primary? true + end + end + + attributes do + integer_primary_key :id + + attribute :name, :string do + description "The display name of the device group." + allow_nil? false + end + + attribute :handle, :string do + description """ + The identifier of the device group. + + It should start with a lower case ASCII letter and only contain \ + lower case ASCII letters, digits and the hyphen - symbol. + """ + + allow_nil? false + end + + # TODO: custom type here + attribute :selector, :string do + description """ + The Selector that will determine which devices belong to the device group. + + This must be a valid selector expression, consult the Selector section \ + of the Edgehog documentation for more information about Selectors. + """ + + allow_nil? false + end + + create_timestamp :inserted_at + update_timestamp :updated_at + end + + relationships do + has_many :devices, Edgehog.Devices.Device do + description "The devices belonging to the group." + api Edgehog.Devices + manual ManualRelationships.Devices + end + end + + identities do + # These have to be named this way to match the existing unique indexes + # we already have. Ash uses identities to add a `unique_constraint` to the + # Ecto changeset, so names have to match. There's no need to explicitly add + # :tenant_id in the fields because identity in a multitenant resource are + # automatically scoped to a specific :tenant_id + # TODO: change index names when we generate migrations at the end of the porting + identity :name_tenant_id, [:name] + identity :handle_tenant_id, [:handle] + end + + validations do + validate Edgehog.Validations.slug(:handle) + validate Validations.Selector + end + + postgres do + table "device_groups" + repo Edgehog.Repo + end +end diff --git a/backend/lib/edgehog/groups/device_group/manual_relationships/devices.ex b/backend/lib/edgehog/groups/device_group/manual_relationships/devices.ex new file mode 100644 index 000000000..577943071 --- /dev/null +++ b/backend/lib/edgehog/groups/device_group/manual_relationships/devices.ex @@ -0,0 +1,53 @@ +# +# This file is part of Edgehog. +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule Edgehog.Groups.DeviceGroup.ManualRelationships.Devices do + use Ash.Resource.ManualRelationship + + alias Edgehog.Selector + require Ash.Query + + @impl true + def select(_opts) do + [:selector] + end + + @impl true + def load(groups, _opts, %{query: query}) do + # We're doing N+1 queries here, but it's probably inevitable at this point + group_id_to_devices = + groups + |> Enum.map(fn group -> + {:ok, ast_root} = Selector.parse(group.selector) + + filter = Selector.to_ash_expr(ast_root) + + devices = + query + |> Ash.Query.filter(^filter) + |> Ash.read!() + + {group.id, devices} + end) + |> Map.new() + + {:ok, group_id_to_devices} + end +end diff --git a/backend/lib/edgehog/groups/device_group/validations/selector.ex b/backend/lib/edgehog/groups/device_group/validations/selector.ex new file mode 100644 index 000000000..44302bd78 --- /dev/null +++ b/backend/lib/edgehog/groups/device_group/validations/selector.ex @@ -0,0 +1,42 @@ +# +# This file is part of Edgehog. +# +# 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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule Edgehog.Groups.DeviceGroup.Validations.Selector do + use Ash.Resource.Validation + + alias Edgehog.Selector + + @impl true + def validate(changeset, _opts) do + case Ash.Changeset.fetch_change(changeset, :selector) do + {:ok, selector} when is_binary(selector) -> + case Selector.parse(selector) do + {:ok, _ash_expr} -> + :ok + + {:error, %Selector.Parser.Error{message: message}} -> + {:error, field: :selector, message: "failed to be parsed with error: " <> message} + end + + _ -> + :ok + end + end +end diff --git a/backend/lib/edgehog/groups/groups.ex b/backend/lib/edgehog/groups/groups.ex new file mode 100644 index 000000000..b30cc5b25 --- /dev/null +++ b/backend/lib/edgehog/groups/groups.ex @@ -0,0 +1,35 @@ +# +# This file is part of Edgehog. +# +# Copyright 2022-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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule Edgehog.Groups do + @moduledoc """ + The Groups context. + """ + + use Ash.Api, extensions: [AshGraphql.Api] + + graphql do + root_level_errors? true + end + + resources do + resource Edgehog.Groups.DeviceGroup + end +end diff --git a/backend/lib/edgehog_web/schema.ex b/backend/lib/edgehog_web/schema.ex index 18bdf5720..585371f3d 100644 --- a/backend/lib/edgehog_web/schema.ex +++ b/backend/lib/edgehog_web/schema.ex @@ -24,7 +24,6 @@ defmodule EdgehogWeb.Schema do import_types EdgehogWeb.Schema.AstarteTypes import_types EdgehogWeb.Schema.BaseImagesTypes import_types EdgehogWeb.Schema.GeolocationTypes - import_types EdgehogWeb.Schema.GroupsTypes import_types EdgehogWeb.Schema.LocalizationTypes import_types EdgehogWeb.Schema.OSManagementTypes import_types EdgehogWeb.Schema.UpdateCampaignsTypes @@ -32,7 +31,12 @@ defmodule EdgehogWeb.Schema do import_types Absinthe.Plug.Types import_types Absinthe.Type.Custom - @apis [Edgehog.Devices, Edgehog.Labeling, Edgehog.Tenants] + @apis [ + Edgehog.Devices, + Edgehog.Groups, + Edgehog.Labeling, + Edgehog.Tenants + ] # TODO: remove define_relay_types?: false once we convert everything to Ash use AshGraphql, @@ -110,14 +114,12 @@ defmodule EdgehogWeb.Schema do end import_fields :base_images_queries - import_fields :groups_queries import_fields :update_campaigns_queries end mutation do import_fields :astarte_mutations import_fields :base_images_mutations - import_fields :groups_mutations import_fields :os_management_mutations import_fields :update_campaigns_mutations end diff --git a/backend/test/edgehog/groups_test.exs b/backend/test/edgehog/groups_test.exs deleted file mode 100644 index a4cffe3bf..000000000 --- a/backend/test/edgehog/groups_test.exs +++ /dev/null @@ -1,261 +0,0 @@ -# -# This file is part of Edgehog. -# -# Copyright 2022 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. -# -# SPDX-License-Identifier: Apache-2.0 -# - -defmodule Edgehog.GroupsTest do - use Edgehog.DataCase, async: true - - alias Edgehog.Groups - - describe "device_groups" do - alias Edgehog.Groups.DeviceGroup - alias Edgehog.Devices - - import Edgehog.AstarteFixtures - import Edgehog.DevicesFixtures - import Edgehog.GroupsFixtures - - @invalid_attrs %{handle: nil, name: nil, selector: nil} - - test "list_device_groups/0 returns all device_groups" do - device_group = device_group_fixture() - assert Groups.list_device_groups() == [device_group] - end - - test "list_devices_in_group/0 returns empty list with no devices" do - device_group = device_group_fixture() - assert Groups.list_devices_in_group(device_group) == [] - end - - test "list_devices_in_group/0 returns devices matching the group selector" do - device_group = device_group_fixture(selector: ~s<"foo" in tags>) - - realm = - cluster_fixture() - |> realm_fixture() - - {:ok, device_1} = - device_fixture(realm) - |> Devices.update_device(%{tags: ["foo", "baz"]}) - - {:ok, _device_2} = - device_fixture(realm, name: "Device 2", device_id: "9FXwmtRtRuqC48DEOjOj7Q") - |> Devices.update_device(%{tags: ["bar"]}) - - assert Groups.list_devices_in_group(device_group) == [device_1] - end - - test "get_groups_for_device_ids/0 returns a device id -> groups map" do - device_group_foo = - device_group_fixture(name: "Foo", handle: "foo", selector: ~s<"foo" in tags>) - - device_group_baz = - device_group_fixture(name: "Baz", handle: "baz", selector: ~s<"baz" in tags>) - - device_group_bar = - device_group_fixture(name: "Bar", handle: "bar", selector: ~s<"bar" in tags>) - - realm = - cluster_fixture() - |> realm_fixture() - - {:ok, device_1} = - device_fixture(realm) - |> Devices.update_device(%{tags: ["foo", "baz"]}) - - {:ok, device_2} = - device_fixture(realm, name: "Device 2", device_id: "9FXwmtRtRuqC48DEOjOj7Q") - |> Devices.update_device(%{tags: ["baz"]}) - - {:ok, device_3} = - device_fixture(realm, name: "Device 3", device_id: "FMFTT25iQ7eod3KlojoFMg") - |> Devices.update_device(%{tags: ["bar"]}) - - {:ok, device_4} = - device_fixture(realm, name: "Device 4", device_id: "SSshD9aaQWa2ce0Ic327qw") - |> Devices.update_device(%{tags: ["fizz"]}) - - device_ids = [ - device_1.id, - device_2.id, - device_3.id, - device_4.id - ] - - result = Groups.get_groups_for_device_ids(device_ids) - - assert Map.get(result, device_1.id) |> length() == 2 - - device_1_groups = Map.get(result, device_1.id) - - assert device_group_foo in device_1_groups - assert device_group_baz in device_1_groups - - assert [device_group_baz] == Map.get(result, device_2.id) - assert [device_group_bar] == Map.get(result, device_3.id) - assert [] == Map.get(result, device_4.id) - end - - test "get_groups_for_device_ids/1 ignores devices that are not requested" do - device_group_foo = - device_group_fixture(name: "Foo", handle: "foo", selector: ~s<"foo" in tags>) - - realm = - cluster_fixture() - |> realm_fixture() - - {:ok, device_1} = - device_fixture(realm) - |> Devices.update_device(%{tags: ["foo", "baz"]}) - - {:ok, device_2} = - device_fixture(realm, name: "Device 2", device_id: "9FXwmtRtRuqC48DEOjOj7Q") - |> Devices.update_device(%{tags: ["foo", "baz"]}) - - device_ids = [device_1.id] - - result = Groups.get_groups_for_device_ids(device_ids) - - assert [device_group_foo] == Map.get(result, device_1.id) - refute Map.has_key?(result, device_2.id) - end - - test "get_groups_for_device_ids/0 reuses the same groups in the result map, without copying them" do - _device_group_foo = - device_group_fixture(name: "Foo", handle: "foo", selector: ~s<"foo" in tags>) - - realm = - cluster_fixture() - |> realm_fixture() - - {:ok, device_1} = - device_fixture(realm) - |> Devices.update_device(%{tags: ["foo"]}) - - {:ok, device_2} = - device_fixture(realm, name: "Device 2", device_id: "9FXwmtRtRuqC48DEOjOj7Q") - |> Devices.update_device(%{tags: ["foo"]}) - - device_ids = [ - device_1.id, - device_2.id - ] - - result = Groups.get_groups_for_device_ids(device_ids) - - [device_group_foo_d1] = Map.get(result, device_1.id) - [device_group_foo_d2] = Map.get(result, device_2.id) - - assert :erts_debug.same(device_group_foo_d1, device_group_foo_d2) - end - - test "fetch_device_group/1 returns the device_group with given id" do - device_group = device_group_fixture() - assert Groups.fetch_device_group(device_group.id) == {:ok, device_group} - end - - test "fetch_device_group/1 returns {:error, :not_found} for unexisting device group" do - assert Groups.fetch_device_group(12_421) == {:error, :not_found} - end - - test "create_device_group/1 with valid data creates a device_group" do - valid_attrs = %{handle: "test-devices", name: "Test Devices", selector: ~s<"test" in tags>} - - assert {:ok, %DeviceGroup{} = device_group} = Groups.create_device_group(valid_attrs) - assert device_group.handle == "test-devices" - assert device_group.name == "Test Devices" - assert device_group.selector == ~s<"test" in tags> - end - - test "create_device_group/1 with empty data returns error changeset" do - assert {:error, %Ecto.Changeset{}} = Groups.create_device_group(@invalid_attrs) - end - - test "create_device_group/1 with invalid handle returns error changeset" do - attrs = %{handle: "invalid handle", name: "Test Devices", selector: ~s<"test" in tags>} - - assert {:error, %Ecto.Changeset{}} = Groups.create_device_group(attrs) - end - - test "create_device_group/1 with invalid selector returns error changeset" do - attrs = %{handle: "test-devices", name: "Test Devices", selector: "invalid selector"} - - assert {:error, %Ecto.Changeset{}} = Groups.create_device_group(attrs) - end - - test "update_device_group/2 with valid data updates the device_group" do - device_group = device_group_fixture() - - update_attrs = %{ - handle: "updated-test-devices", - name: "Updated Test Devices", - selector: ~s<"test" in tags and attributes["custom:is_updated"] == true> - } - - assert {:ok, %DeviceGroup{} = device_group} = - Groups.update_device_group(device_group, update_attrs) - - assert device_group.handle == "updated-test-devices" - assert device_group.name == "Updated Test Devices" - - assert device_group.selector == - ~s<"test" in tags and attributes["custom:is_updated"] == true> - end - - test "update_device_group/2 with empty data returns error changeset" do - device_group = device_group_fixture() - - assert {:error, %Ecto.Changeset{}} = - Groups.update_device_group(device_group, @invalid_attrs) - - assert {:ok, device_group} == Groups.fetch_device_group(device_group.id) - end - - test "update_device_group/1 with invalid handle returns error changeset" do - device_group = device_group_fixture() - - attrs = %{handle: "invalid updated handle"} - - assert {:error, %Ecto.Changeset{}} = Groups.update_device_group(device_group, attrs) - - assert {:ok, device_group} == Groups.fetch_device_group(device_group.id) - end - - test "update_device_group/1 with invalid selector returns error changeset" do - device_group = device_group_fixture() - - attrs = %{selector: "invalid updated selector"} - - assert {:error, %Ecto.Changeset{}} = Groups.update_device_group(device_group, attrs) - - assert {:ok, device_group} == Groups.fetch_device_group(device_group.id) - end - - test "delete_device_group/1 deletes the device_group" do - device_group = device_group_fixture() - assert {:ok, %DeviceGroup{}} = Groups.delete_device_group(device_group) - assert {:error, :not_found} == Groups.fetch_device_group(device_group.id) - end - - test "change_device_group/1 returns a device_group changeset" do - device_group = device_group_fixture() - assert %Ecto.Changeset{} = Groups.change_device_group(device_group) - end - end -end diff --git a/backend/test/edgehog_web/schema/mutation/delete_device_group_test.exs b/backend/test/edgehog_web/schema/mutation/delete_device_group_test.exs index fcbce92f6..9ef77fda8 100644 --- a/backend/test/edgehog_web/schema/mutation/delete_device_group_test.exs +++ b/backend/test/edgehog_web/schema/mutation/delete_device_group_test.exs @@ -1,7 +1,7 @@ # # This file is part of Edgehog. # -# Copyright 2022 SECO Mind Srl +# Copyright 2022-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. @@ -19,22 +19,52 @@ # defmodule EdgehogWeb.Schema.Mutation.DeleteDeviceGroupTest do - use EdgehogWeb.ConnCase, async: true + use EdgehogWeb.GraphqlCase, async: true + + @moduletag :ported_to_ash import Edgehog.GroupsFixtures - alias Edgehog.Groups alias Edgehog.Groups.DeviceGroup - describe "deleteDeviceGroup field" do - setup do - {:ok, device_group: device_group_fixture()} + @moduletag :ported_to_ash + + describe "deleteDeviceGroup query" do + setup %{tenant: tenant} do + device_group = + device_group_fixture(tenant: tenant) + + id = AshGraphql.Resource.encode_relay_id(device_group) + + %{device_group: device_group, id: id} + end + + test "deletes a device group", %{tenant: tenant, id: id, device_group: fixture} do + device_group = + delete_device_group_mutation(tenant: tenant, id: id) + |> extract_result!() + + assert device_group["handle"] == fixture.handle + + refute DeviceGroup + |> Ash.Query.for_read(:get, %{id: fixture.id}, tenant: tenant) + |> Ash.exists?() end - @query """ - mutation DeleteDeviceGroup($input: DeleteDeviceGroupInput!) { - deleteDeviceGroup(input: $input) { - deviceGroup { + test "fails with non-existing id", %{tenant: tenant} do + id = non_existing_device_group_id(tenant) + + result = delete_device_group_mutation(tenant: tenant, id: id) + + assert %{fields: [:id], message: "could not be found"} = extract_error!(result) + end + end + + defp delete_device_group_mutation(opts) do + default_document = """ + mutation DeleteDeviceGroup($id: ID!) { + deleteDeviceGroup(id: $id) { + result { id name handle @@ -43,51 +73,48 @@ defmodule EdgehogWeb.Schema.Mutation.DeleteDeviceGroupTest do } } """ - test "deletes device group", %{ - conn: conn, - api_path: api_path, - device_group: device_group - } do - %DeviceGroup{name: name, handle: handle, selector: selector} = device_group - id = Absinthe.Relay.Node.to_global_id(:device_group, device_group.id, EdgehogWeb.Schema) - - variables = %{ - input: %{ - device_group_id: id - } - } - conn = post(conn, api_path, query: @query, variables: variables) - - assert %{ - "data" => %{ - "deleteDeviceGroup" => %{ - "deviceGroup" => %{ - "id" => ^id, - "name" => ^name, - "handle" => ^handle, - "selector" => ^selector - } - } - } - } = assert(json_response(conn, 200)) + {tenant, opts} = Keyword.pop!(opts, :tenant) + {id, opts} = Keyword.pop!(opts, :id) - assert {:error, :not_found} = Groups.fetch_device_group(device_group.id) - end + document = Keyword.get(opts, :document, default_document) + variables = %{"id" => id} + context = %{tenant: tenant} - test "fails with non existing id", %{conn: conn, api_path: api_path} do - id = Absinthe.Relay.Node.to_global_id(:device_group, 1_234_539, EdgehogWeb.Schema) + Absinthe.run!(document, EdgehogWeb.Schema, variables: variables, context: context) + end - variables = %{ - input: %{ - device_group_id: id - } - } + defp extract_error!(result) do + assert %{ + data: %{"deleteDeviceGroup" => nil}, + errors: [error] + } = result - conn = post(conn, api_path, query: @query, variables: variables) + error + end - assert %{"errors" => [%{"code" => "not_found", "status_code" => 404}]} = - assert(json_response(conn, 200)) - end + defp extract_result!(result) do + refute :errors in Map.keys(result) + refute "errors" in Map.keys(result[:data]) + + assert %{ + data: %{ + "deleteDeviceGroup" => %{ + "result" => device_group + } + } + } = result + + assert device_group != nil + + device_group + end + + defp non_existing_device_group_id(tenant) do + fixture = device_group_fixture(tenant: tenant) + id = AshGraphql.Resource.encode_relay_id(fixture) + :ok = Ash.destroy!(fixture) + + id end end diff --git a/backend/test/edgehog_web/schema/mutation/update_device_group_test.exs b/backend/test/edgehog_web/schema/mutation/update_device_group_test.exs index 107bceeb9..720affe18 100644 --- a/backend/test/edgehog_web/schema/mutation/update_device_group_test.exs +++ b/backend/test/edgehog_web/schema/mutation/update_device_group_test.exs @@ -1,7 +1,7 @@ # # This file is part of Edgehog. # -# Copyright 2022 SECO Mind Srl +# Copyright 2022-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. @@ -19,136 +19,200 @@ # defmodule EdgehogWeb.Schema.Mutation.UpdateDeviceGroupTest do - use EdgehogWeb.ConnCase, async: true + use EdgehogWeb.GraphqlCase, async: true + + @moduletag :ported_to_ash import Edgehog.GroupsFixtures alias Edgehog.Groups.DeviceGroup - describe "updateDeviceGroup field" do - setup do - {:ok, device_group: device_group_fixture()} + describe "updateDeviceGroup query" do + setup %{tenant: tenant} do + device_group = device_group_fixture(tenant: tenant) + id = AshGraphql.Resource.encode_relay_id(device_group) + + {:ok, device_group: device_group, id: id} end - @query """ - mutation UpdateDeviceGroup($input: UpdateDeviceGroupInput!) { - updateDeviceGroup(input: $input) { - deviceGroup { - id - name - handle - selector - } - } - } - """ - test "updates device group with valid data", %{ - conn: conn, - api_path: api_path, - device_group: device_group + test "successfully updates with valid data", %{ + tenant: tenant, + device_group: device_group, + id: id } do - name = "Updated" - handle = "updated" - selector = ~s<"updated" in tags> - - id = Absinthe.Relay.Node.to_global_id(:device_group, device_group.id, EdgehogWeb.Schema) - - variables = %{ - input: %{ - device_group_id: id, - name: name, - handle: handle, - selector: selector - } - } + result = + update_device_group_mutation( + tenant: tenant, + id: id, + name: "Updated Name", + handle: "updatedhandle", + selector: ~s<"updated" in tags> + ) - conn = post(conn, api_path, query: @query, variables: variables) + device_group = extract_result!(result) assert %{ - "data" => %{ - "updateDeviceGroup" => %{ - "deviceGroup" => %{ - "id" => ^id, - "name" => ^name, - "handle" => ^handle, - "selector" => ^selector - } - } - } - } = assert(json_response(conn, 200)) + "id" => _id, + "name" => "Updated Name", + "handle" => "updatedhandle", + "selector" => ~s<"updated" in tags> + } = device_group end - test "updates device group with partial data", %{ - conn: conn, - api_path: api_path, - device_group: device_group - } do - %DeviceGroup{name: initial_name, handle: initial_handle} = device_group + test "supports partial updates", %{tenant: tenant, device_group: device_group, id: id} do + %{handle: old_handle, selector: old_selector} = device_group - selector = ~s<"updated" in tags> + result = + update_device_group_mutation( + tenant: tenant, + id: id, + name: "Only Name Update" + ) - id = Absinthe.Relay.Node.to_global_id(:device_group, device_group.id, EdgehogWeb.Schema) + device_group = extract_result!(result) - variables = %{ - input: %{ - device_group_id: id, - selector: selector - } - } + assert %{ + "name" => "Only Name Update", + "handle" => ^old_handle, + "selector" => ^old_selector + } = device_group + end - conn = post(conn, api_path, query: @query, variables: variables) + test "returns error for invalid handle", %{tenant: tenant, id: id} do + result = + update_device_group_mutation( + tenant: tenant, + id: id, + handle: "123Invalid$" + ) - assert %{ - "data" => %{ - "updateDeviceGroup" => %{ - "deviceGroup" => %{ - "id" => ^id, - "name" => ^initial_name, - "handle" => ^initial_handle, - "selector" => ^selector - } - } - } - } = assert(json_response(conn, 200)) + assert %{fields: [:handle], message: "should only contain" <> _} = extract_error!(result) end - test "fails with invalid data", %{conn: conn, api_path: api_path, device_group: device_group} do - id = Absinthe.Relay.Node.to_global_id(:device_group, device_group.id, EdgehogWeb.Schema) + test "returns error for invalid selector", %{tenant: tenant, id: id} do + result = + update_device_group_mutation( + tenant: tenant, + id: id, + selector: "foobaz" + ) - variables = %{ - input: %{ - device_group_id: id, - name: nil, - handle: nil, - selector: "invalid" - } - } + assert %{fields: [:selector], message: "failed to be parsed" <> _} = extract_error!(result) + end + + test "returns error for duplicate name", %{ + tenant: tenant, + device_group: device_group, + id: id + } do + fixture = device_group_fixture(tenant: tenant) - conn = post(conn, api_path, query: @query, variables: variables) + result = + update_device_group_mutation( + tenant: tenant, + id: id, + name: fixture.name + ) - assert %{"errors" => _} = assert(json_response(conn, 200)) + assert %{fields: [:name], message: "has already been taken"} = extract_error!(result) end - test "fails with non existing id", %{conn: conn, api_path: api_path} do - name = "Updated" - handle = "updated" - selector = ~s<"updated" in tags> + test "returns error for duplicate handle", %{ + tenant: tenant, + device_group: device_group, + id: id + } do + fixture = device_group_fixture(tenant: tenant) + + result = + update_device_group_mutation( + tenant: tenant, + id: id, + handle: fixture.handle + ) - id = Absinthe.Relay.Node.to_global_id(:device_group, 1_234_539, EdgehogWeb.Schema) + assert %{fields: [:handle], message: "has already been taken"} = + extract_error!(result) + end - variables = %{ - input: %{ - device_group_id: id, - name: name, - handle: handle, - selector: selector + test "returns error for non-existing device group", %{tenant: tenant} do + id = non_existing_device_group_id(tenant) + + result = + update_device_group_mutation( + tenant: tenant, + id: id, + name: "Updated" + ) + + assert %{fields: [:id], message: "could not be found"} = extract_error!(result) + end + end + + defp update_device_group_mutation(opts) do + default_document = """ + mutation UpdateDeviceGroup($id: ID!, $input: UpdateDeviceGroupInput!) { + updateDeviceGroup(id: $id, input: $input) { + result { + id + name + handle + selector } } + } + """ - conn = post(conn, api_path, query: @query, variables: variables) + {tenant, opts} = Keyword.pop!(opts, :tenant) + {id, opts} = Keyword.pop!(opts, :id) - assert %{"errors" => [%{"code" => "not_found", "status_code" => 404}]} = - assert(json_response(conn, 200)) - end + input = + %{ + "name" => opts[:name], + "handle" => opts[:handle], + "selector" => opts[:selector] + } + |> Enum.filter(fn {_k, v} -> v != nil end) + |> Enum.into(%{}) + + variables = %{"id" => id, "input" => input} + document = Keyword.get(opts, :document, default_document) + context = %{tenant: tenant} + + Absinthe.run!(document, EdgehogWeb.Schema, variables: variables, context: context) + end + + defp extract_error!(result) do + assert %{ + data: %{"updateDeviceGroup" => nil}, + errors: [error] + } = result + + error + end + + defp extract_result!(result) do + refute :errors in Map.keys(result) + refute "errors" in Map.keys(result[:data]) + + assert %{ + data: %{ + "updateDeviceGroup" => %{ + "result" => device_group + } + } + } = result + + assert device_group != nil + + device_group + end + + defp non_existing_device_group_id(tenant) do + fixture = device_group_fixture(tenant: tenant) + id = AshGraphql.Resource.encode_relay_id(fixture) + :ok = Ash.destroy!(fixture) + + id end end diff --git a/backend/test/edgehog_web/schema/query/device_group_test.exs b/backend/test/edgehog_web/schema/query/device_group_test.exs index da6cd2c4b..a7e53945c 100644 --- a/backend/test/edgehog_web/schema/query/device_group_test.exs +++ b/backend/test/edgehog_web/schema/query/device_group_test.exs @@ -1,7 +1,7 @@ # # This file is part of Edgehog. # -# Copyright 2022 SECO Mind Srl +# Copyright 2022-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. @@ -19,126 +19,112 @@ # defmodule EdgehogWeb.Schema.Query.DeviceGroupTest do - use EdgehogWeb.ConnCase, async: true + use EdgehogWeb.GraphqlCase, async: true + + @moduletag :ported_to_ash - import Edgehog.AstarteFixtures import Edgehog.DevicesFixtures import Edgehog.GroupsFixtures - import Edgehog.UpdateCampaignsFixtures - alias Edgehog.Devices alias Edgehog.Groups.DeviceGroup - describe "deviceGroup field" do - setup do - cluster = cluster_fixture() - realm = realm_fixture(cluster) + describe "deviceGroup query" do + test "returns nil for non existing device group", %{tenant: tenant} do + id = non_existing_device_group_id(tenant) + result = device_group_query(tenant: tenant, id: id) + assert %{data: %{"deviceGroup" => nil}} = result + end + + test "returns device group if it's present", %{tenant: tenant} do + fixture = device_group_fixture(tenant: tenant) - {:ok, device_foo} = - device_fixture(realm) - |> Devices.update_device(%{tags: ["foo"]}) + id = AshGraphql.Resource.encode_relay_id(fixture) - {:ok, device_bar} = - device_fixture(realm, name: "Device Bar", device_id: "eTezXt3hST2bPEw0Inq56A") - |> Devices.update_device(%{tags: ["bar"]}) + result = + device_group_query(tenant: tenant, id: id) + |> extract_result!() - {:ok, realm: realm, device_foo: device_foo, device_bar: device_bar} + assert result["name"] == fixture.name + assert result["handle"] == fixture.handle + assert result["selector"] == fixture.selector end - @query """ - query ($id: ID!) { - deviceGroup(id: $id) { - name - handle - selector - devices { - id - name - deviceId + test "returns only devices that match the selector", %{tenant: tenant} do + selector = ~s<("foo" in tags and "bar" not in tags) or "baz" in tags> + + fixture = device_group_fixture(tenant: tenant, selector: selector) + + id = AshGraphql.Resource.encode_relay_id(fixture) + + foo_device = + device_fixture(tenant: tenant) + |> add_tags(["foo"]) + + foo_bar_device = + device_fixture(tenant: tenant) + |> add_tags(["foo", "bar"]) + + baz_device = + device_fixture(tenant: tenant) + |> add_tags(["baz"]) + + document = """ + query ($id: ID!) { + deviceGroup(id: $id) { + devices { + id + } } } - } - """ - test "returns not found for unexisting device group", %{conn: conn, api_path: api_path} do - variables = %{ - id: Absinthe.Relay.Node.to_global_id(:device_group, 303_040, EdgehogWeb.Schema) - } + """ - conn = get(conn, api_path, query: @query, variables: variables) + assert %{"devices" => devices} = + device_group_query(tenant: tenant, id: id, document: document) + |> extract_result!() - assert %{ - "data" => %{ - "deviceGroup" => nil - }, - "errors" => [ - %{"path" => ["deviceGroup"], "status_code" => 404, "code" => "not_found"} - ] - } = json_response(conn, 200) - end + assert length(devices) == 2 + device_ids = Enum.map(devices, &Map.get(&1, "id")) - test "returns device group if it's present", %{ - conn: conn, - api_path: api_path, - device_foo: device_foo - } do - %DeviceGroup{id: dg_id} = - device_group_fixture(name: "Foos", handle: "foos", selector: ~s<"foo" in tags>) - - variables = %{id: Absinthe.Relay.Node.to_global_id(:device_group, dg_id, EdgehogWeb.Schema)} - conn = get(conn, api_path, query: @query, variables: variables) - - assert %{ - "data" => %{ - "deviceGroup" => device_group - } - } = json_response(conn, 200) - - assert device_group["name"] == "Foos" - assert device_group["handle"] == "foos" - assert device_group["selector"] == ~s<"foo" in tags> - assert [device] = device_group["devices"] - assert device["name"] == device_foo.name - assert device["deviceId"] == device_foo.device_id - - assert {:ok, %{id: d_id, type: :device}} = - Absinthe.Relay.Node.from_global_id(device["id"], EdgehogWeb.Schema) - - assert d_id == to_string(device_foo.id) + assert AshGraphql.Resource.encode_relay_id(foo_device) in device_ids + assert AshGraphql.Resource.encode_relay_id(baz_device) in device_ids + refute AshGraphql.Resource.encode_relay_id(foo_bar_device) in device_ids end + end + + defp non_existing_device_group_id(tenant) do + fixture = device_group_fixture(tenant: tenant) + id = AshGraphql.Resource.encode_relay_id(fixture) + :ok = Ash.destroy!(fixture) + + id + end - @update_channel_query """ + defp device_group_query(opts) do + default_document = """ query ($id: ID!) { deviceGroup(id: $id) { - updateChannel { - id - handle - name - } + name + handle + selector } } """ - test "allows querying updateChannel if present", %{conn: conn, api_path: api_path} do - %DeviceGroup{id: dg_id} = device_group_fixture() - update_channel = update_channel_fixture(target_group_ids: [dg_id]) - - variables = %{id: Absinthe.Relay.Node.to_global_id(:device_group, dg_id, EdgehogWeb.Schema)} - conn = get(conn, api_path, query: @update_channel_query, variables: variables) - - assert %{ - "data" => %{ - "deviceGroup" => device_group - } - } = json_response(conn, 200) - - assert device_group["updateChannel"]["id"] == - Absinthe.Relay.Node.to_global_id( - :update_channel, - update_channel.id, - EdgehogWeb.Schema - ) - - assert device_group["updateChannel"]["handle"] == update_channel.handle - assert device_group["updateChannel"]["name"] == update_channel.name - end + + tenant = Keyword.fetch!(opts, :tenant) + id = Keyword.fetch!(opts, :id) + + variables = %{"id" => id} + + document = Keyword.get(opts, :document, default_document) + + Absinthe.run!(document, EdgehogWeb.Schema, variables: variables, context: %{tenant: tenant}) + end + + defp extract_result!(result) do + refute :errors in Map.keys(result) + assert %{data: %{"deviceGroup" => device_group}} = result + assert device_group != nil + + device_group end end diff --git a/backend/test/edgehog_web/schema/query/device_groups_test.exs b/backend/test/edgehog_web/schema/query/device_groups_test.exs index df8c9823d..867253bea 100644 --- a/backend/test/edgehog_web/schema/query/device_groups_test.exs +++ b/backend/test/edgehog_web/schema/query/device_groups_test.exs @@ -1,7 +1,7 @@ # # This file is part of Edgehog. # -# Copyright 2022 SECO Mind Srl +# Copyright 2022-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. @@ -19,88 +19,120 @@ # defmodule EdgehogWeb.Schema.Query.DeviceGroupsTest do - use EdgehogWeb.ConnCase, async: true + use EdgehogWeb.GraphqlCase, async: true + + @moduletag :ported_to_ash - import Edgehog.AstarteFixtures import Edgehog.DevicesFixtures import Edgehog.GroupsFixtures - alias Edgehog.Devices alias Edgehog.Groups.DeviceGroup - describe "deviceGroups field" do - setup do - cluster = cluster_fixture() - realm = realm_fixture(cluster) + describe "deviceGroups query" do + test "returns empty device groups", %{tenant: tenant} do + assert [] == device_groups_query(tenant: tenant) |> extract_result!() + end + + test "returns device groups if present", %{tenant: tenant} do + fixture = device_group_fixture(tenant: tenant) + + id = AshGraphql.Resource.encode_relay_id(fixture) + + [result] = + device_groups_query(tenant: tenant, id: id) + |> extract_result!() + + assert result["id"] == id + assert result["name"] == fixture.name + assert result["handle"] == fixture.handle + assert result["selector"] == fixture.selector + end + + test "returns only devices that match the selector", %{tenant: tenant} do + foo_group = device_group_fixture(tenant: tenant, name: "foo", selector: ~s<"foo" in tags>) + bar_group = device_group_fixture(tenant: tenant, name: "bar", selector: ~s<"bar" in tags>) + + foo_device = + device_fixture(tenant: tenant) + |> add_tags(["foo"]) + + bar_device = + device_fixture(tenant: tenant) + |> add_tags(["bar"]) + + foo_bar_device = + device_fixture(tenant: tenant) + |> add_tags(["foo", "bar"]) + + baz_device = + device_fixture(tenant: tenant) + |> add_tags(["baz"]) + + document = """ + query { + deviceGroups { + name + devices { + id + } + } + } + """ + + result = + device_groups_query(tenant: tenant, document: document) + |> extract_result!() + + assert length(result) == 2 + + foo_device_ids = + result + |> Enum.find(result, &(&1["name"] == "foo")) + |> Map.fetch!("devices") + |> Enum.map(& &1["id"]) - {:ok, device_foo} = - device_fixture(realm) - |> Devices.update_device(%{tags: ["foo"]}) + assert length(foo_device_ids) == 2 - {:ok, device_bar} = - device_fixture(realm, name: "Device Bar", device_id: "eTezXt3hST2bPEw0Inq56A") - |> Devices.update_device(%{tags: ["bar"]}) + assert AshGraphql.Resource.encode_relay_id(foo_device) in foo_device_ids + assert AshGraphql.Resource.encode_relay_id(foo_bar_device) in foo_device_ids - {:ok, realm: realm, device_foo: device_foo, device_bar: device_bar} + bar_device_ids = + result + |> Enum.find(result, &(&1["name"] == "bar")) + |> Map.fetch!("devices") + |> Enum.map(& &1["id"]) + + assert length(bar_device_ids) == 2 + + assert AshGraphql.Resource.encode_relay_id(bar_device) in bar_device_ids + assert AshGraphql.Resource.encode_relay_id(foo_bar_device) in bar_device_ids end + end - @query """ + defp device_groups_query(opts) do + default_document = """ query { deviceGroups { id name handle selector - devices { - id - name - deviceId - } } } """ - test "returns empty device groups", %{conn: conn, api_path: api_path} do - conn = get(conn, api_path, query: @query) - - assert json_response(conn, 200) == %{ - "data" => %{ - "deviceGroups" => [] - } - } - end - - test "returns device groups if they're present", %{ - conn: conn, - api_path: api_path, - device_foo: device_foo - } do - %DeviceGroup{id: dg_id} = - device_group_fixture(name: "Foos", handle: "foos", selector: ~s<"foo" in tags>) - - conn = get(conn, api_path, query: @query) - assert %{ - "data" => %{ - "deviceGroups" => [device_group] - } - } = json_response(conn, 200) + tenant = Keyword.fetch!(opts, :tenant) - assert {:ok, %{id: id, type: :device_group}} = - Absinthe.Relay.Node.from_global_id(device_group["id"], EdgehogWeb.Schema) + document = Keyword.get(opts, :document, default_document) - assert id == to_string(dg_id) - - assert device_group["name"] == "Foos" - assert device_group["handle"] == "foos" - assert device_group["selector"] == ~s<"foo" in tags> - assert [device] = device_group["devices"] - assert device["name"] == device_foo.name - assert device["deviceId"] == device_foo.device_id + Absinthe.run!(document, EdgehogWeb.Schema, context: %{tenant: tenant}) + end - assert {:ok, %{id: d_id, type: :device}} = - Absinthe.Relay.Node.from_global_id(device["id"], EdgehogWeb.Schema) + defp extract_result!(result) do + assert %{data: %{"deviceGroups" => device_groups}} = result + refute :errors in Map.keys(result) + assert device_groups != nil - assert d_id == to_string(device_foo.id) - end + device_groups end end diff --git a/backend/test/support/fixtures/devices_fixtures.ex b/backend/test/support/fixtures/devices_fixtures.ex index fe6751ae8..6245d818b 100644 --- a/backend/test/support/fixtures/devices_fixtures.ex +++ b/backend/test/support/fixtures/devices_fixtures.ex @@ -145,8 +145,9 @@ defmodule Edgehog.DevicesFixtures do Adds tags to a %Devices.Device{} """ def add_tags(device, tags) do - {:ok, device} = Devices.update_device(device, %{tags: tags}) device + |> Ash.Changeset.for_update(:add_tags, %{tags: tags}) + |> Ash.update!() end # Needed to avoid legacy tests compilation errors diff --git a/backend/test/support/fixtures/groups_fixtures.ex b/backend/test/support/fixtures/groups_fixtures.ex index 74b69a557..9d3a9db93 100644 --- a/backend/test/support/fixtures/groups_fixtures.ex +++ b/backend/test/support/fixtures/groups_fixtures.ex @@ -42,16 +42,18 @@ defmodule Edgehog.GroupsFixtures do @doc """ Generate a device_group. """ - def device_group_fixture(attrs \\ %{}) do - {:ok, device_group} = - attrs - |> Enum.into(%{ + def device_group_fixture(opts \\ []) do + {tenant, opts} = Keyword.pop!(opts, :tenant) + + params = + Enum.into(opts, %{ handle: unique_device_group_handle(), name: unique_device_group_name(), selector: unique_device_group_selector() }) - |> Edgehog.Groups.create_device_group() - device_group + Edgehog.Groups.DeviceGroup + |> Ash.Changeset.for_create(:create, params, tenant: tenant) + |> Ash.create!() end end