Wednesday, July 15, 2026 5:00:46 AM
> territory_info_spec.rb
# frozen_string_literal: true

RSpec.describe ESM::Website::API::Handlers::TerritoryInfo do
  describe ".call", :requires_connection do
    include_context "connection"

    # The player making the request. Whether this uid appears in the territory's
    # owner_uid/build_rights/moderators is exactly what the access scope keys on,
    # so each context below places it in a different column (or none).
    let(:steam_uid) { Faker::Steam.uid }

    # Exile always seeds the owner into both lists when a flag is placed, so a real
    # territory never has an empty member set. Mirror that: an empty
    # build_rights + moderators would make the name-lookup query build `IN ()`,
    # which is a MySQL syntax error rather than a returned row.
    let(:owner_uid) { Faker::Steam.uid }
    let(:moderators) { [owner_uid] }
    let(:build_rights) { [owner_uid] }

    let!(:territory) do
      create(
        :exile_territory,
        owner_uid:,
        moderators:,
        build_rights:,
        server_id: server.id
      )
    end

    def call
      described_class.call(
        server_id: server.id,
        encoded_territory_id: territory.encoded_id,
        steam_uid:
      )
    end

    shared_examples "resolves to the territory" do
      it "resolves to the territory data" do
        promise = call
        expect(promise).to be_a(Concurrent::Promise)

        # Round-trips through Arma: proves the uid was accepted by the access scope
        # and the encoded id decoded/re-encoded symmetrically.
        data = promise.value!(10).to_istruct
        expect(data.id).to eq(territory.encoded_id)
        expect(data.territory_name).to eq(territory.name)
      end
    end

    context "when the requesting player owns the territory" do
      let(:owner_uid) { steam_uid }

      include_examples "resolves to the territory"
    end

    context "when the requesting player has build rights" do
      let(:build_rights) { [steam_uid] }

      include_examples "resolves to the territory"
    end

    context "when the requesting player is a moderator" do
      let(:moderators) { [steam_uid] }

      include_examples "resolves to the territory"
    end

    context "when the requesting player has no rights to the territory" do
      # steam_uid is in none of owner_uid/build_rights/moderators, so the scoped
      # query returns zero rows and the lookup is denied. This is the whole point
      # of threading requesting_uid through: an arbitrary id can't be fetched.
      it "resolves to nil" do
        expect(call.value!(10)).to be_nil
      end
    end

    context "when the territory has no build rights or moderators" do
      # A degenerate row: the owner is absent from both member lists. This can't
      # happen in real Exile (the owner is seeded into both), but the invariant
      # isn't enforced on our side, so the name lookup must not build `IN ()` and
      # crash. It should return the territory with no members. Guards
      # create_name_lookup (see command_player_territories.rs).
      let(:owner_uid) { steam_uid }
      let(:moderators) { [] }
      let(:build_rights) { [] }

      it "resolves to the territory instead of erroring" do
        data = call.value!(10).to_istruct
        expect(data.id).to eq(territory.encoded_id)
        expect(data.build_rights).to be_empty
        expect(data.moderators).to be_empty
      end
    end
  end

  describe ".call" do
    context "when the server does not exist" do
      it "raises ArgumentError without offloading" do
        expect {
          described_class.call(
            server_id: "does-not-exist",
            encoded_territory_id: "anything",
            steam_uid: Faker::Steam.uid
          )
        }.to raise_error(ArgumentError, /Unknown server/)
      end
    end
  end
end
All opinions represented herein are my own
- © 2024 - 2026 itsthedevman
- build 43aa0d7