mirror of
https://github.com/wanderer-industries/wanderer
synced 2026-01-13 18:30:33 +00:00
Compare commits
13 Commits
advent-cha
...
v1.90.12
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52d90361e9 | ||
|
|
1c902d3319 | ||
|
|
8f671a359b | ||
|
|
840c416684 | ||
|
|
56e29ad30a | ||
|
|
cd8f8b5801 | ||
|
|
70e013fa3d | ||
|
|
d6bfaf8008 | ||
|
|
95944199a0 | ||
|
|
3bd5db8cf3 | ||
|
|
a245330ada | ||
|
|
e5afa1d5bc | ||
|
|
1473fe8646 |
@@ -16,3 +16,8 @@ export WANDERER_SSE_ENABLED="true"
|
||||
export WANDERER_WEBHOOKS_ENABLED="true"
|
||||
export WANDERER_SSE_MAX_CONNECTIONS="1000"
|
||||
export WANDERER_WEBHOOK_TIMEOUT_MS="15000"
|
||||
|
||||
# Promo codes for map subscriptions (optional)
|
||||
# Format: CODE:DISCOUNT_PERCENT,CODE2:DISCOUNT_PERCENT2
|
||||
# Codes are case-insensitive, discounts stack with period discounts
|
||||
# export WANDERER_PROMO_CODES="PROMO2025:10,NEWUSER:20"
|
||||
|
||||
28
CHANGELOG.md
28
CHANGELOG.md
@@ -2,6 +2,34 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.90.12](https://github.com/wanderer-industries/wanderer/compare/v1.90.11...v1.90.12) (2025-12-19)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed permissions update after character corp updates
|
||||
|
||||
## [v1.90.11](https://github.com/wanderer-industries/wanderer/compare/v1.90.10...v1.90.11) (2025-12-18)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.90.10](https://github.com/wanderer-industries/wanderer/compare/v1.90.9...v1.90.10) (2025-12-18)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.90.9](https://github.com/wanderer-industries/wanderer/compare/v1.90.8...v1.90.9) (2025-12-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: reduce chracters untrack grace period to 15 mins (after change/close/disconnect from map)
|
||||
|
||||
## [v1.90.8](https://github.com/wanderer-industries/wanderer/compare/v1.90.7...v1.90.8) (2025-12-15)
|
||||
|
||||
|
||||
|
||||
@@ -92,6 +92,31 @@ map_subscription_extra_hubs_10_price =
|
||||
config_dir
|
||||
|> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_EXTRA_HUBS_10_PRICE", 10_000_000)
|
||||
|
||||
# Parse promo codes from environment variable
|
||||
# Format: "CODE1:10,CODE2:20" where numbers are discount percentages
|
||||
promo_codes =
|
||||
config_dir
|
||||
|> get_var_from_path_or_env("WANDERER_PROMO_CODES", "")
|
||||
|> case do
|
||||
"" ->
|
||||
%{}
|
||||
|
||||
codes_string ->
|
||||
codes_string
|
||||
|> String.split(",")
|
||||
|> Enum.map(fn entry ->
|
||||
case String.split(String.trim(entry), ":") do
|
||||
[code, discount] ->
|
||||
{String.upcase(String.trim(code)), String.to_integer(String.trim(discount))}
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|> Map.new()
|
||||
end
|
||||
|
||||
map_connection_auto_expire_hours =
|
||||
config_dir
|
||||
|> get_int_from_path_or_env("WANDERER_MAP_CONNECTION_AUTO_EXPIRE_HOURS", 24)
|
||||
@@ -176,7 +201,8 @@ config :wanderer_app,
|
||||
}
|
||||
],
|
||||
extra_characters_50: map_subscription_extra_characters_50_price,
|
||||
extra_hubs_10: map_subscription_extra_hubs_10_price
|
||||
extra_hubs_10: map_subscription_extra_hubs_10_price,
|
||||
promo_codes: promo_codes
|
||||
},
|
||||
# Finch pool configuration - separate pools for different services
|
||||
# ESI Character Tracking pool - high capacity for bulk character operations
|
||||
|
||||
@@ -731,6 +731,14 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:character_alliance, {character_id, character_update}}
|
||||
)
|
||||
|
||||
# Broadcast permission update to trigger LiveView refresh
|
||||
# This ensures users are kicked off maps they no longer have access to
|
||||
@pubsub_client.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character.eve_id}",
|
||||
:update_permissions
|
||||
)
|
||||
|
||||
state
|
||||
|> Map.merge(%{alliance_id: nil})
|
||||
end
|
||||
@@ -769,6 +777,14 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:character_alliance, {character_id, character_update}}
|
||||
)
|
||||
|
||||
# Broadcast permission update to trigger LiveView refresh
|
||||
# This ensures users are kicked off maps they no longer have access to
|
||||
@pubsub_client.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character.eve_id}",
|
||||
:update_permissions
|
||||
)
|
||||
|
||||
state
|
||||
|> Map.merge(%{alliance_id: alliance_id})
|
||||
|
||||
@@ -823,6 +839,14 @@ defmodule WandererApp.Character.Tracker do
|
||||
}}}
|
||||
)
|
||||
|
||||
# Broadcast permission update to trigger LiveView refresh
|
||||
# This ensures users are kicked off maps they no longer have access to
|
||||
@pubsub_client.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character.eve_id}",
|
||||
:update_permissions
|
||||
)
|
||||
|
||||
state
|
||||
|> Map.merge(%{corporation_id: corporation_id})
|
||||
|
||||
|
||||
@@ -42,6 +42,35 @@ defmodule WandererApp.Env do
|
||||
def corp_eve_id(), do: get_key(:corp_id, -1)
|
||||
def subscription_settings(), do: get_key(:subscription_settings)
|
||||
|
||||
@doc """
|
||||
Returns the promo code configuration map.
|
||||
Keys are uppercase code strings, values are discount percentages.
|
||||
"""
|
||||
def promo_codes() do
|
||||
case subscription_settings() do
|
||||
%{promo_codes: codes} when is_map(codes) -> codes
|
||||
_ -> %{}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Validates a promo code and returns the discount percentage.
|
||||
Returns {:ok, discount_percent} if valid, {:error, :invalid_code} otherwise.
|
||||
Codes are case-insensitive.
|
||||
"""
|
||||
def validate_promo_code(nil), do: {:error, :invalid_code}
|
||||
def validate_promo_code(""), do: {:error, :invalid_code}
|
||||
|
||||
def validate_promo_code(code) when is_binary(code) do
|
||||
normalized = String.upcase(String.trim(code))
|
||||
|
||||
case Map.get(promo_codes(), normalized) do
|
||||
nil -> {:error, :invalid_code}
|
||||
discount when is_integer(discount) and discount > 0 and discount <= 100 -> {:ok, discount}
|
||||
_ -> {:error, :invalid_code}
|
||||
end
|
||||
end
|
||||
|
||||
@decorate cacheable(
|
||||
cache: WandererApp.Cache,
|
||||
key: "restrict_maps_creation"
|
||||
|
||||
@@ -7,7 +7,8 @@ defmodule WandererApp.EveDataService do
|
||||
|
||||
alias WandererApp.Utils.JSONUtil
|
||||
|
||||
@eve_db_dump_url "https://www.fuzzwork.co.uk/dump/latest"
|
||||
# @eve_db_dump_url "https://www.fuzzwork.co.uk/dump/latest"
|
||||
@eve_db_dump_url "https://wanderer-industries.github.io/wanderer-assets/sde-files"
|
||||
|
||||
@dump_file_names [
|
||||
"invGroups.csv",
|
||||
|
||||
@@ -23,7 +23,10 @@ defmodule WandererApp.Map do
|
||||
characters_limit: nil,
|
||||
hubs_limit: nil
|
||||
|
||||
def new(%{id: map_id, name: name, scope: scope, owner_id: owner_id, acls: acls, hubs: hubs} = input) do
|
||||
def new(
|
||||
%{id: map_id, name: name, scope: scope, owner_id: owner_id, acls: acls, hubs: hubs} =
|
||||
input
|
||||
) do
|
||||
# Extract the new scopes array field if present (nil if not set)
|
||||
scopes = Map.get(input, :scopes)
|
||||
|
||||
|
||||
@@ -72,28 +72,36 @@ defmodule WandererApp.Map.SubscriptionManager do
|
||||
:ok
|
||||
end
|
||||
|
||||
def estimate_price(params, renew?, promo_code \\ nil)
|
||||
|
||||
def estimate_price(
|
||||
%{
|
||||
"period" => period,
|
||||
"characters_limit" => characters_limit,
|
||||
"hubs_limit" => hubs_limit
|
||||
},
|
||||
renew?
|
||||
} = params,
|
||||
renew?,
|
||||
promo_code
|
||||
)
|
||||
when is_binary(characters_limit),
|
||||
do:
|
||||
estimate_price(
|
||||
%{
|
||||
period: period |> String.to_integer(),
|
||||
characters_limit: characters_limit |> String.to_integer(),
|
||||
hubs_limit: hubs_limit |> String.to_integer()
|
||||
},
|
||||
renew?
|
||||
)
|
||||
when is_binary(characters_limit) do
|
||||
# Extract promo_code from params if passed there (from form)
|
||||
promo_code = promo_code || Map.get(params, "promo_code")
|
||||
|
||||
estimate_price(
|
||||
%{
|
||||
period: period |> String.to_integer(),
|
||||
characters_limit: characters_limit |> String.to_integer(),
|
||||
hubs_limit: hubs_limit |> String.to_integer()
|
||||
},
|
||||
renew?,
|
||||
promo_code
|
||||
)
|
||||
end
|
||||
|
||||
def estimate_price(
|
||||
%{characters_limit: characters_limit, hubs_limit: hubs_limit} = params,
|
||||
renew?
|
||||
renew?,
|
||||
promo_code
|
||||
) do
|
||||
%{
|
||||
plans: plans,
|
||||
@@ -136,7 +144,7 @@ defmodule WandererApp.Map.SubscriptionManager do
|
||||
|
||||
total_price = estimated_price * period
|
||||
|
||||
{:ok, discount} =
|
||||
{:ok, period_discount} =
|
||||
calc_discount(
|
||||
period,
|
||||
total_price,
|
||||
@@ -144,13 +152,27 @@ defmodule WandererApp.Map.SubscriptionManager do
|
||||
renew?
|
||||
)
|
||||
|
||||
{:ok, total_price, discount}
|
||||
# Calculate promo discount on price after period discount
|
||||
price_after_period_discount = total_price - period_discount
|
||||
|
||||
{:ok, promo_discount, promo_valid?} =
|
||||
calc_promo_discount(promo_code, price_after_period_discount)
|
||||
|
||||
total_discount = period_discount + promo_discount
|
||||
|
||||
{:ok, total_price, total_discount, promo_valid?}
|
||||
end
|
||||
|
||||
def calc_additional_price(params, selected_subscription, promo_code \\ nil)
|
||||
|
||||
def calc_additional_price(
|
||||
%{"characters_limit" => characters_limit, "hubs_limit" => hubs_limit},
|
||||
selected_subscription
|
||||
%{"characters_limit" => characters_limit, "hubs_limit" => hubs_limit} = params,
|
||||
selected_subscription,
|
||||
promo_code
|
||||
) do
|
||||
# Extract promo_code from params if passed there (from form)
|
||||
promo_code = promo_code || Map.get(params, "promo_code")
|
||||
|
||||
%{
|
||||
plans: plans,
|
||||
extra_characters_50: extra_characters_50,
|
||||
@@ -189,7 +211,7 @@ defmodule WandererApp.Map.SubscriptionManager do
|
||||
|
||||
total_price = additional_price * period
|
||||
|
||||
{:ok, discount} =
|
||||
{:ok, period_discount} =
|
||||
calc_discount(
|
||||
period,
|
||||
total_price,
|
||||
@@ -197,7 +219,15 @@ defmodule WandererApp.Map.SubscriptionManager do
|
||||
false
|
||||
)
|
||||
|
||||
{:ok, total_price, discount}
|
||||
# Calculate promo discount on price after period discount
|
||||
price_after_period_discount = total_price - period_discount
|
||||
|
||||
{:ok, promo_discount, promo_valid?} =
|
||||
calc_promo_discount(promo_code, price_after_period_discount)
|
||||
|
||||
total_discount = period_discount + promo_discount
|
||||
|
||||
{:ok, total_price, total_discount, promo_valid?}
|
||||
end
|
||||
|
||||
defp get_active_months(subscription) do
|
||||
@@ -255,6 +285,22 @@ defmodule WandererApp.Map.SubscriptionManager do
|
||||
when period >= 3,
|
||||
do: {:ok, round(total_price * month_3_discount)}
|
||||
|
||||
# Calculates the promo code discount amount.
|
||||
# Returns {:ok, discount_amount, is_valid?}
|
||||
defp calc_promo_discount(nil, _price), do: {:ok, 0, false}
|
||||
defp calc_promo_discount("", _price), do: {:ok, 0, false}
|
||||
|
||||
defp calc_promo_discount(promo_code, price) when is_binary(promo_code) do
|
||||
case WandererApp.Env.validate_promo_code(promo_code) do
|
||||
{:ok, discount_percent} ->
|
||||
discount_amount = round(price * discount_percent / 100)
|
||||
{:ok, discount_amount, true}
|
||||
|
||||
{:error, :invalid_code} ->
|
||||
{:ok, 0, false}
|
||||
end
|
||||
end
|
||||
|
||||
def get_balance(map) do
|
||||
map
|
||||
|> WandererApp.MapRepo.load_relationships([
|
||||
@@ -302,7 +348,8 @@ defmodule WandererApp.Map.SubscriptionManager do
|
||||
|
||||
defp renew_subscription(%{auto_renew?: true, map: map} = subscription)
|
||||
when is_map(subscription) do
|
||||
with {:ok, estimated_price, discount} <- estimate_price(subscription, true),
|
||||
# No promo code for auto-renewals, ignore the promo_valid? return value
|
||||
with {:ok, estimated_price, discount, _promo_valid?} <- estimate_price(subscription, true),
|
||||
{:ok, map_balance} <- get_balance(map) do
|
||||
case map_balance >= estimated_price do
|
||||
true ->
|
||||
|
||||
@@ -569,6 +569,9 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
end
|
||||
)
|
||||
|
||||
# Broadcast permission update to trigger LiveView refresh
|
||||
broadcast_permission_update(character_id)
|
||||
|
||||
:has_update
|
||||
|
||||
{:character_corporation, _info} ->
|
||||
@@ -580,6 +583,9 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
end
|
||||
)
|
||||
|
||||
# Broadcast permission update to trigger LiveView refresh
|
||||
broadcast_permission_update(character_id)
|
||||
|
||||
:has_update
|
||||
|
||||
_ ->
|
||||
@@ -953,4 +959,21 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
track: true
|
||||
})
|
||||
end
|
||||
|
||||
# Broadcasts permission update to trigger LiveView refresh for the character's user.
|
||||
# This is called when a character's corporation or alliance changes, ensuring
|
||||
# users are kicked off maps they no longer have access to.
|
||||
defp broadcast_permission_update(character_id) do
|
||||
case WandererApp.Character.get_character(character_id) do
|
||||
{:ok, %{eve_id: eve_id}} when not is_nil(eve_id) ->
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{eve_id}",
|
||||
:update_permissions
|
||||
)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -31,11 +31,7 @@
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<.new_version_banner
|
||||
app_version={@app_version}
|
||||
enabled={true}
|
||||
latest_post={@latest_post}
|
||||
/>
|
||||
<.new_version_banner app_version={@app_version} enabled={true} latest_post={@latest_post} />
|
||||
</div>
|
||||
|
||||
<.live_component module={WandererAppWeb.Alerts} id="notifications" view_flash={@flash} />
|
||||
|
||||
@@ -117,43 +117,48 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div :if={length(@members) > @members_per_page} class="flex items-center justify-between px-3 py-2 border-t border-gray-500 bg-black bg-opacity-25">
|
||||
<span class="text-sm text-gray-400">
|
||||
Page {@members_page} of {total_pages(@members, @members_per_page)} ({length(@members)} members)
|
||||
</span>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
phx-click="members_prev_page"
|
||||
disabled={@members_page <= 1}
|
||||
class={"btn btn-sm btn-ghost " <> if(@members_page <= 1, do: "btn-disabled", else: "")}
|
||||
>
|
||||
<.icon name="hero-chevron-left" class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
phx-click="members_next_page"
|
||||
disabled={@members_page >= total_pages(@members, @members_per_page)}
|
||||
class={"btn btn-sm btn-ghost " <> if(@members_page >= total_pages(@members, @members_per_page), do: "btn-disabled", else: "")}
|
||||
>
|
||||
<.icon name="hero-chevron-right" class="w-4 h-4" />
|
||||
</button>
|
||||
<div
|
||||
:if={length(@members) > @members_per_page}
|
||||
class="flex items-center justify-between px-3 py-2 border-t border-gray-500 bg-black bg-opacity-25"
|
||||
>
|
||||
<span class="text-sm text-gray-400">
|
||||
Page {@members_page} of {total_pages(@members, @members_per_page)} ({length(
|
||||
@members
|
||||
)} members)
|
||||
</span>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
phx-click="members_prev_page"
|
||||
disabled={@members_page <= 1}
|
||||
class={"btn btn-sm btn-ghost " <> if(@members_page <= 1, do: "btn-disabled", else: "")}
|
||||
>
|
||||
<.icon name="hero-chevron-left" class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
phx-click="members_next_page"
|
||||
disabled={@members_page >= total_pages(@members, @members_per_page)}
|
||||
class={"btn btn-sm btn-ghost " <> if(@members_page >= total_pages(@members, @members_per_page), do: "btn-disabled", else: "")}
|
||||
>
|
||||
<.icon name="hero-chevron-right" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<.link
|
||||
:if={@selected_acl_id != "" and can_add_members?(@access_list, @current_user)}
|
||||
class="btn w-full btn-neutral rounded-none"
|
||||
patch={~p"/access-lists/#{@selected_acl_id}/add-members"}
|
||||
>
|
||||
<.icon name="hero-plus-solid" class="w-6 h-6" />
|
||||
<h3 class="card-title text-center text-md">Add Members</h3>
|
||||
</.link>
|
||||
<div
|
||||
:if={@selected_acl_id == "" or not can_add_members?(@access_list, @current_user)}
|
||||
class="btn mt-2 w-full btn-neutral rounded-none btn-disabled"
|
||||
>
|
||||
<.icon name="hero-plus-solid" class="w-6 h-6" />
|
||||
<h3 class="card-title text-center text-md">Add Members</h3>
|
||||
</div>
|
||||
<.link
|
||||
:if={@selected_acl_id != "" and can_add_members?(@access_list, @current_user)}
|
||||
class="btn w-full btn-neutral rounded-none"
|
||||
patch={~p"/access-lists/#{@selected_acl_id}/add-members"}
|
||||
>
|
||||
<.icon name="hero-plus-solid" class="w-6 h-6" />
|
||||
<h3 class="card-title text-center text-md">Add Members</h3>
|
||||
</.link>
|
||||
<div
|
||||
:if={@selected_acl_id == "" or not can_add_members?(@access_list, @current_user)}
|
||||
class="btn mt-2 w-full btn-neutral rounded-none btn-disabled"
|
||||
>
|
||||
<.icon name="hero-plus-solid" class="w-6 h-6" />
|
||||
<h3 class="card-title text-center text-md">Add Members</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -179,10 +184,10 @@
|
||||
placeholder="Select an owner"
|
||||
options={Enum.map(@characters, fn character -> {character.label, character.id} end)}
|
||||
/>
|
||||
|
||||
|
||||
<!-- Divider between above inputs and the API key section -->
|
||||
<hr class="my-4 border-gray-600" />
|
||||
|
||||
|
||||
<!-- API Key Section with grid layout -->
|
||||
<div class="mt-2">
|
||||
<label class="block text-sm font-medium text-gray-200 mb-1">ACL API key</label>
|
||||
|
||||
@@ -15,6 +15,9 @@ defmodule WandererAppWeb.Maps.MapSubscriptionsComponent do
|
||||
is_adding_subscription?: false,
|
||||
map_subscriptions: [],
|
||||
selected_subscription: nil,
|
||||
promo_code: "",
|
||||
promo_code_valid?: false,
|
||||
promo_code_error: nil,
|
||||
subscription_periods: [
|
||||
{"1 Month", "1"},
|
||||
{"3 Months", "3"},
|
||||
@@ -34,12 +37,13 @@ defmodule WandererAppWeb.Maps.MapSubscriptionsComponent do
|
||||
"period" => "1",
|
||||
"characters_limit" => "50",
|
||||
"hubs_limit" => "20",
|
||||
"auto_renew?" => true
|
||||
"auto_renew?" => true,
|
||||
"promo_code" => ""
|
||||
}
|
||||
|
||||
{:ok, map} = WandererApp.MapRepo.get(map_id)
|
||||
|
||||
{:ok, estimated_price, discount} =
|
||||
{:ok, estimated_price, discount, _promo_valid?} =
|
||||
SubscriptionManager.estimate_price(subscription_form, false)
|
||||
|
||||
{:ok, map_subscriptions} =
|
||||
@@ -53,7 +57,10 @@ defmodule WandererAppWeb.Maps.MapSubscriptionsComponent do
|
||||
map_subscriptions: map_subscriptions,
|
||||
subscription_form: subscription_form |> to_form(),
|
||||
estimated_price: estimated_price,
|
||||
discount: discount
|
||||
discount: discount,
|
||||
promo_code: "",
|
||||
promo_code_valid?: false,
|
||||
promo_code_error: nil
|
||||
)
|
||||
|
||||
{:ok, socket}
|
||||
@@ -73,10 +80,11 @@ defmodule WandererAppWeb.Maps.MapSubscriptionsComponent do
|
||||
"plan" => "omega",
|
||||
"characters_limit" => "#{selected_subscription.characters_limit}",
|
||||
"hubs_limit" => "#{selected_subscription.hubs_limit}",
|
||||
"auto_renew?" => selected_subscription.auto_renew?
|
||||
"auto_renew?" => selected_subscription.auto_renew?,
|
||||
"promo_code" => ""
|
||||
}
|
||||
|
||||
{:ok, additional_price, discount} =
|
||||
{:ok, additional_price, discount, _promo_valid?} =
|
||||
SubscriptionManager.calc_additional_price(
|
||||
subscription_form,
|
||||
selected_subscription
|
||||
@@ -89,6 +97,9 @@ defmodule WandererAppWeb.Maps.MapSubscriptionsComponent do
|
||||
selected_subscription: selected_subscription,
|
||||
additional_price: additional_price,
|
||||
discount: discount,
|
||||
promo_code: "",
|
||||
promo_code_valid?: false,
|
||||
promo_code_error: nil,
|
||||
subscription_form: subscription_form |> to_form()
|
||||
)}
|
||||
end
|
||||
@@ -142,23 +153,46 @@ defmodule WandererAppWeb.Maps.MapSubscriptionsComponent do
|
||||
params,
|
||||
%{assigns: %{selected_subscription: selected_subscription}} = socket
|
||||
) do
|
||||
promo_code = Map.get(params, "promo_code", "")
|
||||
|
||||
# Validate promo code and set error message
|
||||
{promo_code_valid?, promo_code_error} =
|
||||
case WandererApp.Env.validate_promo_code(promo_code) do
|
||||
{:ok, _discount} -> {true, nil}
|
||||
{:error, :invalid_code} when promo_code != "" -> {false, "Invalid promo code"}
|
||||
_ -> {false, nil}
|
||||
end
|
||||
|
||||
socket =
|
||||
case is_nil(selected_subscription) do
|
||||
true ->
|
||||
{:ok, estimated_price, discount} =
|
||||
{:ok, estimated_price, discount, _valid?} =
|
||||
WandererApp.Map.SubscriptionManager.estimate_price(params, false)
|
||||
|
||||
socket
|
||||
|> assign(estimated_price: estimated_price, discount: discount)
|
||||
|> assign(
|
||||
estimated_price: estimated_price,
|
||||
discount: discount,
|
||||
promo_code: promo_code,
|
||||
promo_code_valid?: promo_code_valid?,
|
||||
promo_code_error: promo_code_error
|
||||
)
|
||||
|
||||
_ ->
|
||||
{:ok, additional_price, discount} =
|
||||
{:ok, additional_price, discount, _valid?} =
|
||||
WandererApp.Map.SubscriptionManager.calc_additional_price(
|
||||
params,
|
||||
selected_subscription
|
||||
)
|
||||
|
||||
socket |> assign(additional_price: additional_price, discount: discount)
|
||||
socket
|
||||
|> assign(
|
||||
additional_price: additional_price,
|
||||
discount: discount,
|
||||
promo_code: promo_code,
|
||||
promo_code_valid?: promo_code_valid?,
|
||||
promo_code_error: promo_code_error
|
||||
)
|
||||
end
|
||||
|
||||
{:noreply, assign(socket, subscription_form: params)}
|
||||
@@ -176,8 +210,9 @@ defmodule WandererAppWeb.Maps.MapSubscriptionsComponent do
|
||||
%{assigns: %{map_id: map_id, map: map, current_user: current_user}} = socket
|
||||
) do
|
||||
period = period |> String.to_integer()
|
||||
promo_code = Map.get(subscription_form, "promo_code", "")
|
||||
|
||||
{:ok, estimated_price, discount} =
|
||||
{:ok, estimated_price, discount, _promo_valid?} =
|
||||
WandererApp.Map.SubscriptionManager.estimate_price(subscription_form, false)
|
||||
|
||||
active_till =
|
||||
@@ -219,7 +254,8 @@ defmodule WandererAppWeb.Maps.MapSubscriptionsComponent do
|
||||
|
||||
:telemetry.execute([:wanderer_app, :map, :subscription, :new], %{count: 1}, %{
|
||||
map_id: map_id,
|
||||
amount: estimated_price - discount
|
||||
amount: estimated_price - discount,
|
||||
promo_code: if(promo_code != "", do: String.upcase(promo_code), else: nil)
|
||||
})
|
||||
|
||||
# Automatically create a license for the map
|
||||
@@ -266,7 +302,7 @@ defmodule WandererAppWeb.Maps.MapSubscriptionsComponent do
|
||||
}
|
||||
} = socket
|
||||
) do
|
||||
{:ok, additional_price, discount} =
|
||||
{:ok, additional_price, discount, _promo_valid?} =
|
||||
WandererApp.Map.SubscriptionManager.calc_additional_price(
|
||||
subscription_form,
|
||||
selected_subscription
|
||||
@@ -537,6 +573,17 @@ defmodule WandererAppWeb.Maps.MapSubscriptionsComponent do
|
||||
class="range range-xs"
|
||||
/>
|
||||
<.input field={f[:auto_renew?]} label="Auto Renew" type="checkbox" />
|
||||
<div :if={is_nil(@selected_subscription)} class="mt-2">
|
||||
<.input
|
||||
field={f[:promo_code]}
|
||||
label="Promo Code (optional)"
|
||||
type="text"
|
||||
placeholder="Enter promo code"
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
<p :if={@promo_code_error} class="text-rose-500 text-xs mt-1">{@promo_code_error}</p>
|
||||
<p :if={@promo_code_valid?} class="text-green-500 text-xs mt-1">✓ Promo code applied!</p>
|
||||
</div>
|
||||
<div
|
||||
:if={is_nil(@selected_subscription)}
|
||||
class="stats w-full bg-primary text-primary-content mt-2"
|
||||
@@ -556,7 +603,12 @@ defmodule WandererAppWeb.Maps.MapSubscriptionsComponent do
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="stat-title">Discount</div>
|
||||
<div class="stat-title">
|
||||
Discount
|
||||
<span :if={@promo_code_valid?} class="text-xs text-green-400 ml-1">
|
||||
(incl. promo)
|
||||
</span>
|
||||
</div>
|
||||
<div class="stat-value text-white relative">
|
||||
ISK {@discount
|
||||
|> Number.to_human(units: ["", "K", "M", "B", "T", "P"])}
|
||||
|
||||
2
mix.exs
2
mix.exs
@@ -3,7 +3,7 @@ defmodule WandererApp.MixProject do
|
||||
|
||||
@source_url "https://github.com/wanderer-industries/wanderer"
|
||||
|
||||
@version "1.90.8"
|
||||
@version "1.90.12"
|
||||
|
||||
def project do
|
||||
[
|
||||
|
||||
@@ -186,7 +186,9 @@ defmodule WandererApp.AclMemberCacheInvalidationTest do
|
||||
|
||||
# Verify cache was invalidated
|
||||
cached_data_after = WandererApp.Cache.lookup!(cache_key)
|
||||
assert is_nil(cached_data_after), "Cache should be invalidated after adding corporation member"
|
||||
|
||||
assert is_nil(cached_data_after),
|
||||
"Cache should be invalidated after adding corporation member"
|
||||
end
|
||||
|
||||
@tag :integration
|
||||
|
||||
355
test/integration/map/corporation_change_permission_test.exs
Normal file
355
test/integration/map/corporation_change_permission_test.exs
Normal file
@@ -0,0 +1,355 @@
|
||||
defmodule WandererApp.Map.CorporationChangePermissionTest do
|
||||
@moduledoc """
|
||||
Integration tests for permission revocation when a character's corporation changes.
|
||||
|
||||
This tests the fix for the issue where:
|
||||
- A user is granted map access via corporation-based ACL membership
|
||||
- The user's character leaves or changes corporation
|
||||
- The user could still see the map until they logged out
|
||||
|
||||
The fix ensures that when a character's corporation changes:
|
||||
1. An :update_permissions broadcast is sent to the character's LiveView connections
|
||||
2. The LiveView triggers a permission refresh
|
||||
3. If access is revoked, the user is redirected away from the map
|
||||
|
||||
Related files:
|
||||
- lib/wanderer_app/character/tracker.ex (broadcasts on corp change)
|
||||
- lib/wanderer_app/map/server/map_server_characters_impl.ex (backup broadcast)
|
||||
- lib/wanderer_app_web/live/map/event_handlers/map_core_event_handler.ex (handles broadcast)
|
||||
"""
|
||||
|
||||
use WandererApp.DataCase, async: false
|
||||
|
||||
alias WandererAppWeb.Factory
|
||||
|
||||
import Mox
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
@test_corp_id_a 98000001
|
||||
@test_corp_id_b 98000002
|
||||
@test_alliance_id_a 99000001
|
||||
|
||||
setup do
|
||||
# Configure the PubSubMock to forward to real Phoenix.PubSub for broadcast testing
|
||||
Test.PubSubMock
|
||||
|> Mox.stub(:broadcast!, fn server, topic, message ->
|
||||
Phoenix.PubSub.broadcast!(server, topic, message)
|
||||
end)
|
||||
|> Mox.stub(:broadcast, fn server, topic, message ->
|
||||
Phoenix.PubSub.broadcast(server, topic, message)
|
||||
end)
|
||||
|> Mox.stub(:subscribe, fn server, topic ->
|
||||
Phoenix.PubSub.subscribe(server, topic)
|
||||
end)
|
||||
|> Mox.stub(:unsubscribe, fn server, topic ->
|
||||
Phoenix.PubSub.unsubscribe(server, topic)
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
describe "PubSub broadcast on corporation change" do
|
||||
test "broadcasts :update_permissions to character channel when corporation update is simulated" do
|
||||
# Create test data
|
||||
user = Factory.create_user()
|
||||
|
||||
character =
|
||||
Factory.create_character(%{
|
||||
user_id: user.id,
|
||||
corporation_id: @test_corp_id_a,
|
||||
corporation_name: "Test Corp A",
|
||||
corporation_ticker: "TCPA"
|
||||
})
|
||||
|
||||
# Subscribe to the character's channel (this is what LiveView does via tracking_utils.ex)
|
||||
Phoenix.PubSub.subscribe(WandererApp.PubSub, "character:#{character.eve_id}")
|
||||
|
||||
# Simulate what happens in tracker.ex when a corporation change is detected
|
||||
# This simulates the fix: broadcasting :update_permissions after corp change
|
||||
simulate_corporation_change(character, @test_corp_id_b)
|
||||
|
||||
# Should receive :update_permissions broadcast
|
||||
assert_receive :update_permissions, 1000,
|
||||
"Should receive :update_permissions when corporation changes"
|
||||
end
|
||||
|
||||
test "broadcasts :update_permissions to character channel when alliance update is simulated" do
|
||||
# Create test data
|
||||
user = Factory.create_user()
|
||||
|
||||
character =
|
||||
Factory.create_character(%{
|
||||
user_id: user.id,
|
||||
corporation_id: @test_corp_id_a,
|
||||
alliance_id: @test_alliance_id_a,
|
||||
alliance_name: "Test Alliance A",
|
||||
alliance_ticker: "TALA"
|
||||
})
|
||||
|
||||
# Subscribe to the character's channel
|
||||
Phoenix.PubSub.subscribe(WandererApp.PubSub, "character:#{character.eve_id}")
|
||||
|
||||
# Simulate what happens when alliance is removed
|
||||
simulate_alliance_removal(character)
|
||||
|
||||
# Should receive :update_permissions broadcast
|
||||
assert_receive :update_permissions, 1000,
|
||||
"Should receive :update_permissions when alliance is removed"
|
||||
end
|
||||
end
|
||||
|
||||
describe "Corporation-based ACL permission verification" do
|
||||
test "character with corp A has access to map with corp A ACL" do
|
||||
# Setup: Create a map with corporation-based ACL
|
||||
owner_user = Factory.create_user()
|
||||
owner = Factory.create_character(%{user_id: owner_user.id})
|
||||
|
||||
map =
|
||||
Factory.create_map(%{
|
||||
owner_id: owner.id,
|
||||
name: "Corp Access Test Map",
|
||||
slug: "corp-access-test-#{:rand.uniform(1_000_000)}"
|
||||
})
|
||||
|
||||
# Create ACL that grants access to corporation A
|
||||
acl = Factory.create_access_list(owner.id, %{name: "Corp A Access"})
|
||||
_map_acl = Factory.create_map_access_list(map.id, acl.id)
|
||||
|
||||
_corp_member =
|
||||
Factory.create_access_list_member(acl.id, %{
|
||||
eve_corporation_id: "#{@test_corp_id_a}",
|
||||
name: "Corporation A",
|
||||
role: "member"
|
||||
})
|
||||
|
||||
# Create user with character in corp A
|
||||
test_user = Factory.create_user()
|
||||
|
||||
test_character =
|
||||
Factory.create_character(%{
|
||||
user_id: test_user.id,
|
||||
corporation_id: @test_corp_id_a,
|
||||
corporation_name: "Test Corp A",
|
||||
corporation_ticker: "TCPA"
|
||||
})
|
||||
|
||||
# Verify character has access via corporation membership
|
||||
{:ok, map_with_acls} =
|
||||
WandererApp.MapRepo.get(map.id,
|
||||
acls: [
|
||||
:owner_id,
|
||||
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
|
||||
]
|
||||
)
|
||||
|
||||
[character_permissions] =
|
||||
WandererApp.Permissions.check_characters_access([test_character], map_with_acls.acls)
|
||||
|
||||
map_permissions =
|
||||
WandererApp.Permissions.get_map_permissions(
|
||||
character_permissions,
|
||||
owner.id,
|
||||
[test_character.id]
|
||||
)
|
||||
|
||||
assert map_permissions.view_system == true,
|
||||
"Character in corp A should have view_system permission"
|
||||
end
|
||||
|
||||
test "character in corp B does not have access to map with corp A ACL" do
|
||||
# Setup: Create a map with corporation-based ACL for corp A
|
||||
owner_user = Factory.create_user()
|
||||
owner = Factory.create_character(%{user_id: owner_user.id})
|
||||
|
||||
map =
|
||||
Factory.create_map(%{
|
||||
owner_id: owner.id,
|
||||
name: "CorpB Test",
|
||||
slug: "corp-access-test-2-#{:rand.uniform(1_000_000)}"
|
||||
})
|
||||
|
||||
# Create ACL that grants access only to corporation A
|
||||
acl = Factory.create_access_list(owner.id, %{name: "Corp A Only Access"})
|
||||
_map_acl = Factory.create_map_access_list(map.id, acl.id)
|
||||
|
||||
_corp_member =
|
||||
Factory.create_access_list_member(acl.id, %{
|
||||
eve_corporation_id: "#{@test_corp_id_a}",
|
||||
name: "Corporation A",
|
||||
role: "member"
|
||||
})
|
||||
|
||||
# Create user with character in corp B (not A)
|
||||
test_user = Factory.create_user()
|
||||
|
||||
test_character =
|
||||
Factory.create_character(%{
|
||||
user_id: test_user.id,
|
||||
corporation_id: @test_corp_id_b,
|
||||
corporation_name: "Test Corp B",
|
||||
corporation_ticker: "TCPB"
|
||||
})
|
||||
|
||||
# Verify character does NOT have access
|
||||
{:ok, map_with_acls} =
|
||||
WandererApp.MapRepo.get(map.id,
|
||||
acls: [
|
||||
:owner_id,
|
||||
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
|
||||
]
|
||||
)
|
||||
|
||||
[character_permissions] =
|
||||
WandererApp.Permissions.check_characters_access([test_character], map_with_acls.acls)
|
||||
|
||||
map_permissions =
|
||||
WandererApp.Permissions.get_map_permissions(
|
||||
character_permissions,
|
||||
owner.id,
|
||||
[test_character.id]
|
||||
)
|
||||
|
||||
assert map_permissions.view_system == false,
|
||||
"Character in corp B should NOT have view_system permission for corp A map"
|
||||
end
|
||||
|
||||
test "permission check result changes when character changes from corp A to corp B" do
|
||||
# Setup: Create a map with corporation-based ACL
|
||||
owner_user = Factory.create_user()
|
||||
owner = Factory.create_character(%{user_id: owner_user.id})
|
||||
|
||||
map =
|
||||
Factory.create_map(%{
|
||||
owner_id: owner.id,
|
||||
name: "Corp Change Test Map",
|
||||
slug: "corp-change-test-#{:rand.uniform(1_000_000)}"
|
||||
})
|
||||
|
||||
# Create ACL that grants access to corporation A
|
||||
acl = Factory.create_access_list(owner.id, %{name: "Corp A Access"})
|
||||
_map_acl = Factory.create_map_access_list(map.id, acl.id)
|
||||
|
||||
_corp_member =
|
||||
Factory.create_access_list_member(acl.id, %{
|
||||
eve_corporation_id: "#{@test_corp_id_a}",
|
||||
name: "Corporation A",
|
||||
role: "member"
|
||||
})
|
||||
|
||||
# Create user with character initially in corp A
|
||||
test_user = Factory.create_user()
|
||||
|
||||
test_character =
|
||||
Factory.create_character(%{
|
||||
user_id: test_user.id,
|
||||
corporation_id: @test_corp_id_a,
|
||||
corporation_name: "Test Corp A",
|
||||
corporation_ticker: "TCPA"
|
||||
})
|
||||
|
||||
{:ok, map_with_acls} =
|
||||
WandererApp.MapRepo.get(map.id,
|
||||
acls: [
|
||||
:owner_id,
|
||||
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
|
||||
]
|
||||
)
|
||||
|
||||
# Verify initial access
|
||||
[initial_permissions] =
|
||||
WandererApp.Permissions.check_characters_access([test_character], map_with_acls.acls)
|
||||
|
||||
initial_map_permissions =
|
||||
WandererApp.Permissions.get_map_permissions(
|
||||
initial_permissions,
|
||||
owner.id,
|
||||
[test_character.id]
|
||||
)
|
||||
|
||||
assert initial_map_permissions.view_system == true,
|
||||
"Initially character in corp A should have view_system permission"
|
||||
|
||||
# Now simulate the character changing corporation
|
||||
# Update the character's corporation in the database
|
||||
character_update = %{
|
||||
corporation_id: @test_corp_id_b,
|
||||
corporation_name: "Test Corp B",
|
||||
corporation_ticker: "TCPB"
|
||||
}
|
||||
|
||||
{:ok, updated_character} =
|
||||
WandererApp.Api.Character.update_corporation(test_character, character_update)
|
||||
|
||||
WandererApp.Character.update_character(test_character.id, character_update)
|
||||
|
||||
# Verify character no longer has access after corporation change
|
||||
[new_permissions] =
|
||||
WandererApp.Permissions.check_characters_access([updated_character], map_with_acls.acls)
|
||||
|
||||
new_map_permissions =
|
||||
WandererApp.Permissions.get_map_permissions(
|
||||
new_permissions,
|
||||
owner.id,
|
||||
[updated_character.id]
|
||||
)
|
||||
|
||||
assert new_map_permissions.view_system == false,
|
||||
"After changing to corp B, character should NOT have view_system permission"
|
||||
end
|
||||
end
|
||||
|
||||
# Helper functions that simulate what the tracker does
|
||||
|
||||
defp simulate_corporation_change(character, new_corporation_id) do
|
||||
# Update character in database
|
||||
character_update = %{
|
||||
corporation_id: new_corporation_id,
|
||||
corporation_name: "Test Corp B",
|
||||
corporation_ticker: "TCPB"
|
||||
}
|
||||
|
||||
{:ok, _} = WandererApp.Api.Character.update_corporation(character, character_update)
|
||||
WandererApp.Character.update_character(character.id, character_update)
|
||||
|
||||
# Broadcast corporation change (existing behavior)
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character.id}:corporation",
|
||||
{:character_corporation, {character.id, character_update}}
|
||||
)
|
||||
|
||||
# Broadcast permission update (THE FIX)
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character.eve_id}",
|
||||
:update_permissions
|
||||
)
|
||||
end
|
||||
|
||||
defp simulate_alliance_removal(character) do
|
||||
# Update character in database
|
||||
character_update = %{
|
||||
alliance_id: nil,
|
||||
alliance_name: nil,
|
||||
alliance_ticker: nil
|
||||
}
|
||||
|
||||
{:ok, _} = WandererApp.Api.Character.update_alliance(character, character_update)
|
||||
WandererApp.Character.update_character(character.id, character_update)
|
||||
|
||||
# Broadcast alliance change (existing behavior)
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character.id}:alliance",
|
||||
{:character_alliance, {character.id, character_update}}
|
||||
)
|
||||
|
||||
# Broadcast permission update (THE FIX)
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character.eve_id}",
|
||||
:update_permissions
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -237,7 +237,10 @@ defmodule WandererApp.Map.MapScopeFilteringTest do
|
||||
|
||||
# old_location is also high-sec (k-space to k-space)
|
||||
old_location = %{solar_system_id: @hs_system_mili}
|
||||
result = SystemsImpl.maybe_add_system("map_id", location, old_location, [], [:wormholes, :null])
|
||||
|
||||
result =
|
||||
SystemsImpl.maybe_add_system("map_id", location, old_location, [], [:wormholes, :null])
|
||||
|
||||
assert result == :ok
|
||||
end
|
||||
|
||||
@@ -245,7 +248,10 @@ defmodule WandererApp.Map.MapScopeFilteringTest do
|
||||
location = %{solar_system_id: @ls_system_halmah}
|
||||
# old_location is high-sec (k-space to k-space)
|
||||
old_location = %{solar_system_id: @hs_system_halenan}
|
||||
result = SystemsImpl.maybe_add_system("map_id", location, old_location, [], [:wormholes, :null])
|
||||
|
||||
result =
|
||||
SystemsImpl.maybe_add_system("map_id", location, old_location, [], [:wormholes, :null])
|
||||
|
||||
assert result == :ok
|
||||
end
|
||||
|
||||
|
||||
@@ -230,7 +230,8 @@ defmodule WandererApp.Map.Server.MapScopesTest do
|
||||
assert ConnectionsImpl.is_connection_valid([:null], @hs_system_id, @ls_system_id) == false
|
||||
|
||||
# HS to LS with [:hi, :low]: valid (both match)
|
||||
assert ConnectionsImpl.is_connection_valid([:hi, :low], @hs_system_id, @ls_system_id) == true
|
||||
assert ConnectionsImpl.is_connection_valid([:hi, :low], @hs_system_id, @ls_system_id) ==
|
||||
true
|
||||
|
||||
# HS to HS: valid with [:hi] (both match)
|
||||
assert ConnectionsImpl.is_connection_valid([:hi], @hs_system_id, 30_000_002) == true
|
||||
@@ -356,13 +357,15 @@ defmodule WandererApp.Map.Server.MapScopesTest do
|
||||
test "WORMHOLE BORDER: WH->Hi-Sec with [:wormholes] is VALID (border k-space added)" do
|
||||
# Border case: moving from WH to k-space
|
||||
# Valid because :wormholes enabled AND one system is WH
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @wh_system_id, @hs_system_id) == true
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @wh_system_id, @hs_system_id) ==
|
||||
true
|
||||
end
|
||||
|
||||
test "WORMHOLE BORDER: Hi-Sec->WH with [:wormholes] is VALID (border k-space added)" do
|
||||
# Border case: moving from k-space to WH
|
||||
# Valid because :wormholes enabled AND one system is WH
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @hs_system_id, @wh_system_id) == true
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @hs_system_id, @wh_system_id) ==
|
||||
true
|
||||
end
|
||||
|
||||
test "K-SPACE ONLY: Hi-Sec->Hi-Sec with [:wormholes] is REJECTED" do
|
||||
@@ -373,13 +376,21 @@ defmodule WandererApp.Map.Server.MapScopesTest do
|
||||
test "K-SPACE ONLY: Null->Hi-Sec with [:wormholes, :null] is REJECTED (no border for k-space)" do
|
||||
# Neither system is a wormhole, so no border behavior
|
||||
# Null matches :null, but Hi-Sec doesn't match any scope -> BOTH must match
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes, :null], @ns_system_id, @hs_system_id) ==
|
||||
assert ConnectionsImpl.is_connection_valid(
|
||||
[:wormholes, :null],
|
||||
@ns_system_id,
|
||||
@hs_system_id
|
||||
) ==
|
||||
false
|
||||
end
|
||||
|
||||
test "K-SPACE ONLY: Hi-Sec->Low-Sec with [:wormholes, :null] is REJECTED" do
|
||||
# Neither Hi-Sec nor Low-Sec match [:wormholes, :null], no WH involved
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes, :null], @hs_system_id, @ls_system_id) ==
|
||||
assert ConnectionsImpl.is_connection_valid(
|
||||
[:wormholes, :null],
|
||||
@hs_system_id,
|
||||
@ls_system_id
|
||||
) ==
|
||||
false
|
||||
end
|
||||
|
||||
@@ -402,7 +413,11 @@ defmodule WandererApp.Map.Server.MapScopesTest do
|
||||
|
||||
test "WORMHOLE BORDER: Pochven->WH with [:wormholes, :pochven] is VALID" do
|
||||
# WH is wormhole, :wormholes enabled -> border behavior applies
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes, :pochven], @pochven_id, @wh_system_id) ==
|
||||
assert ConnectionsImpl.is_connection_valid(
|
||||
[:wormholes, :pochven],
|
||||
@pochven_id,
|
||||
@wh_system_id
|
||||
) ==
|
||||
true
|
||||
end
|
||||
|
||||
@@ -414,9 +429,12 @@ defmodule WandererApp.Map.Server.MapScopesTest do
|
||||
test "border systems: WH->Hi-Sec->WH path with [:wormholes] scope" do
|
||||
# Simulates a character path through k-space between WHs
|
||||
# First jump: WH to Hi-Sec - valid (wormhole border)
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @wh_system_id, @hs_system_id) == true
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @wh_system_id, @hs_system_id) ==
|
||||
true
|
||||
|
||||
# Second jump: Hi-Sec to WH - valid (wormhole border)
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @hs_system_id, @c2_system_id) == true
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @hs_system_id, @c2_system_id) ==
|
||||
true
|
||||
end
|
||||
|
||||
test "excluded path: k-space chain with [:wormholes] scope remains excluded" do
|
||||
@@ -428,10 +446,19 @@ defmodule WandererApp.Map.Server.MapScopesTest do
|
||||
test "excluded path: Null->Hi-Sec->Low-Sec with [:wormholes, :null] - only Null tracked" do
|
||||
# Character in Null (tracked) jumps to Hi-Sec (border - but NO wormhole!) -> REJECTED
|
||||
# This is the key case: k-space to k-space should NOT add border systems
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes, :null], @ns_system_id, @hs_system_id) ==
|
||||
assert ConnectionsImpl.is_connection_valid(
|
||||
[:wormholes, :null],
|
||||
@ns_system_id,
|
||||
@hs_system_id
|
||||
) ==
|
||||
false
|
||||
|
||||
# Hi-Sec to Low-Sec also rejected (neither matches)
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes, :null], @hs_system_id, @ls_system_id) ==
|
||||
assert ConnectionsImpl.is_connection_valid(
|
||||
[:wormholes, :null],
|
||||
@hs_system_id,
|
||||
@ls_system_id
|
||||
) ==
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user