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 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..427aa14a --- /dev/null +++ b/apps/elven_data/lib/elven_data/enums/item_enums.ex @@ -0,0 +1,39 @@ +defmodule ElvenData.Enums.ItemEnums do + @moduledoc """ + TODO: Documentation + """ + + 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/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/character.ex b/apps/elven_database/lib/elven_database/players/character.ex index 4c35ad5f..b2694a4d 100644 --- a/apps/elven_database/lib/elven_database/players/character.ex +++ b/apps/elven_database/lib/elven_database/players/character.ex @@ -6,9 +6,12 @@ defmodule ElvenDatabase.Players.Character do import Ecto.Changeset # import EctoBitfield - require ElvenData.Enums.PlayerEnums + require ElvenData.Enums.PlayerEnums, as: PlayerEnums - alias ElvenData.Enums.PlayerEnums + alias ElvenDatabase.Players.Item + + # FIXME: Later improve this typespec + @type t :: %__MODULE__{} # defbitfield GameOptions, # exchange_blocked: round(:math.pow(2, 1)), @@ -26,6 +29,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 @@ -80,6 +85,8 @@ defmodule ElvenDatabase.Players.Character do # field :game_options, GameOptions + has_many :items, Item, foreign_key: :owner_id + timestamps() end @@ -136,8 +143,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) @@ -147,8 +154,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..50ab6c59 --- /dev/null +++ b/apps/elven_database/lib/elven_database/players/item.ex @@ -0,0 +1,70 @@ +defmodule ElvenDatabase.Players.Item do + @moduledoc false + + use Ecto.Schema + + import Ecto.Changeset + + require ElvenData.Enums.ItemEnums, as: ItemEnums + + alias __MODULE__ + alias ElvenDatabase.Players.Character + + @type id :: non_neg_integer() + @type t :: %Item{ + id: id(), + 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(), + # Ecto fields + __meta__: Ecto.Schema.Metadata.t(), + inserted_at: any(), + updated_at: any() + } + + ## 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{} = item, attrs) do + attrs = + case attrs do + %{slot: slot} when is_atom(slot) -> Map.put(attrs, :slot, ItemEnums.slot_type(slot)) + attrs -> attrs + end + + attrs = + case attrs do + %{owner: %Character{} = owner} -> Map.put(attrs, :owner_id, owner.id) + attrs -> attrs + end + + item + |> cast(attrs, @fields) + |> foreign_key_constraint(:owner_id) + |> 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 new file mode 100644 index 00000000..b8ac6984 --- /dev/null +++ b/apps/elven_database/lib/elven_database/players/items.ex @@ -0,0 +1,76 @@ +defmodule ElvenDatabase.Players.Items do + @moduledoc """ + TODO: Documentation + """ + + import Ecto.Query, only: [from: 2] + + 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, Item.t()} | {:error, Ecto.Changeset.t()} + def create(attrs) do + %Item{} + |> Item.changeset(attrs) + |> Repo.insert() + end + + @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, 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, Item.t()} | {:error, Ecto.Changeset.t()} + def update!(%Item{} = item, attrs) do + item + |> Item.changeset(attrs) + |> Repo.update!() + end + + @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, Item.t()} | {:error, Ecto.Changeset.t()} + def delete!(%Item{} = item) do + Repo.delete!(item) + end +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/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..17e8393f --- /dev/null +++ b/apps/elven_database/priv/repo/migrations/20240820084419_create_items.exs @@ -0,0 +1,25 @@ +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 + + timestamps() + end + + create unique_index(:items, [:owner_id, :inventory_type, :slot], name: :owner_inventory_slot) + end +end diff --git a/apps/elven_database/priv/repo/seeds.exs b/apps/elven_database/priv/repo/seeds.exs index d792e5c3..4b83d6fc 100644 --- a/apps/elven_database/priv/repo/seeds.exs +++ b/apps/elven_database/priv/repo/seeds.exs @@ -10,7 +10,12 @@ # 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, + Characters, + Items +} ## Accounts @@ -29,7 +34,7 @@ alias ElvenDatabase.Players.{Account, Accounts, Characters} ## Characters -Characters.create!(%{ +admin_char = Characters.create!(%{ account_id: admin_id, slot: 1, name: "DarkyZ", @@ -57,7 +62,7 @@ Characters.create!(%{ compliment: 500 }) -Characters.create!(%{ +user_char = Characters.create!(%{ account_id: user_id, slot: 0, name: "ExampleUser", @@ -76,3 +81,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 <- [admin_char, user_char] do + item + |> 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 new file mode 100644 index 00000000..da674272 --- /dev/null +++ b/apps/elven_database/test/elven_database/players/items_test.exs @@ -0,0 +1,350 @@ +defmodule ElvenDatabase.Players.ItemsTest do + use ElvenDatabase.RepoCase, async: true + + alias ElvenDatabase.Players.{Accounts, Characters, Item, Items} + + ## Setup + + setup ctx do + accounts_count = Map.get(ctx, :accounts, 1) + characters_count = Map.get(ctx, :characters, 1) + + # 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 + %{accounts: accounts, characters: characters} + end + + ## Tests + + 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 {: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, + 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", %{characters: [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 + + @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 + assert {:ok, _item} = Items.create(attrs) + + # 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 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)