Creating Summary Records

- 3 mins read

Returning to my previous test for the unfinished get_active_games_for_user function, I update the test to reflect the changes with game_id and the introduction of a join_code.

  describe "get_active_games_for_user/1 when user has active games" do
    setup [:create_user, :join_games]

    test "should return a list of user's active game ids", ctx do
      active_game_codes = [
        ctx.join_code1,
        ctx.join_code2
      ]

      result = GameEngine.get_active_games_for_user(ctx.user.id)

      assert [game_summary1, game_summary2] = result
      assert Enum.member?(active_game_codes, game_summary1.join_code)
      assert Enum.member?(active_game_codes, game_summary2.join_code)
    end
  end

I update get_active_games_for_user with an Ecto query to find the records based on how I expect them to exist in the database even though there is not yet any logic for creating records.

  def get_active_games_for_user(user_id) do
    from(g in GameSessionSummary,
      join: p in PlayerGameSessionSummary,
      on: g.id == p.game_session_id and p.user_id == ^user_id,
      where: g.game_status == :active
    )
    |> select([g], %{join_code: g.join_code})
    |> Repo.all()
  end

The test is still failing since I need to add the functionality which creates GameSessionSummary records in the database when the session process is started. However, I will first need to create a new migration to update the changes with game_id since it is now a UUID in the code, but the length for this column in the database table is only 8 characters. I also need to add a join_code column which will be in the return value for get_active_games_for_user. There aren’t any records in the game_session_summaries table, so the game_id column type can just be converted directly. However, I do get an error when trying to make this conversion:

(Postgrex.Error) ERROR 42804 (datatype_mismatch) column "game_id" cannot be cast automatically to type uuid

    hint: You might need to specify "USING game_id::uuid"

I change my migration to execute raw SQL so I can follow the suggestion from PostgreSQL.

defmodule Minotaur.Repo.Migrations.ChangeGameIdToUuid do
  use Ecto.Migration

  def change do
    execute(
      "ALTER TABLE game_session_summaries ALTER COLUMN game_id TYPE uuid USING (game_id::uuid)",
      "ALTER TABLE game_session_summaries ALTER COLUMN game_id TYPE VARCHAR(8)"
    )

    alter table(:game_session_summaries) do
      add :join_code, :string, size: 8, null: false
    end
  end
end

I also update the schema definition in the application code to add join_id and change game_id type.

defmodule Minotaur.GameEngine.GameSessionSummary do
  # …

  schema "game_session_summaries" do
    field :game_id, :binary_id
    field :join_code, :string
    field :game_status, Ecto.Enum, values: [:active, :concluded, :canceled]
    field :latest_round, :integer

    timestamps()

    has_many :player_game_session_summaries, PlayerGameSessionSummary,
      foreign_key: :game_session_id
  end
end

I update the init callback for game session processes so a GameSessionSummary record is created at startup.

defmodule Minotaur.GameEngine.SessionServer do
  # …

  def init({%Game{} = game, join_code}) do
    Process.flag(:trap_exit, true)
    Horde.Registry.register(SessionJoinCodeRegistry, join_code, self())

    game_state = get_stashed_state(game.id, game)
    update_summary_record(game_state, join_code)

    {:ok, {game_state, join_code}, {:continue, :continue_game}}
  end
 
  defp update_summary_record(game, join_code) do
    attrs = %{
      game_id: game.id,
      join_code: join_code,
      game_status: :active,
      latest_round: game.round,
      player_summaries:
        game.players
        |> Enum.map(fn {_, player} ->
          %PlayerGameSessionSummary{
            user_id: player.user_id,
            player_status: player.status
          }
        end)
    }

    %GameSessionSummary{}
    |> GameSessionSummary.changeset(attrs)
    |> Repo.insert()
  end
end

This inserts a new GameSessionSummary record as well as a PlayerSessionSummaryRecord for each player in the game. The test case is passing!

Ecto sandbox connection pool modes

Game session processes now interact with the database which is causing some of my existing tests to fail. This is due to the default generated Ecto sandbox configuration which checks out a database connection and sets the calling process as the owner. The connection is only shared with other processes if the async: true tag is not set by the test module.

  def setup_sandbox(tags) do
    pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Minotaur.Repo, shared: not tags[:async])
    on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
  end

Since these failing tests need to have a shared connection between the process running the test and the spawned game session server, the test module cannot be run asynchronously with the other tests in the suite. If they were to run with a shared connection in async mode, other concurrently running tests would also share the connection which would mean the tests are no longer guaranteed to have an isolated database state.