Skip to content

Commit

Permalink
Port Astarte.Realm resource to Ash
Browse files Browse the repository at this point in the history
Signed-off-by: Riccardo Binetti <[email protected]>
  • Loading branch information
rbino committed Dec 18, 2023
1 parent 38c30a0 commit 6989e1e
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 86 deletions.
78 changes: 55 additions & 23 deletions backend/lib/edgehog/astarte/realm.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#
# This file is part of Edgehog.
#
# Copyright 2021 SECO Mind Srl
# Copyright 2021-2023 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.
Expand All @@ -19,32 +19,64 @@
#

defmodule Edgehog.Astarte.Realm do
use Ecto.Schema
import Ecto.Changeset
import Edgehog.ChangesetValidation
use Edgehog.MultitenantResource,
data_layer: AshPostgres.DataLayer

alias Edgehog.Astarte.Cluster
alias Edgehog.Astarte.Device
alias Edgehog.Validations

schema "realms" do
field :name, :string
field :private_key, :string
field :tenant_id, :id
belongs_to :cluster, Cluster
has_many :devices, Device
code_interface do
define_for Edgehog.Astarte
define :fetch_by_name, action: :by_name, args: [:name]
define :create
define :destroy
end

actions do
defaults [:read, :destroy]

read :by_name do
get_by :name
end

create :create do
primary? true

argument :cluster_id, :integer, allow_nil?: false

change manage_relationship(:cluster_id, :cluster, type: :append)
end
end

attributes do
integer_primary_key :id

attribute :name, :string, allow_nil?: false

attribute :private_key, :string do
allow_nil? false
constraints trim?: false
end

create_timestamp :inserted_at
update_timestamp :updated_at
end

relationships do
belongs_to :cluster, Edgehog.Astarte.Cluster
end

identities do
identity :name_tenant_id, [:name, :tenant_id]
identity :name_cluster_id, [:name, :cluster_id]
end

timestamps()
validations do
validate Validations.realm_name(:name)
validate {Validations.PEMPrivateKey, attribute: :private_key}
end

@doc false
def changeset(realm, attrs) do
realm
|> cast(attrs, [:name, :private_key])
|> validate_required([:name, :private_key])
|> foreign_key_constraint(:cluster_id)
|> unique_constraint([:name, :tenant_id])
|> unique_constraint([:name, :cluster_id])
|> validate_realm_name(:name)
|> validate_pem_private_key(:private_key)
postgres do
table "realms"
repo Edgehog.Repo
end
end
1 change: 1 addition & 0 deletions backend/lib/edgehog/astarte/registry.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ defmodule Edgehog.Astarte.Registry do

entries do
entry Edgehog.Astarte.Cluster
entry Edgehog.Astarte.Realm
end
end
50 changes: 50 additions & 0 deletions backend/lib/edgehog/validations/pem_private_key.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#
# This file is part of Edgehog.
#
# Copyright 2023 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.Validations.PEMPrivateKey do
use Ash.Resource.Validation

def init(opts) do
if is_atom(opts[:attribute]) do
{:ok, opts}
else
{:error, "attribute must be an atom"}
end
end

def validate(changeset, opts) do
case Ash.Changeset.fetch_argument_or_change(changeset, opts[:attribute]) do
{:ok, nil} ->
:ok

{:ok, changing_to} when is_binary(changing_to) ->
case X509.PrivateKey.from_pem(changing_to) do
{:ok, _} ->
:ok

{:error, _reason} ->
{:error, field: opts[:attribute], message: "is not a valid PEM private key"}
end

_ ->
:ok
end
end
end
9 changes: 9 additions & 0 deletions backend/lib/edgehog/validations/validations.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,13 @@ defmodule Edgehog.Validations do
match: ~r/^[a-z\d\-]+$/,
message: "should only contain lower case ASCII letters (from a to z), digits and -"}
end

