From 64cd7643b02675d6077a36251f3b9cd14ef06cb1 Mon Sep 17 00:00:00 2001 From: DarkyZ aka NotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Tue, 20 Aug 2024 11:05:32 +0200 Subject: [PATCH 1/7] :sparkles: Create items migration --- .../lib/elven_data/enums/item_enums.ex | 15 +++++++++++++ .../lib/elven_database/players/character.ex | 2 ++ .../20210414112303_create_accounts.exs | 9 +++----- .../20210515152135_create_characters.exs | 9 +++----- .../20240820084419_create_items.exs | 21 +++++++++++++++++++ 5 files changed, 44 insertions(+), 12 deletions(-) create mode 100644 apps/elven_data/lib/elven_data/enums/item_enums.ex create mode 100644 apps/elven_database/priv/repo/migrations/20240820084419_create_items.exs diff --git a/apps/elven_data/lib/elven_data/enums/item_enums.ex b/apps/elven_data/lib/elven_data/enums/item_enums.ex new file mode 100644 index 00000000..d5093b5b --- /dev/null +++ b/apps/elven_data/lib/elven_data/enums/item_enums.ex @@ -0,0 +1,15 @@ +defmodule ElvenData.Enums.ItemEnums do + @moduledoc """ + TODO: Documentation + """ + + import SimpleEnum, only: [defenum: 2] + + defenum :inventory_type, + equipment: 0, + main: 1, + etc: 2, + miniland: 3, + specialist: 6, + costume: 7 +end diff --git a/apps/elven_database/lib/elven_database/players/character.ex b/apps/elven_database/lib/elven_database/players/character.ex index 4c35ad5f..abc13034 100644 --- a/apps/elven_database/lib/elven_database/players/character.ex +++ b/apps/elven_database/lib/elven_database/players/character.ex @@ -10,6 +10,8 @@ defmodule ElvenDatabase.Players.Character do alias ElvenData.Enums.PlayerEnums + @type t :: %__MODULE__{} + # defbitfield GameOptions, # exchange_blocked: round(:math.pow(2, 1)), # friend_request_blocked: round(:math.pow(2, 2)), diff --git a/apps/elven_database/priv/repo/migrations/20210414112303_create_accounts.exs b/apps/elven_database/priv/repo/migrations/20210414112303_create_accounts.exs index 670aed07..8444d28d 100644 --- a/apps/elven_database/priv/repo/migrations/20210414112303_create_accounts.exs +++ b/apps/elven_database/priv/repo/migrations/20210414112303_create_accounts.exs @@ -1,13 +1,10 @@ defmodule ElvenDatabase.Repo.Migrations.CreateAccounts do use Ecto.Migration - require ElvenDatabase.EctoEnumHelpers - require ElvenData.Enums.PlayerEnums + require ElvenDatabase.EctoEnumHelpers, as: EctoEnumHelpers + require ElvenData.Enums.PlayerEnums, as: PlayerEnums - alias ElvenDatabase.EctoEnumHelpers - alias ElvenData.Enums.PlayerEnums - - def change do + def change() do execute( EctoEnumHelpers.create_query(PlayerEnums, :authority), EctoEnumHelpers.drop_query(:authority) diff --git a/apps/elven_database/priv/repo/migrations/20210515152135_create_characters.exs b/apps/elven_database/priv/repo/migrations/20210515152135_create_characters.exs index 7c885ec5..3badbec5 100644 --- a/apps/elven_database/priv/repo/migrations/20210515152135_create_characters.exs +++ b/apps/elven_database/priv/repo/migrations/20210515152135_create_characters.exs @@ -1,13 +1,10 @@ defmodule ElvenDatabase.Repo.Migrations.CreateCharacters do use Ecto.Migration - require ElvenDatabase.EctoEnumHelpers - require ElvenData.Enums.PlayerEnums + require ElvenDatabase.EctoEnumHelpers, as: EctoEnumHelpers + require ElvenData.Enums.PlayerEnums, as: PlayerEnums - alias ElvenDatabase.EctoEnumHelpers - alias ElvenData.Enums.PlayerEnums - - def change do + def change() do execute( EctoEnumHelpers.create_query(PlayerEnums, :character_class), EctoEnumHelpers.drop_query(:character_class) diff --git a/apps/elven_database/priv/repo/migrations/20240820084419_create_items.exs b/apps/elven_database/priv/repo/migrations/20240820084419_create_items.exs new file mode 100644 index 00000000..5e12bfa8 --- /dev/null +++ b/apps/elven_database/priv/repo/migrations/20240820084419_create_items.exs @@ -0,0 +1,21 @@ +defmodule ElvenDatabase.Repo.Migrations.CreateItems do + use Ecto.Migration + + require ElvenDatabase.EctoEnumHelpers, as: EctoEnumHelpers + require ElvenData.Enums.ItemEnums, as: ItemEnums + + def change() do + execute( + EctoEnumHelpers.create_query(ItemEnums, :inventory_type), + EctoEnumHelpers.drop_query(:inventory_type) + ) + + create table(:items) do + add :owner_id, references(:characters), null: false + add :inventory_type, :inventory_type_enum, null: false + add :slot, :int2, null: false + add :vnum, :int2, null: false + add :quantity, :int2, null: false + end + end +end From 23555c9c983448fd9bbdb227627a97aac0ef02a7 Mon Sep 17 00:00:00 2001 From: DarkyZ aka NotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Tue, 20 Aug 2024 12:07:40 +0200 Subject: [PATCH 2/7] :sparkles: Add Items queries module --- .../lib/elven_data/enums/item_enums.ex | 24 ++++++++ .../lib/elven_database/players/character.ex | 15 ++--- .../lib/elven_database/players/item.ex | 57 +++++++++++++++++++ .../lib/elven_database/players/items.ex | 26 +++++++++ .../20240820084419_create_items.exs | 4 +- apps/elven_database/priv/repo/seeds.exs | 53 ++++++++++++++++- 6 files changed, 168 insertions(+), 11 deletions(-) create mode 100644 apps/elven_database/lib/elven_database/players/item.ex create mode 100644 apps/elven_database/lib/elven_database/players/items.ex diff --git a/apps/elven_data/lib/elven_data/enums/item_enums.ex b/apps/elven_data/lib/elven_data/enums/item_enums.ex index d5093b5b..427aa14a 100644 --- a/apps/elven_data/lib/elven_data/enums/item_enums.ex +++ b/apps/elven_data/lib/elven_data/enums/item_enums.ex @@ -5,11 +5,35 @@ defmodule ElvenData.Enums.ItemEnums do import SimpleEnum, only: [defenum: 2] + ## Enums + defenum :inventory_type, + # equipped: not present in the game, for DB purpose + equipped: -1, equipment: 0, main: 1, etc: 2, miniland: 3, specialist: 6, costume: 7 + + defenum :slot_type, [ + :main_weapon, + :armor, + :hat, + :gloves, + :boots, + :secondary_weapon, + :necklace, + :ring, + :bracelet, + :mask, + :fairy, + :amulet, + :sp, + :costume_suit, + :costume_hat, + :weapon_skin, + :wings + ] end diff --git a/apps/elven_database/lib/elven_database/players/character.ex b/apps/elven_database/lib/elven_database/players/character.ex index abc13034..b5ae9e12 100644 --- a/apps/elven_database/lib/elven_database/players/character.ex +++ b/apps/elven_database/lib/elven_database/players/character.ex @@ -6,10 +6,9 @@ defmodule ElvenDatabase.Players.Character do import Ecto.Changeset # import EctoBitfield - require ElvenData.Enums.PlayerEnums - - alias ElvenData.Enums.PlayerEnums + require ElvenData.Enums.PlayerEnums, as: PlayerEnums + # FIXME: Later improve this typespec @type t :: %__MODULE__{} # defbitfield GameOptions, @@ -28,6 +27,8 @@ defmodule ElvenDatabase.Players.Character do # hats_hidden: round(:math.pow(2, 16)), # ui_locked: round(:math.pow(2, 17)) + ## Schema + schema "characters" do belongs_to :account, ElvenDatabase.Players.Account field :name, :string @@ -138,8 +139,8 @@ defmodule ElvenDatabase.Players.Character do @name_regex ~r/^[\x21-\x7E\xA1-\xAC\xAE-\xFF\x{4e00}-\x{9fa5}\x{0E01}-\x{0E3A}\x{0E3F}-\x{0E5B}\x2E]{4,14}$/u @doc false - def changeset(account, attrs) do - account + def changeset(character, attrs) do + character |> cast(attrs, @fields) |> cast_assoc(:account) |> validate_required(@required_fields) @@ -149,8 +150,8 @@ defmodule ElvenDatabase.Players.Character do end @doc false - def disabled_changeset(account, attrs) do - account + def disabled_changeset(character, attrs) do + character |> cast(attrs, @fields) |> cast_assoc(:account) |> validate_required(@required_fields) diff --git a/apps/elven_database/lib/elven_database/players/item.ex b/apps/elven_database/lib/elven_database/players/item.ex new file mode 100644 index 00000000..7720b4f3 --- /dev/null +++ b/apps/elven_database/lib/elven_database/players/item.ex @@ -0,0 +1,57 @@ +defmodule ElvenDatabase.Players.Item do + @moduledoc false + + use Ecto.Schema + + import Ecto.Changeset + + require ElvenData.Enums.ItemEnums, as: ItemEnums + + alias __MODULE__ + + @type t :: %Item{ + id: non_neg_integer(), + owner_id: non_neg_integer(), + inventory_type: ItemEnums.inventory_type_keys(), + slot: ItemEnums.slot_type() | non_neg_integer(), + vnum: non_neg_integer(), + quantity: non_neg_integer() + } + + ## Schema + + schema "items" do + belongs_to :owner, ElvenDatabase.Players.Character + + field :inventory_type, Ecto.Enum, values: ItemEnums.inventory_type(:__keys__) + field :slot, :integer + field :vnum, :integer + field :quantity, :integer + + timestamps() + end + + ## Public API + + @fields [ + :owner_id, + :inventory_type, + :slot, + :vnum, + :quantity + ] + + @spec changeset(Item.t(), map()) :: Ecto.Changeset.t() + def changeset(item, attrs) do + attrs = + case attrs do + %{slot: slot} when is_atom(slot) -> Map.put(attrs, :slot, ItemEnums.slot_type(slot)) + attrs -> attrs + end + + item + |> cast(attrs, @fields) + |> cast_assoc(:owner) + |> validate_required(@fields) + end +end diff --git a/apps/elven_database/lib/elven_database/players/items.ex b/apps/elven_database/lib/elven_database/players/items.ex new file mode 100644 index 00000000..49614b1b --- /dev/null +++ b/apps/elven_database/lib/elven_database/players/items.ex @@ -0,0 +1,26 @@ +defmodule ElvenDatabase.Players.Items do + @moduledoc """ + TODO: Documentation + """ + + # import Ecto.Query, only: [from: 2] + + alias ElvenDatabase.Players.Item + alias ElvenDatabase.Repo + + ## Public API + + @spec create(map()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} + def create(attrs) do + %Item{} + |> Item.changeset(attrs) + |> Repo.insert() + end + + @spec create!(map()) :: Ecto.Schema.t() + def create!(attrs) do + %Item{} + |> Item.changeset(attrs) + |> Repo.insert!() + end +end diff --git a/apps/elven_database/priv/repo/migrations/20240820084419_create_items.exs b/apps/elven_database/priv/repo/migrations/20240820084419_create_items.exs index 5e12bfa8..a969d598 100644 --- a/apps/elven_database/priv/repo/migrations/20240820084419_create_items.exs +++ b/apps/elven_database/priv/repo/migrations/20240820084419_create_items.exs @@ -9,13 +9,15 @@ defmodule ElvenDatabase.Repo.Migrations.CreateItems do EctoEnumHelpers.create_query(ItemEnums, :inventory_type), EctoEnumHelpers.drop_query(:inventory_type) ) - + create table(:items) do add :owner_id, references(:characters), null: false add :inventory_type, :inventory_type_enum, null: false add :slot, :int2, null: false add :vnum, :int2, null: false add :quantity, :int2, null: false + + timestamps() end end end diff --git a/apps/elven_database/priv/repo/seeds.exs b/apps/elven_database/priv/repo/seeds.exs index d792e5c3..a35439b9 100644 --- a/apps/elven_database/priv/repo/seeds.exs +++ b/apps/elven_database/priv/repo/seeds.exs @@ -10,7 +10,13 @@ # We recommend using the bang functions (`insert!`, `update!` # and so on) as they will fail if something goes wrong. -alias ElvenDatabase.Players.{Account, Accounts, Characters} +alias ElvenDatabase.Players.{ + Account, + Accounts, + Character, + Characters, + Items +} ## Accounts @@ -29,7 +35,7 @@ alias ElvenDatabase.Players.{Account, Accounts, Characters} ## Characters -Characters.create!(%{ +%Character{id: admin_char_id} = Characters.create!(%{ account_id: admin_id, slot: 1, name: "DarkyZ", @@ -57,7 +63,7 @@ Characters.create!(%{ compliment: 500 }) -Characters.create!(%{ +%Character{id: user_char_id} = Characters.create!(%{ account_id: user_id, slot: 0, name: "ExampleUser", @@ -76,3 +82,44 @@ Characters.create!(%{ dignity: 100, compliment: 50 }) + +## Base items + +base_items = [ + %{ + inventory_type: :equipped, + slot: :main_weapon, + vnum: 1, + quantity: 1, + }, + %{ + inventory_type: :equipped, + slot: :armor, + vnum: 12, + quantity: 1, + }, + %{ + inventory_type: :equipped, + slot: :secondary_weapon, + vnum: 8, + quantity: 1, + }, + %{ + inventory_type: :etc, + slot: 0, + vnum: 2024, + quantity: 10, + }, + %{ + inventory_type: :etc, + slot: 1, + vnum: 2081, + quantity: 1, + } +] + +for item <- base_items, character_id <- [admin_char_id, user_char_id] do + item + |> Map.put(:owner_id, character_id) + |> Items.create!() +end From 6baaaf34d0f071440c3471f7c55897804fabf964 Mon Sep 17 00:00:00 2001 From: DarkyZ aka NotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Tue, 20 Aug 2024 15:12:55 +0200 Subject: [PATCH 3/7] :sparkles: Fix Dyalizer warnings --- apps/elven_database/lib/elven_database/players/item.ex | 8 ++++++-- apps/elven_database/lib/elven_database/players/items.ex | 7 +++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/apps/elven_database/lib/elven_database/players/item.ex b/apps/elven_database/lib/elven_database/players/item.ex index 7720b4f3..1c4cc042 100644 --- a/apps/elven_database/lib/elven_database/players/item.ex +++ b/apps/elven_database/lib/elven_database/players/item.ex @@ -15,7 +15,11 @@ defmodule ElvenDatabase.Players.Item do inventory_type: ItemEnums.inventory_type_keys(), slot: ItemEnums.slot_type() | non_neg_integer(), vnum: non_neg_integer(), - quantity: non_neg_integer() + quantity: non_neg_integer(), + # Ecto fields + __meta__: Ecto.Schema.Metadata.t(), + inserted_at: any(), + updated_at: any() } ## Schema @@ -42,7 +46,7 @@ defmodule ElvenDatabase.Players.Item do ] @spec changeset(Item.t(), map()) :: Ecto.Changeset.t() - def changeset(item, attrs) do + def changeset(%Item{} = item, attrs) do attrs = case attrs do %{slot: slot} when is_atom(slot) -> Map.put(attrs, :slot, ItemEnums.slot_type(slot)) diff --git a/apps/elven_database/lib/elven_database/players/items.ex b/apps/elven_database/lib/elven_database/players/items.ex index 49614b1b..5dd04ace 100644 --- a/apps/elven_database/lib/elven_database/players/items.ex +++ b/apps/elven_database/lib/elven_database/players/items.ex @@ -8,6 +8,13 @@ defmodule ElvenDatabase.Players.Items do alias ElvenDatabase.Players.Item alias ElvenDatabase.Repo + # Dyalizer doesn't like `Item.changeset(%Item{}, attrs)` + # because fields on Item struct can't be nil + @dialyzer [ + {:no_return, create: 1, create!: 1}, + {:no_fail_call, create: 1, create!: 1} + ] + ## Public API @spec create(map()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} From f9ef07c99f41fc3dee84fee852d8dbb2135a5f00 Mon Sep 17 00:00:00 2001 From: DarkyZ aka NotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Tue, 20 Aug 2024 15:13:27 +0200 Subject: [PATCH 4/7] :sparkles: Items CRUD --- .../lib/elven_database/players/character.ex | 4 +++ .../lib/elven_database/players/item.ex | 1 + .../lib/elven_database/players/items.ex | 32 ++++++++++++++++++- .../20240820084419_create_items.exs | 2 ++ 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/apps/elven_database/lib/elven_database/players/character.ex b/apps/elven_database/lib/elven_database/players/character.ex index b5ae9e12..b2694a4d 100644 --- a/apps/elven_database/lib/elven_database/players/character.ex +++ b/apps/elven_database/lib/elven_database/players/character.ex @@ -8,6 +8,8 @@ defmodule ElvenDatabase.Players.Character do require ElvenData.Enums.PlayerEnums, as: PlayerEnums + alias ElvenDatabase.Players.Item + # FIXME: Later improve this typespec @type t :: %__MODULE__{} @@ -83,6 +85,8 @@ defmodule ElvenDatabase.Players.Character do # field :game_options, GameOptions + has_many :items, Item, foreign_key: :owner_id + timestamps() end diff --git a/apps/elven_database/lib/elven_database/players/item.ex b/apps/elven_database/lib/elven_database/players/item.ex index 1c4cc042..726fcbe2 100644 --- a/apps/elven_database/lib/elven_database/players/item.ex +++ b/apps/elven_database/lib/elven_database/players/item.ex @@ -57,5 +57,6 @@ defmodule ElvenDatabase.Players.Item do |> cast(attrs, @fields) |> cast_assoc(:owner) |> validate_required(@fields) + |> unique_constraint(:slot, name: :owner_inventory_slot) end end diff --git a/apps/elven_database/lib/elven_database/players/items.ex b/apps/elven_database/lib/elven_database/players/items.ex index 5dd04ace..e959d750 100644 --- a/apps/elven_database/lib/elven_database/players/items.ex +++ b/apps/elven_database/lib/elven_database/players/items.ex @@ -3,7 +3,7 @@ defmodule ElvenDatabase.Players.Items do TODO: Documentation """ - # import Ecto.Query, only: [from: 2] + import Ecto.Query, only: [from: 2] alias ElvenDatabase.Players.Item alias ElvenDatabase.Repo @@ -30,4 +30,34 @@ defmodule ElvenDatabase.Players.Items do |> Item.changeset(attrs) |> Repo.insert!() end + + @spec list_by_owner(non_neg_integer()) :: [Item.t()] + def list_by_owner(character_id) do + from(c in Item, where: c.owner_id == ^character_id) + |> Repo.all() + end + + @spec update(Item.t(), map()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} + def update(%Item{} = item, attrs) do + item + |> Item.changeset(attrs) + |> Repo.update() + end + + @spec update!(Item.t(), map()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} + def update!(%Item{} = item, attrs) do + item + |> Item.changeset(attrs) + |> Repo.update!() + end + + @spec delete(Item.t()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} + def delete(%Item{} = item) do + Repo.delete(item) + end + + @spec delete!(Item.t()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} + def delete!(%Item{} = item) do + Repo.delete!(item) + end end diff --git a/apps/elven_database/priv/repo/migrations/20240820084419_create_items.exs b/apps/elven_database/priv/repo/migrations/20240820084419_create_items.exs index a969d598..17e8393f 100644 --- a/apps/elven_database/priv/repo/migrations/20240820084419_create_items.exs +++ b/apps/elven_database/priv/repo/migrations/20240820084419_create_items.exs @@ -19,5 +19,7 @@ defmodule ElvenDatabase.Repo.Migrations.CreateItems do timestamps() end + + create unique_index(:items, [:owner_id, :inventory_type, :slot], name: :owner_inventory_slot) end end From 6150aae1cc0a7d3f21fa087f1e1f2ab916e34887 Mon Sep 17 00:00:00 2001 From: DarkyZ aka NotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Tue, 20 Aug 2024 17:41:45 +0200 Subject: [PATCH 5/7] :sparkles: Setup tests for ElvenDatabase --- apps/elven_database/config/runtime.exs | 7 ++ apps/elven_database/config/runtime.test.exs | 8 ++ .../lib/elven_database/players/item.ex | 3 +- apps/elven_database/mix.exs | 10 ++- .../elven_database/players/items_test.exs | 83 +++++++++++++++++++ apps/elven_database/test/support/repo_case.ex | 74 +++++++++++++++++ apps/elven_database/test/test_helper.exs | 1 + 7 files changed, 183 insertions(+), 3 deletions(-) create mode 100644 apps/elven_database/config/runtime.exs create mode 100644 apps/elven_database/config/runtime.test.exs create mode 100644 apps/elven_database/test/elven_database/players/items_test.exs create mode 100644 apps/elven_database/test/support/repo_case.ex diff --git a/apps/elven_database/config/runtime.exs b/apps/elven_database/config/runtime.exs new file mode 100644 index 00000000..fe70c92d --- /dev/null +++ b/apps/elven_database/config/runtime.exs @@ -0,0 +1,7 @@ +import Config + +# Import environment specific config. This must remain at the bottom +# of this file so it overrides the configuration defined above. +if File.exists?("#{__DIR__}/runtime.#{config_env()}.exs") do + Code.require_file("runtime.#{config_env()}.exs", __DIR__) +end diff --git a/apps/elven_database/config/runtime.test.exs b/apps/elven_database/config/runtime.test.exs new file mode 100644 index 00000000..369c9901 --- /dev/null +++ b/apps/elven_database/config/runtime.test.exs @@ -0,0 +1,8 @@ +import Config + +config :elven_database, ElvenDatabase.Repo, + database: "elvengard_test", + username: "postgres", + password: "postgres", + hostname: "localhost", + pool: Ecto.Adapters.SQL.Sandbox diff --git a/apps/elven_database/lib/elven_database/players/item.ex b/apps/elven_database/lib/elven_database/players/item.ex index 726fcbe2..cbe8bacd 100644 --- a/apps/elven_database/lib/elven_database/players/item.ex +++ b/apps/elven_database/lib/elven_database/players/item.ex @@ -55,7 +55,8 @@ defmodule ElvenDatabase.Players.Item do item |> cast(attrs, @fields) - |> cast_assoc(:owner) + # |> cast_assoc(:owner) + |> foreign_key_constraint(:owner_id) |> validate_required(@fields) |> unique_constraint(:slot, name: :owner_inventory_slot) end diff --git a/apps/elven_database/mix.exs b/apps/elven_database/mix.exs index 99e4a5d6..ec53a9d9 100644 --- a/apps/elven_database/mix.exs +++ b/apps/elven_database/mix.exs @@ -8,10 +8,15 @@ defmodule ElvenDatabase.MixProject do elixir: "~> 1.13", start_permanent: Mix.env() == :prod, deps: deps(), - aliases: aliases() + aliases: aliases(), + elixirc_paths: elixirc_paths(Mix.env()) ] end + # Specifies which paths to compile per environment. + defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(_), do: ["lib"] + # Run "mix help compile.app" to learn about applications. def application do [ @@ -32,7 +37,8 @@ defmodule ElvenDatabase.MixProject do defp aliases do [ "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], - "ecto.reset": ["ecto.drop", "ecto.setup"] + "ecto.reset": ["ecto.drop", "ecto.setup"], + test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"] ] end end diff --git a/apps/elven_database/test/elven_database/players/items_test.exs b/apps/elven_database/test/elven_database/players/items_test.exs new file mode 100644 index 00000000..9d1b46d1 --- /dev/null +++ b/apps/elven_database/test/elven_database/players/items_test.exs @@ -0,0 +1,83 @@ +defmodule ElvenDatabase.Players.ItemsTest do + use ElvenDatabase.RepoCase, async: true + + alias ElvenDatabase.Players.{Accounts, Characters, Item, Items} + + ## Setup + + setup do + # Create an account for each test + account = Accounts.create!(account_attrs()) + + # Each account have a character + character = Characters.create!(character_attrs(account.id)) + + # Return state + %{account: account, character: character} + end + + ## Tests + + describe "create/1" do + test "can create an item", %{character: character} do + attrs = %{ + owner_id: character.id, + inventory_type: :etc, + slot: 10, + vnum: 2, + quantity: 3 + } + + assert {:ok, item} = Items.create(attrs) + assert %Item{} = item + assert item.owner_id == character.id + assert item.inventory_type == :etc + assert item.slot == 10 + assert item.vnum == 2 + assert item.quantity == 3 + end + + test "can create an item with a slot as atom", %{character: character} do + attrs = %{ + owner_id: character.id, + inventory_type: :equipped, + slot: :secondary_weapon, + vnum: 1, + quantity: 1 + } + + assert {:ok, item} = Items.create(attrs) + assert item.slot == 5 + end + + test "owner must exists" do + attrs = %{ + owner_id: 30_000, + inventory_type: :equipped, + slot: :secondary_weapon, + vnum: 1, + quantity: 1 + } + + assert {:error, changeset} = Items.create(attrs) + assert changeset_error(changeset) == "owner_id does not exist" + end + + test "slot must be unique", %{character: character} do + attrs = %{ + owner_id: character.id, + inventory_type: :equipped, + slot: :secondary_weapon, + vnum: 1, + quantity: 1 + } + + # First insert is fine + assert {:ok, _item} = Items.create(attrs) + + # Same slot return an error + assert {:error, changeset} = Items.create(attrs) + assert changeset_error(changeset) == "slot has already been taken" + end + end +end diff --git a/apps/elven_database/test/support/repo_case.ex b/apps/elven_database/test/support/repo_case.ex new file mode 100644 index 00000000..5db0d344 --- /dev/null +++ b/apps/elven_database/test/support/repo_case.ex @@ -0,0 +1,74 @@ +defmodule ElvenDatabase.RepoCase do + use ExUnit.CaseTemplate + + require ElvenData.Enums.PlayerEnums + alias ElvenData.Enums.PlayerEnums + + ## Case + + using do + quote do + import Ecto + import Ecto.Query + import unquote(__MODULE__), only: :functions + + alias ElvenDatabase.Repo + end + end + + setup tags do + pid = Ecto.Adapters.SQL.Sandbox.start_owner!(ElvenDatabase.Repo, shared: not tags[:async]) + on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end) + :ok + end + + ## Public API + + def changeset_error(%Ecto.Changeset{errors: [{field, {error, _}}]}) do + "#{field} #{error}" + end + + def account_attrs() do + %{ + username: random_string(), + password: random_string(), + authority: Enum.random(PlayerEnums.authority(:__keys__)) + } + end + + def character_attrs(account_id) do + %{ + account_id: account_id, + slot: Enum.random(0..3), + name: random_string(), + gender: :female, + hair_style: :hair_style_a, + hair_color: :dark_purple, + class: :martial_artist, + faction: :demon, + map_id: 1, + map_x: :rand.uniform(3) + 77, + map_y: :rand.uniform(4) + 113, + gold: 1_000_000_000, + bank_gold: 5_000_000, + biography: nil, + level: 96, + job_level: 80, + hero_level: 25, + level_xp: 3_000, + job_level_xp: 4_500, + hero_level_xp: 1_000, + reputation: 5_000_000, + dignity: 100, + sp_points: 10_000, + sp_additional_points: 500_000, + compliment: 500 + } + end + + ## Private API + + defp random_string() do + :crypto.strong_rand_bytes(5) |> Base.encode16(case: :lower) + end +end diff --git a/apps/elven_database/test/test_helper.exs b/apps/elven_database/test/test_helper.exs index 869559e7..517c7dec 100644 --- a/apps/elven_database/test/test_helper.exs +++ b/apps/elven_database/test/test_helper.exs @@ -1 +1,2 @@ ExUnit.start() +Ecto.Adapters.SQL.Sandbox.mode(ElvenDatabase.Repo, :manual) From b1936a4dc74dfa134bd547ec13f865de2e9a6219 Mon Sep 17 00:00:00 2001 From: DarkyZ aka NotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Wed, 21 Aug 2024 14:39:33 +0200 Subject: [PATCH 6/7] :rocket: Add few tests --- .../lib/elven_database/players/item.ex | 11 +- .../lib/elven_database/players/items.ex | 25 +- apps/elven_database/priv/repo/seeds.exs | 9 +- .../elven_database/players/items_test.exs | 289 +++++++++++++++++- 4 files changed, 310 insertions(+), 24 deletions(-) diff --git a/apps/elven_database/lib/elven_database/players/item.ex b/apps/elven_database/lib/elven_database/players/item.ex index cbe8bacd..50ab6c59 100644 --- a/apps/elven_database/lib/elven_database/players/item.ex +++ b/apps/elven_database/lib/elven_database/players/item.ex @@ -8,9 +8,11 @@ defmodule ElvenDatabase.Players.Item do require ElvenData.Enums.ItemEnums, as: ItemEnums alias __MODULE__ + alias ElvenDatabase.Players.Character + @type id :: non_neg_integer() @type t :: %Item{ - id: non_neg_integer(), + id: id(), owner_id: non_neg_integer(), inventory_type: ItemEnums.inventory_type_keys(), slot: ItemEnums.slot_type() | non_neg_integer(), @@ -53,9 +55,14 @@ defmodule ElvenDatabase.Players.Item do attrs -> attrs end + attrs = + case attrs do + %{owner: %Character{} = owner} -> Map.put(attrs, :owner_id, owner.id) + attrs -> attrs + end + item |> cast(attrs, @fields) - # |> cast_assoc(:owner) |> foreign_key_constraint(:owner_id) |> validate_required(@fields) |> unique_constraint(:slot, name: :owner_inventory_slot) diff --git a/apps/elven_database/lib/elven_database/players/items.ex b/apps/elven_database/lib/elven_database/players/items.ex index e959d750..b8ac6984 100644 --- a/apps/elven_database/lib/elven_database/players/items.ex +++ b/apps/elven_database/lib/elven_database/players/items.ex @@ -17,46 +17,59 @@ defmodule ElvenDatabase.Players.Items do ## Public API - @spec create(map()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} + @spec create(map()) :: {:ok, Item.t()} | {:error, Ecto.Changeset.t()} def create(attrs) do %Item{} |> Item.changeset(attrs) |> Repo.insert() end - @spec create!(map()) :: Ecto.Schema.t() + @spec create!(map()) :: Item.t() def create!(attrs) do %Item{} |> Item.changeset(attrs) |> Repo.insert!() end + @spec get(Item.id()) :: {:ok, Item.t()} | {:error, :not_found} + def get(id) do + case Repo.get(Item, id) do + nil -> {:error, :not_found} + item -> {:ok, item} + end + end + + @spec get!(Item.id()) :: Item.t() + def get!(id) do + Repo.get!(Item, id) + end + @spec list_by_owner(non_neg_integer()) :: [Item.t()] def list_by_owner(character_id) do from(c in Item, where: c.owner_id == ^character_id) |> Repo.all() end - @spec update(Item.t(), map()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} + @spec update(Item.t(), map()) :: {:ok, Item.t()} | {:error, Ecto.Changeset.t()} def update(%Item{} = item, attrs) do item |> Item.changeset(attrs) |> Repo.update() end - @spec update!(Item.t(), map()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} + @spec update!(Item.t(), map()) :: {:ok, Item.t()} | {:error, Ecto.Changeset.t()} def update!(%Item{} = item, attrs) do item |> Item.changeset(attrs) |> Repo.update!() end - @spec delete(Item.t()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} + @spec delete(Item.t()) :: {:ok, Item.t()} | {:error, Ecto.Changeset.t()} def delete(%Item{} = item) do Repo.delete(item) end - @spec delete!(Item.t()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} + @spec delete!(Item.t()) :: {:ok, Item.t()} | {:error, Ecto.Changeset.t()} def delete!(%Item{} = item) do Repo.delete!(item) end diff --git a/apps/elven_database/priv/repo/seeds.exs b/apps/elven_database/priv/repo/seeds.exs index a35439b9..4b83d6fc 100644 --- a/apps/elven_database/priv/repo/seeds.exs +++ b/apps/elven_database/priv/repo/seeds.exs @@ -13,7 +13,6 @@ alias ElvenDatabase.Players.{ Account, Accounts, - Character, Characters, Items } @@ -35,7 +34,7 @@ alias ElvenDatabase.Players.{ ## Characters -%Character{id: admin_char_id} = Characters.create!(%{ +admin_char = Characters.create!(%{ account_id: admin_id, slot: 1, name: "DarkyZ", @@ -63,7 +62,7 @@ alias ElvenDatabase.Players.{ compliment: 500 }) -%Character{id: user_char_id} = Characters.create!(%{ +user_char = Characters.create!(%{ account_id: user_id, slot: 0, name: "ExampleUser", @@ -118,8 +117,8 @@ base_items = [ } ] -for item <- base_items, character_id <- [admin_char_id, user_char_id] do +for item <- base_items, character <- [admin_char, user_char] do item - |> Map.put(:owner_id, character_id) + |> Map.put(:owner, character) |> Items.create!() end diff --git a/apps/elven_database/test/elven_database/players/items_test.exs b/apps/elven_database/test/elven_database/players/items_test.exs index 9d1b46d1..da674272 100644 --- a/apps/elven_database/test/elven_database/players/items_test.exs +++ b/apps/elven_database/test/elven_database/players/items_test.exs @@ -5,21 +5,48 @@ defmodule ElvenDatabase.Players.ItemsTest do ## Setup - setup do - # Create an account for each test - account = Accounts.create!(account_attrs()) + setup ctx do + accounts_count = Map.get(ctx, :accounts, 1) + characters_count = Map.get(ctx, :characters, 1) - # Each account have a character - character = Characters.create!(character_attrs(account.id)) + # Create accounts for each test + accounts = + for _ <- 1..accounts_count do + Accounts.create!(account_attrs()) + end + + # Create characters for each account + characters = + for account <- accounts, _ <- 1..characters_count do + Characters.create!(character_attrs(account.id)) + end # Return state - %{account: account, character: character} + %{accounts: accounts, characters: characters} end ## Tests describe "create/1" do - test "can create an item", %{character: character} do + test "can create an item using owner", %{characters: [character]} do + attrs = %{ + owner: character, + inventory_type: :etc, + slot: 10, + vnum: 2, + quantity: 3 + } + + assert {:ok, item} = Items.create(attrs) + assert %Item{} = item + assert item.owner_id == character.id + assert item.inventory_type == :etc + assert item.slot == 10 + assert item.vnum == 2 + assert item.quantity == 3 + end + + test "can create an item using owner_id", %{characters: [character]} do attrs = %{ owner_id: character.id, inventory_type: :etc, @@ -37,7 +64,7 @@ defmodule ElvenDatabase.Players.ItemsTest do assert item.quantity == 3 end - test "can create an item with a slot as atom", %{character: character} do + test "can create an item with a slot as atom", %{characters: [character]} do attrs = %{ owner_id: character.id, inventory_type: :equipped, @@ -63,9 +90,12 @@ defmodule ElvenDatabase.Players.ItemsTest do assert changeset_error(changeset) == "owner_id does not exist" end - test "slot must be unique", %{character: character} do + @tag characters: 2 + test "slot must be unique by owner + inventory_type + slot", %{characters: characters} do + [character1, character2] = characters + attrs = %{ - owner_id: character.id, + owner_id: character1.id, inventory_type: :equipped, slot: :secondary_weapon, vnum: 1, @@ -75,9 +105,246 @@ defmodule ElvenDatabase.Players.ItemsTest do # First insert is fine assert {:ok, _item} = Items.create(attrs) - # Same slot return an error + # Using same inventory_type + slot but another owner_id is fine + assert {:ok, _item} = Items.create(%{attrs | owner_id: character2.id}) + + # Using same owner_id + slot but another inventory_type is fine + assert {:ok, _item} = Items.create(%{attrs | inventory_type: :etc}) + + # Using same owner_id + inventory_type but another slot is fine + assert {:ok, _item} = Items.create(%{attrs | slot: :main_weapon}) + + # Same owner + inventory_type + slot return an error assert {:error, changeset} = Items.create(attrs) assert changeset_error(changeset) == "slot has already been taken" end end + + describe "create!/1" do + test "can create an item using owner", %{characters: [character]} do + attrs = %{ + owner: character, + inventory_type: :etc, + slot: 10, + vnum: 2, + quantity: 3 + } + + assert %Item{} = item = Items.create!(attrs) + assert item.owner_id == character.id + assert item.inventory_type == :etc + assert item.slot == 10 + assert item.vnum == 2 + assert item.quantity == 3 + end + + test "can create an item using owner_id", %{characters: [character]} do + attrs = %{ + owner_id: character.id, + inventory_type: :etc, + slot: 10, + vnum: 2, + quantity: 3 + } + + assert %Item{} = item = Items.create!(attrs) + assert item.owner_id == character.id + assert item.inventory_type == :etc + assert item.slot == 10 + assert item.vnum == 2 + assert item.quantity == 3 + end + + test "can create an item with a slot as atom", %{characters: [character]} do + attrs = %{ + owner_id: character.id, + inventory_type: :equipped, + slot: :secondary_weapon, + vnum: 1, + quantity: 1 + } + + item = Items.create!(attrs) + assert item.slot == 5 + end + + test "owner must exists" do + attrs = %{ + owner_id: 30_000, + inventory_type: :equipped, + slot: :secondary_weapon, + vnum: 1, + quantity: 1 + } + + assert_raise Ecto.InvalidChangesetError, fn -> + Items.create!(attrs) + end + end + + @tag characters: 2 + test "slot must be unique by owner + inventory_type + slot", %{characters: characters} do + [character1, character2] = characters + + attrs = %{ + owner_id: character1.id, + inventory_type: :equipped, + slot: :secondary_weapon, + vnum: 1, + quantity: 1 + } + + # First insert is fine + _item = Items.create!(attrs) + + # Using same inventory_type + slot but another owner_id is fine + _item = Items.create!(%{attrs | owner_id: character2.id}) + + # Using same owner_id + slot but another inventory_type is fine + _item = Items.create!(%{attrs | inventory_type: :etc}) + + # Using same owner_id + inventory_type but another slot is fine + _item = Items.create!(%{attrs | slot: :main_weapon}) + + # Same owner + inventory_type + slot raise an error + assert_raise Ecto.InvalidChangesetError, fn -> + Items.create!(attrs) + end + end + end + + describe "get/1" do + test "get item by id", %{characters: [character]} do + item1 = + Items.create!(%{ + owner: character, + inventory_type: :etc, + slot: 10, + vnum: 2, + quantity: 3 + }) + + assert Items.get(item1.id) == {:ok, item1} + assert Items.get(10_000) == {:error, :not_found} + end + end + + describe "get!/1" do + test "get item by id", %{characters: [character]} do + item1 = + Items.create!(%{ + owner: character, + inventory_type: :etc, + slot: 10, + vnum: 2, + quantity: 3 + }) + + assert Items.get!(item1.id) == item1 + + assert_raise Ecto.NoResultsError, fn -> + Items.get!(10_000) + end + end + end + + @tag characters: 2 + describe "list_by_owner/1" do + test "list items by owner", %{characters: characters} do + [character1, character2] = characters + + item1 = + Items.create!(%{ + owner: character1, + inventory_type: :etc, + slot: 10, + vnum: 2, + quantity: 3 + }) + + item2 = + Items.create!(%{ + owner: character1, + inventory_type: :etc, + slot: 1, + vnum: 2, + quantity: 3 + }) + + assert Items.list_by_owner(character1.id) == [item1, item2] + assert Items.list_by_owner(character2.id) == [] + end + end + + describe "update/2" do + test "update an item", %{characters: [character]} do + item = + Items.create!(%{ + owner: character, + inventory_type: :etc, + slot: 10, + vnum: 2, + quantity: 3 + }) + + assert Items.update(item, %{slot: 42}) == {:ok, %Item{item | slot: 42}} + end + end + + describe "update!/2" do + test "update an item", %{characters: [character]} do + item = + Items.create!(%{ + owner: character, + inventory_type: :etc, + slot: 10, + vnum: 2, + quantity: 3 + }) + + assert Items.update!(item, %{slot: 42}) == %Item{item | slot: 42} + end + end + + describe "delete/1" do + test "delete an item", %{characters: [character]} do + item = + Items.create!(%{ + owner: character, + inventory_type: :etc, + slot: 10, + vnum: 2, + quantity: 3 + }) + + assert {:ok, %Item{}} = Items.delete(item) + assert Items.get(item.id) == {:error, :not_found} + + # Can't delete an item 2 times + assert_raise Ecto.StaleEntryError, fn -> + Items.delete(item) + end + end + end + + describe "delete!/1" do + test "delete an item", %{characters: [character]} do + item = + Items.create!(%{ + owner: character, + inventory_type: :etc, + slot: 10, + vnum: 2, + quantity: 3 + }) + + assert %Item{} = Items.delete!(item) + assert Items.get(item.id) == {:error, :not_found} + + # Can't delete an item 2 times + assert_raise Ecto.StaleEntryError, fn -> + Items.delete!(item) + end + end + end end From cfff3365a96861a5528ea70517a03938e705b0f6 Mon Sep 17 00:00:00 2001 From: DarkyZ aka NotAVirus <17680522+ImNotAVirus@users.noreply.github.com> Date: Wed, 21 Aug 2024 15:34:17 +0200 Subject: [PATCH 7/7] :rocket: Add Postgres to CI --- .github/workflows/ci.yml | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 78eab6a2..a13337ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,17 +29,20 @@ jobs: app: ${{fromJson(needs.directories.outputs.apps)}} env: MIX_ENV: test - # services: - # db: - # image: postgres:11 - # ports: ['5432:5432'] - # env: - # POSTGRES_PASSWORD: postgres - # options: >- - # --health-cmd pg_isready - # --health-interval 10s - # --health-timeout 5s - # --health-retries 5 + services: + db: + image: postgres:latest + ports: ['5432:5432'] + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_HOST_AUTH_METHOD: 'trust' + POSTGRES_DB: elvengard_test + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 steps: - uses: actions/checkout@v4 - name: Set up Elixir