diff --git a/lib/diffo/provider/assigner/assigner.ex b/lib/diffo/provider/assigner/assigner.ex index fbc2229..58bebe8 100644 --- a/lib/diffo/provider/assigner/assigner.ex +++ b/lib/diffo/provider/assigner/assigner.ex @@ -13,13 +13,26 @@ defmodule Diffo.Provider.Assigner do alias Diffo.Provider.AssignableCharacteristic alias Diffo.Provider.AssignmentRelationship + @assignable_resource_states [:installing, :operating] + @assignable_service_states [:feasibilityChecked, :reserved, :inactive, :active, :suspended] + + @doc """ + The resource lifecycle states from which an instance may make assignments. + """ + def assignable_resource_states, do: @assignable_resource_states + + @doc """ + The service lifecycle states from which an instance may make assignments. + """ + def assignable_service_states, do: @assignable_service_states + @doc """ Assign a thing using the pool declared via `pools do` on the instance module. The thing name is looked up from the pool declaration. """ def assign(result, changeset, pool_name) when is_struct(result) and is_struct(changeset, Ash.Changeset) and is_atom(pool_name) do - with :ok <- check_lifecycle(result) do + with :ok <- assignable_state?(result) do case result.__struct__.pool(pool_name) do nil -> {:error, "pool #{pool_name} not declared on #{result.__struct__}"} pool -> assign(result, changeset, pool_name, pool.thing) @@ -61,17 +74,23 @@ defmodule Diffo.Provider.Assigner do end end - defp check_lifecycle(%{type: :resource, resource_state: state}) when state != :operating, - do: - {:error, "cannot assign: resource lifecycle state is #{inspect(state)}, must be :operating"} - - defp check_lifecycle(%{type: :service, service_state: state}) - when state not in [:active, :inactive], - do: - {:error, - "cannot assign: service state is #{inspect(state)}, must be :active or :inactive"} - - defp check_lifecycle(_), do: :ok + @doc """ + Returns `:ok` if the instance is in a lifecycle state that permits assignment, + otherwise `{:error, reason}`. + """ + def assignable_state?(%{type: :resource, resource_state: state}) + when state not in @assignable_resource_states, + do: + {:error, + "cannot assign: resource lifecycle state is #{inspect(state)}, must be one of #{inspect(@assignable_resource_states)}"} + + def assignable_state?(%{type: :service, service_state: state}) + when state not in @assignable_service_states, + do: + {:error, + "cannot assign: service state is #{inspect(state)}, must be one of #{inspect(@assignable_service_states)}"} + + def assignable_state?(_), do: :ok defp create_assignment(result, pool, thing, value, assignee_id, alias_name) when is_struct(result) and is_atom(pool) and is_atom(thing) and is_integer(value) and diff --git a/test/provider/extension/assigner_test.exs b/test/provider/extension/assigner_test.exs index a0b2f0f..5e662a2 100644 --- a/test/provider/extension/assigner_test.exs +++ b/test/provider/extension/assigner_test.exs @@ -6,6 +6,7 @@ defmodule Diffo.Provider.Extension.AssignerTest do @moduledoc false use ExUnit.Case, async: true @moduletag :domain_extended + alias Diffo.Provider.Assigner alias Diffo.Provider.Specification alias Diffo.Provider.Characteristic alias Diffo.Provider.Assignment @@ -19,6 +20,52 @@ defmodule Diffo.Provider.Extension.AssignerTest do on_exit(&AshNeo4j.Sandbox.rollback/0) end + # Issue #168 — broadened lifecycle policy. Service-side now covers the full + # committed lifecycle (excludes :initial, :cancelled, :terminated); resource + # side now allows :installing in addition to :operating. + describe "assignable_state?/1 (#168)" do + test "resource: :operating is permitted" do + assert :ok = Assigner.assignable_state?(%{type: :resource, resource_state: :operating}) + end + + test "resource: :installing is permitted" do + assert :ok = Assigner.assignable_state?(%{type: :resource, resource_state: :installing}) + end + + test "resource: :planning is rejected" do + assert {:error, msg} = + Assigner.assignable_state?(%{type: :resource, resource_state: :planning}) + + assert msg =~ ":planning" + end + + test "resource: :retiring is rejected" do + assert {:error, _} = + Assigner.assignable_state?(%{type: :resource, resource_state: :retiring}) + end + + test "service: committed lifecycle states are permitted" do + for state <- [:feasibilityChecked, :reserved, :inactive, :active, :suspended] do + assert :ok = Assigner.assignable_state?(%{type: :service, service_state: state}), + "expected service_state #{inspect(state)} to be assignable" + end + end + + test "service: :initial is rejected" do + assert {:error, msg} = + Assigner.assignable_state?(%{type: :service, service_state: :initial}) + + assert msg =~ ":initial" + end + + test "service: terminal states are rejected" do + for state <- [:cancelled, :terminated] do + assert {:error, _} = Assigner.assignable_state?(%{type: :service, service_state: state}), + "expected service_state #{inspect(state)} to be rejected" + end + end + end + describe "build card" do @tag :card test "create a card" do @@ -213,5 +260,45 @@ defmodule Diffo.Provider.Extension.AssignerTest do assert encoding == ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"lifecycleState\":\"operating\"}) end + + test "auto assign port to resource in :installing state (#168)" do + {:ok, assignee} = Parties.build_shelf_with_installer() + + {:ok, card} = Servo.build_card(%{}) + + updates = [ + card: [family: :ISAM, model: "EBLT48", technology: :adsl2Plus], + ports: [first: 1, last: 48, assignable_type: "ADSL2+"] + ] + + {:ok, card} = Servo.define_card(card, %{characteristic_value_updates: updates}) + {:ok, card} = Servo.lifecycle_card(card, %{resource_state: :installing}) + + {:ok, card} = + Servo.assign_port(card, %{ + assignment: %Assignment{assignee_id: assignee.id, operation: :auto_assign} + }) + + assert length(card.assignments) == 1 + end + + test "assign rejected while resource is in :planning state (#168)" do + {:ok, assignee} = Parties.build_shelf_with_installer() + + {:ok, card} = Servo.build_card(%{}) + + updates = [ + card: [family: :ISAM, model: "EBLT48", technology: :adsl2Plus], + ports: [first: 1, last: 48, assignable_type: "ADSL2+"] + ] + + {:ok, card} = Servo.define_card(card, %{characteristic_value_updates: updates}) + {:ok, card} = Servo.lifecycle_card(card, %{resource_state: :planning}) + + assert {:error, _} = + Servo.assign_port(card, %{ + assignment: %Assignment{assignee_id: assignee.id, operation: :auto_assign} + }) + end end end