def realm_name(attribute) do
{Validation.Match,
attribute: attribute,
match: ~r/^[a-z][a-z0-9]{0,47}$/,
message:
"should only contain lower case ASCII letters (from a to z) and digits, " <>
"and start with a lower case ASCII letter"}
end
end
116 changes: 62 additions & 54 deletions backend/test/edgehog/astarte_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -106,89 +106,97 @@ defmodule Edgehog.AstarteTest do
end
end

describe "realms" do
describe "Realm" do
@describetag :ported_to_ash

alias Edgehog.Astarte.Realm

@valid_private_key X509.PrivateKey.new_ec(:secp256r1) |> X509.PrivateKey.to_pem()
@other_private_key X509.PrivateKey.new_ec(:secp256r1) |> X509.PrivateKey.to_pem()

setup do
%{cluster: cluster_fixture()}
end

@invalid_attrs %{name: nil, private_key: nil}

test "list_realms/0 returns all realms", %{cluster: cluster} do
realm = realm_fixture(cluster)
assert Astarte.list_realms() == [realm]
end

test "get_realm!/1 returns the realm with given id", %{cluster: cluster} do
realm = realm_fixture(cluster)
assert Astarte.get_realm!(realm.id) == realm
end

test "create_realm/1 with valid data creates a realm", %{cluster: cluster} do
valid_attrs = %{name: "somename", private_key: @valid_private_key}
test "create with valid data creates a realm" do
cluster = cluster_fixture()
tenant = tenant_fixture()
valid_attrs = %{cluster_id: cluster.id, name: "somename", private_key: @valid_private_key}

assert {:ok, %Realm{} = realm} = Astarte.create_realm(cluster, valid_attrs)
assert {:ok, %Realm{} = realm} = Realm.create(valid_attrs, tenant: tenant)
assert realm.name == "somename"
assert realm.private_key == @valid_private_key
assert realm.tenant_id == tenant.tenant_id
end

test "create_realm/1 with invalid data returns error changeset", %{cluster: cluster} do
assert {:error, %Ecto.Changeset{}} = Astarte.create_realm(cluster, @invalid_attrs)
test "create with invalid name returns error" do
assert {:error, %Ash.Error.Invalid{errors: [error]}} = create_realm(name: "42INVALID")
assert %{field: :name} = error
end

test "create_realm/2 with a duplicate name in the same tenant returns error", %{
cluster: cluster
} do
realm = realm_fixture(cluster)
test "create with invalid private key returns error" do
assert {:error, %Ash.Error.Invalid{errors: [error]}} =
create_realm(private_key: "not a private key")

attrs = %{name: realm.name, private_key: @valid_private_key}
assert %{field: :private_key} = error
end

assert {:error, changeset} = Astarte.create_realm(cluster, attrs)
assert "has already been taken" in errors_on(changeset)[:name]
test "create with a duplicate name in the same tenant returns error" do
tenant = tenant_fixture()
cluster = cluster_fixture()
realm = realm_fixture(cluster_id: cluster.id, tenant: tenant)

attrs = %{cluster_id: cluster.id, name: realm.name, private_key: @valid_private_key}

assert {:error, %Ash.Error.Invalid{errors: [error]}} = Realm.create(attrs, tenant: tenant)
assert %{field: :name, message: "has already been taken"} = error
end

test "create_realm/2 with a duplicate name in another tenant returns error", %{
cluster: cluster
} do
realm = realm_fixture(cluster)
test "create with a duplicate name in another tenant returns error" do
other_tenant = tenant_fixture()
cluster = cluster_fixture()
realm = realm_fixture(tenant: other_tenant, cluster_id: cluster.id)

tenant = tenant_fixture()
Repo.put_tenant_id(tenant.tenant_id)
attrs = %{cluster_id: cluster.id, name: realm.name, private_key: @valid_private_key}

attrs = %{name: realm.name, private_key: @valid_private_key}

assert {:error, changeset} = Astarte.create_realm(cluster, attrs)
assert "has already been taken" in errors_on(changeset)[:name]
assert {:error, %Ash.Error.Invalid{errors: [error]}} = Realm.create(attrs, tenant: tenant)
assert %{field: :name, message: "has already been taken"} = error
end

test "update_realm/2 with valid data updates the realm", %{cluster: cluster} do
realm = realm_fixture(cluster)
update_attrs = %{name: "someupdatedname", private_key: @other_private_key}
test "fetch_by_name returns the realm given its name" do
tenant = tenant_fixture()
realm = realm_fixture(tenant: tenant)

assert {:ok, %Realm{} = realm} = Astarte.update_realm(realm, update_attrs)
assert realm.name == "someupdatedname"
assert realm.private_key == @other_private_key
assert {:ok, realm} = Realm.fetch_by_name(realm.name, tenant: tenant, load: [:cluster])
end

test "update_realm/2 with invalid data returns error changeset", %{cluster: cluster} do
realm = realm_fixture(cluster)
assert {:error, %Ecto.Changeset{}} = Astarte.update_realm(realm, @invalid_attrs)
assert realm == Astarte.get_realm!(realm.id)
test "fetch_by_name returns error for non-existing realm" do
tenant = tenant_fixture()

assert {:error, %Ash.Error.Query.NotFound{}} =
Realm.fetch_by_name("nonexisting", tenant: tenant)
end

test "delete_realm/1 deletes the realm", %{cluster: cluster} do
realm = realm_fixture(cluster)
assert {:ok, %Realm{}} = Astarte.delete_realm(realm)
assert_raise Ecto.NoResultsError, fn -> Astarte.get_realm!(realm.id) end
test "destroy deletes the realm" do
tenant = tenant_fixture()
realm = realm_fixture(tenant: tenant)
assert :ok = Realm.destroy(realm)

assert {:error, %Ash.Error.Invalid{errors: [%Ash.Error.Query.NotFound{}]}} =
Astarte.get(Realm, realm.id, tenant: tenant)
end

test "change_realm/1 returns a realm changeset", %{cluster: cluster} do
realm = realm_fixture(cluster)
assert %Ecto.Changeset{} = Astarte.change_realm(realm)
defp create_realm(opts \\ []) do
{tenant, opts} = Keyword.pop_lazy(opts, :tenant, &Edgehog.TenantsFixtures.tenant_fixture/0)

{cluster_id, opts} =
Keyword.pop_lazy(opts, :cluster_id, fn -> cluster_fixture() |> Map.fetch!(:id) end)

opts
|> Enum.into(%{
cluster_id: cluster_id,
name: unique_realm_name(),
private_key: @valid_private_key
})
|> Edgehog.Astarte.Realm.create(tenant: tenant)
end
end

Expand Down
20 changes: 11 additions & 9 deletions backend/test/support/fixtures/astarte_fixtures.ex
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,19 @@ defmodule Edgehog.AstarteFixtures do
@doc """
Generate a realm.
"""
def realm_fixture(cluster, attrs \\ %{}) do
attrs =
attrs
|> Enum.into(%{
name: unique_realm_name(),
private_key: @private_key
})
def realm_fixture(opts \\ []) do
{tenant, opts} = Keyword.pop_lazy(opts, :tenant, &Edgehog.TenantsFixtures.tenant_fixture/0)

{:ok, realm} = Edgehog.Astarte.create_realm(cluster, attrs)
{cluster_id, opts} =
Keyword.pop_lazy(opts, :cluster_id, fn -> cluster_fixture() |> Map.fetch!(:id) end)

realm
opts
|> Enum.into(%{
cluster_id: cluster_id,
name: unique_realm_name(),
private_key: @private_key
})
|> Edgehog.Astarte.Realm.create!(tenant: tenant.tenant_id)
end

@doc """
Expand Down

0 comments on commit 6989e1e

Please sign in to comment.