Compare commits

...

16 Commits

Author SHA1 Message Date
Dmitry Popov
1226b6abf3 chore: added advent challenge 2025-12-18 19:04:43 +01:00
Dmitry Popov
7a1f5c0966 chore: [skip ci] 2025-12-17 19:32:37 +01:00
Dmitry Popov
7039ced11e fix(core): reduce chracters untrack grace period to 15 mins (after change/close/disconnect from map)
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
2025-12-15 12:46:02 +01:00
CI
42b5bb337f chore: [skip ci] 2025-12-15 11:35:24 +00:00
CI
1dbb24f6ec chore: release version v1.90.8 2025-12-15 11:35:24 +00:00
Dmitry Popov
c242f510e0 fix(core): skip systems or connections cleanup for not started maps 2025-12-15 12:34:55 +01:00
CI
c59d51636e chore: [skip ci] 2025-12-15 00:36:18 +00:00
CI
c5a8aa1b4d chore: release version v1.90.7 2025-12-15 00:36:18 +00:00
Dmitry Popov
cba050a9e7 fix(core): fixed scopes 2025-12-15 01:35:41 +01:00
CI
59fcbef3b1 chore: [skip ci] 2025-12-12 18:49:02 +00:00
CI
2f1eb6eeaa chore: release version v1.90.6 2025-12-12 18:49:02 +00:00
Dmitry Popov
71ae326cf7 fix(core): fixed map scopes 2025-12-12 19:48:26 +01:00
CI
07829caf0f chore: [skip ci] 2025-12-12 18:36:03 +00:00
CI
a5850b5a8d chore: release version v1.90.5 2025-12-12 18:36:03 +00:00
Dmitry Popov
9f6849209b fix(core): fixed map scopes 2025-12-12 19:35:26 +01:00
CI
7bd295cbad chore: [skip ci] 2025-12-12 17:07:55 +00:00
18 changed files with 802 additions and 42 deletions

View File

@@ -2,6 +2,42 @@
<!-- changelog -->
## [v1.90.8](https://github.com/wanderer-industries/wanderer/compare/v1.90.7...v1.90.8) (2025-12-15)
### Bug Fixes:
* core: skip systems or connections cleanup for not started maps
## [v1.90.7](https://github.com/wanderer-industries/wanderer/compare/v1.90.6...v1.90.7) (2025-12-15)
### Bug Fixes:
* core: fixed scopes
## [v1.90.6](https://github.com/wanderer-industries/wanderer/compare/v1.90.5...v1.90.6) (2025-12-12)
### Bug Fixes:
* core: fixed map scopes
## [v1.90.5](https://github.com/wanderer-industries/wanderer/compare/v1.90.4...v1.90.5) (2025-12-12)
### Bug Fixes:
* core: fixed map scopes
## [v1.90.4](https://github.com/wanderer-industries/wanderer/compare/v1.90.3...v1.90.4) (2025-12-12)

View File

@@ -57,7 +57,7 @@ export default {
};
refreshZone.addEventListener('click', handleUpdate);
refreshZone.addEventListener('mouseover', handleUpdate);
// refreshZone.addEventListener('mouseover', handleUpdate);
this.updated();
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -12,6 +12,7 @@ defmodule WandererApp.Map do
defstruct map_id: nil,
name: nil,
scope: :none,
scopes: nil,
owner_id: nil,
characters: [],
systems: Map.new(),
@@ -22,11 +23,15 @@ 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}) 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)
map =
struct!(__MODULE__,
map_id: map_id,
scope: scope,
scopes: scopes,
owner_id: owner_id,
name: name,
acls: acls,

View File

@@ -822,16 +822,25 @@ defmodule WandererApp.Map.Server.CharactersImpl do
) do
scopes = get_effective_scopes(map)
ConnectionsImpl.is_connection_valid(
scopes,
old_location.solar_system_id,
location.solar_system_id
is_valid =
ConnectionsImpl.is_connection_valid(
scopes,
old_location.solar_system_id,
location.solar_system_id
)
Logger.debug(
"[CharacterTracking] update_location: map=#{map_id}, " <>
"from=#{old_location.solar_system_id}, to=#{location.solar_system_id}, " <>
"scopes=#{inspect(scopes)}, map.scopes=#{inspect(map[:scopes])}, " <>
"map.scope=#{inspect(map[:scope])}, is_valid=#{is_valid}"
)
|> case do
case is_valid do
true ->
# Connection is valid (at least one system matches scopes)
# Add BOTH systems including border systems - filtering already done by is_connection_valid
case SystemsImpl.maybe_add_system(map_id, location, old_location, map_opts) do
# Add systems that match the map's scopes - individual system filtering by maybe_add_system
case SystemsImpl.maybe_add_system(map_id, location, old_location, map_opts, scopes) do
:ok ->
:ok
@@ -841,8 +850,8 @@ defmodule WandererApp.Map.Server.CharactersImpl do
)
end
# Add old location system (in case it wasn't on map)
case SystemsImpl.maybe_add_system(map_id, old_location, location, map_opts) do
# Add old location system (in case it wasn't on map) - only if it matches scopes
case SystemsImpl.maybe_add_system(map_id, old_location, location, map_opts, scopes) do
:ok ->
:ok
@@ -882,13 +891,16 @@ defmodule WandererApp.Map.Server.CharactersImpl do
defp is_character_in_space?(%{station_id: station_id, structure_id: structure_id} = _location),
do: is_nil(structure_id) && is_nil(station_id)
# Get effective scopes from map, with fallback to legacy scope
defp get_effective_scopes(%{scopes: scopes}) when is_list(scopes) and scopes != [], do: scopes
@doc """
Get effective scopes from map, with fallback to legacy scope.
Returns the scopes array that should be used for filtering.
"""
def get_effective_scopes(%{scopes: scopes}) when is_list(scopes) and scopes != [], do: scopes
defp get_effective_scopes(%{scope: scope}) when is_atom(scope),
def get_effective_scopes(%{scope: scope}) when is_atom(scope),
do: legacy_scope_to_scopes(scope)
defp get_effective_scopes(_), do: [:wormholes]
def get_effective_scopes(_), do: [:wormholes]
# Legacy scope to new scopes array conversion
defp legacy_scope_to_scopes(:wormholes), do: [:wormholes]

View File

@@ -296,6 +296,30 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
do: update_connection(map_id, :update_custom_info, [:custom_info], connection_update)
def cleanup_connections(map_id) do
# Defensive check: Skip cleanup if cache appears invalid
# This prevents incorrectly deleting connections when cache is empty due to
# race conditions during map restart or cache corruption
case WandererApp.Map.get_map(map_id) do
{:error, :not_found} ->
Logger.warning(
"[cleanup_connections] Skipping map #{map_id} - cache miss detected, " <>
"map data not found in cache"
)
:telemetry.execute(
[:wanderer_app, :map, :cleanup_connections, :cache_miss],
%{system_time: System.system_time()},
%{map_id: map_id}
)
:ok
{:ok, _map} ->
do_cleanup_connections(map_id)
end
end
defp do_cleanup_connections(map_id) do
connection_auto_expire_hours = get_connection_auto_expire_hours()
connection_auto_eol_hours = get_connection_auto_eol_hours()
connection_eol_expire_timeout_hours = get_eol_expire_timeout_mins() / 60

View File

@@ -256,6 +256,37 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
defp maybe_update_connection_mass_status(_map_id, _old_sig, _updated_sig), do: :ok
@doc """
Wrapper for updating a signature's linked_system_id with logging.
Logs all unlink operations (when linked_system_id is set to nil) with context
to help diagnose unexpected unlinking issues.
"""
def update_signature_linked_system(signature, %{linked_system_id: nil} = params) do
# Log all unlink operations with context for debugging
Logger.warning(
"[Signature Unlink] eve_id=#{signature.eve_id} " <>
"system_id=#{signature.system_id} " <>
"old_linked_system_id=#{signature.linked_system_id} " <>
"stacktrace=#{format_stacktrace()}"
)
MapSystemSignature.update_linked_system(signature, params)
end
def update_signature_linked_system(signature, params) do
MapSystemSignature.update_linked_system(signature, params)
end
defp format_stacktrace do
{:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace)
stacktrace
|> Enum.take(10)
|> Enum.map_join(" <- ", fn {mod, fun, arity, _} ->
"#{inspect(mod)}.#{fun}/#{arity}"
end)
end
defp track_activity(event, map_id, solar_system_id, user_id, character_id, signatures) do
ActivityTracker.track_map_event(event, %{
map_id: map_id,

View File

@@ -4,6 +4,7 @@ defmodule WandererApp.Map.Server.SystemsImpl do
require Logger
alias WandererApp.Map.Server.Impl
alias WandererApp.Map.Server.SignaturesImpl
@ddrt Application.compile_env(:wanderer_app, :ddrt)
@system_auto_expire_minutes 15
@@ -146,6 +147,30 @@ defmodule WandererApp.Map.Server.SystemsImpl do
end
def cleanup_systems(map_id) do
# Defensive check: Skip cleanup if cache appears invalid
# This prevents incorrectly deleting systems when cache is empty due to
# race conditions during map restart or cache corruption
case WandererApp.Map.get_map(map_id) do
{:error, :not_found} ->
Logger.warning(
"[cleanup_systems] Skipping map #{map_id} - cache miss detected, " <>
"map data not found in cache"
)
:telemetry.execute(
[:wanderer_app, :map, :cleanup_systems, :cache_miss],
%{system_time: System.system_time()},
%{map_id: map_id}
)
:ok
{:ok, _map} ->
do_cleanup_systems(map_id)
end
end
defp do_cleanup_systems(map_id) do
expired_systems =
map_id
|> WandererApp.Map.list_systems!()
@@ -423,7 +448,8 @@ defmodule WandererApp.Map.Server.SystemsImpl do
{:ok, %{eve_id: eve_id, system: system}} = sig |> Ash.load([:system])
# Clear the linked_system_id instead of destroying the signature
case WandererApp.Api.MapSystemSignature.update_linked_system(sig, %{
# Use the wrapper to log unlink operations
case SignaturesImpl.update_signature_linked_system(sig, %{
linked_system_id: nil
}) do
{:ok, _updated_sig} ->
@@ -506,10 +532,25 @@ defmodule WandererApp.Map.Server.SystemsImpl do
# Check if the system matches the map's configured scopes before adding
should_add =
case scopes do
nil -> true
[] -> true
nil ->
true
[] ->
true
scopes when is_list(scopes) ->
ConnectionsImpl.can_add_location(scopes, location.solar_system_id)
# First check: does the location directly match scopes?
if ConnectionsImpl.can_add_location(scopes, location.solar_system_id) do
true
else
# Second check: wormhole border behavior
# If :wormholes scope is enabled AND old_location is a wormhole,
# allow this system to be added as a border system (so you can see
# where your wormhole exits to)
:wormholes in scopes and
not is_nil(old_location) and
ConnectionsImpl.can_add_location([:wormholes], old_location.solar_system_id)
end
end
if should_add do

View File

@@ -23,6 +23,7 @@ defmodule WandererAppWeb.Layouts do
attr :app_version, :string
attr :enabled, :boolean
attr :latest_post, :any, default: nil
def new_version_banner(assigns) do
~H"""
@@ -36,27 +37,89 @@ defmodule WandererAppWeb.Layouts do
>
<div class="hs-overlay-backdrop transition duration absolute left-0 top-0 w-full h-full bg-gray-900 bg-opacity-50 dark:bg-opacity-80 dark:bg-neutral-900">
</div>
<div class="absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] flex items-center">
<div class="rounded w-9 h-9 w-[80px] h-[66px] flex items-center justify-center relative z-20">
<.icon name="hero-chevron-double-right" class="w-9 h-9 mr-[-40px]" />
</div>
<div id="refresh-area">
<.live_component module={WandererAppWeb.MapRefresh} id="map-refresh" />
<div class="absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] flex flex-col items-center gap-6">
<div class="flex items-center">
<div class="rounded w-9 h-9 w-[80px] h-[66px] flex items-center justify-center relative z-20">
<.icon name="hero-chevron-double-right" class="w-9 h-9 mr-[-40px]" />
</div>
<div id="refresh-area">
<.live_component module={WandererAppWeb.MapRefresh} id="map-refresh" />
</div>
<div class="rounded h-[66px] flex items-center justify-center relative z-20">
<div class=" flex items-center w-[200px] h-full">
<.icon name="hero-chevron-double-left" class="w-9 h-9 mr-[20px]" />
<div class=" flex flex-col items-center justify-center h-full">
<div class="text-white text-nowrap text-sm [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">
Update Required
</div>
<a
href="/changelog"
target="_blank"
class="text-sm link-secondary [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]"
>
What's new?
</a>
</div>
</div>
</div>
</div>
<div class="rounded h-[66px] flex items-center justify-center relative z-20">
<div class=" flex items-center w-[200px] h-full">
<.icon name="hero-chevron-double-left" class="w-9 h-9 mr-[20px]" />
<div class=" flex flex-col items-center justify-center h-full">
<div class="text-white text-nowrap text-sm [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">
Update Required
<div class="flex flex-row gap-6 z-20">
<div
:if={@latest_post}
class="bg-gray-800/80 rounded-lg overflow-hidden min-w-[300px] backdrop-blur-sm border border-gray-700"
>
<a href={"/news/#{@latest_post.id}"} target="_blank" class="block group/post">
<div class="relative">
<img
src={@latest_post.cover_image_uri}
class="w-[300px] h-[140px] object-cover opacity-80 group-hover/post:opacity-100 transition-opacity"
/>
<div class="absolute top-0 left-0 w-full h-full bg-gradient-to-b from-transparent to-black/70">
</div>
<div class="absolute top-2 left-2 flex items-center gap-1 bg-orange-500/90 px-2 py-0.5 rounded text-xs font-semibold">
<.icon name="hero-newspaper-solid" class="w-3 h-3" />
<span>Latest News</span>
</div>
<div class="absolute bottom-0 left-0 w-full p-3">
<% [first_part | rest] = String.split(@latest_post.title, ":", parts: 2) %>
<h3 class="text-white text-sm font-bold ccp-font [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">
{first_part}
</h3>
<p
:if={rest != []}
class="text-gray-200 text-xs ccp-font text-ellipsis overflow-hidden whitespace-nowrap [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]"
>
{List.first(rest)}
</p>
</div>
</div>
</a>
</div>
<div class="bg-gray-800/80 rounded-lg p-4 min-w-[280px] backdrop-blur-sm border border-gray-700">
<div class="flex items-center gap-2 mb-3">
<.icon name="hero-gift-solid" class="w-5 h-5 text-green-400" />
<span class="text-white font-semibold text-sm [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">
Support Wanderer
</span>
</div>
<div class="text-gray-300 text-xs mb-3 [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">
Buy PLEX from the official EVE Online store using our promocode to support the development.
</div>
<div class="flex items-center gap-3">
<code class="bg-gray-900/60 px-2 py-1 rounded text-green-400 text-sm font-mono border border-gray-600">
WANDERER
</code>
<a
href="/changelog"
href="https://www.eveonline.com/plex"
target="_blank"
class="text-sm link-secondary [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]"
rel="noopener noreferrer"
class="inline-flex items-center gap-1 text-sm text-green-400 hover:text-green-300 transition-colors"
>
What's new?
<span>Get PLEX</span>
<.icon name="hero-arrow-top-right-on-square-mini" class="w-4 h-4" />
</a>
</div>
</div>

View File

@@ -31,7 +31,11 @@
</div>
</aside>
<.new_version_banner app_version={@app_version} enabled={@map_subscriptions_enabled?} />
<.new_version_banner
app_version={@app_version}
enabled={true}
latest_post={@latest_post}
/>
</div>
<.live_component module={WandererAppWeb.Alerts} id="notifications" view_flash={@flash} />

View File

@@ -363,8 +363,8 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
linked_sig_eve_id: nil
})
s
|> WandererApp.Api.MapSystemSignature.update_linked_system(%{
# Use the wrapper to log unlink operations
WandererApp.Map.Server.SignaturesImpl.update_signature_linked_system(s, %{
linked_system_id: nil
})
end)

View File

@@ -16,6 +16,8 @@ defmodule WandererAppWeb.Nav do
show_admin =
socket.assigns.current_user_role == :admin
latest_post = WandererApp.Blog.recent_posts(1) |> List.first()
{:cont,
socket
|> attach_hook(:active_tab, :handle_params, &set_active_tab/3)
@@ -25,7 +27,8 @@ defmodule WandererAppWeb.Nav do
show_admin: show_admin,
show_sidebar: true,
map_subscriptions_enabled?: WandererApp.Env.map_subscriptions_enabled?(),
app_version: WandererApp.Env.vsn()
app_version: WandererApp.Env.vsn(),
latest_post: latest_post
)}
end

View File

@@ -25,8 +25,8 @@ defmodule WandererAppWeb.PresenceGracePeriodManager do
require Logger
# 1 hour grace period before removing disconnected characters
@grace_period_ms :timer.hours(1)
# 15 minutes grace period before removing disconnected characters
@grace_period_ms :timer.minutes(15)
defstruct pending_removals: %{}, timers: %{}

View File

@@ -3,7 +3,7 @@ defmodule WandererApp.MixProject do
@source_url "https://github.com/wanderer-industries/wanderer"
@version "1.90.4"
@version "1.90.8"
def project do
[

View File

@@ -0,0 +1,68 @@
%{
title: "Christmas Giveaway Challenge",
author: "Wanderer Team",
cover_image_uri: "/images/news/2025/12-18-advent-giveaway/cover.jpg",
tags: ~w(event giveaway challenge christmas advent partnership),
description: "Join our Advent Christmas Giveaway Challenge! Win exclusive partnership codes every day for a week. Be the fastest to claim your reward!"
}
---
![Christmas Giveaway Challenge](/images/news/2025/12-18-advent-giveaway/cover.jpg "Christmas Giveaway Challenge")
### The Season of Giving
This holiday season, we're spreading some festive cheer with a special event for our community: the **Advent Christmas Giveaway Challenge**!
Starting next week, we'll be giving away **1 exclusive partnership code every day for 7 days**. But here's the twist — it's a challenge!
---
### How It Works
1. **Daily Giveaway:**
- Every day during the event week, a partnership code will be revealed at a specific scheduled time.
- The exact reveal time will be announced for each day.
2. **The Challenge:**
- When the code is revealed, it becomes visible to **all participants** at the exact same moment.
- **First person to activate the code wins!**
- Speed and timing are everything.
3. **One Code Per Day:**
- Each day features a single partnership code.
- Miss today? Come back tomorrow for another chance!
---
### Event Details
- **Event Name:** Advent Christmas Giveaway
- **Duration:** 1 week (7 days, 7 codes)
- **Organizer:** @Demiro (Wanderer core developer, EventCortex CTO)
- **Event Link:** [Advent Christmas Giveaway - EventCortex](https://eventcortex.com/events/invite/cYdBywu1ygfVS3UN6ZZcmDzL1q85aDmH)
---
### Tips for Participants
- **Be Ready:** Know the reveal time and be online a few minutes early.
- **Stay Alert:** The code appears for everyone simultaneously — every second counts!
- **Keep Trying:** Didn't win today? There's always tomorrow's code.
---
### Why Participate?
Partnership codes can be redeemed in EVE Online for **exclusive partnership SKINs** — unique ship skins that let you fly in style! This is your chance to grab one for free — if you're fast enough!
Good luck, and may the fastest capsuleer win!
---
Fly safe and happy holidays,
**The Wanderer Team**
---

View File

@@ -0,0 +1,473 @@
defmodule WandererApp.Map.MapScopeFilteringTest do
@moduledoc """
Integration tests for map scope filtering during character location tracking.
These tests verify that systems are correctly filtered based on map scope settings
when characters move between systems. The key scenarios tested:
1. Characters moving between systems with [:wormholes, :null] scopes:
- Wormhole systems should be added
- Null-sec systems should be added
- High-sec systems should NOT be added (filtered out)
- Low-sec systems should NOT be added (filtered out)
2. Wormhole border behavior:
- When a character jumps from wormhole to k-space, the wormhole should be added
- K-space border systems should only be added if they match the scopes
3. K-space only movement:
- Characters moving within k-space should only track systems matching scopes
- No "border system" behavior for k-space to k-space movement
Reference bug: Characters with [:wormholes, :null] scopes were getting
high-sec (0.6) and low-sec (0.4) systems added to the map when traveling.
"""
use WandererApp.DataCase
# System class constants (matching ConnectionsImpl)
@c1 1
@c2 2
@hs 7
@ls 8
@ns 9
# Test solar system IDs
# C1 wormhole
@wh_system_j100001 31_000_001
# C2 wormhole
@wh_system_j100002 31_000_002
# High-sec system (0.6)
@hs_system_halenan 30_000_001
# High-sec system (0.6)
@hs_system_mili 30_000_002
# Low-sec system (0.4)
@ls_system_halmah 30_000_100
# Null-sec system
@ns_system_geminate 30_000_200
setup do
# Setup system static info cache with both wormhole and k-space systems
setup_scope_test_systems()
:ok
end
# Setup system static info for scope testing
defp setup_scope_test_systems do
test_systems = %{
# C1 Wormhole
@wh_system_j100001 => %{
solar_system_id: @wh_system_j100001,
solar_system_name: "J100001",
solar_system_name_lc: "j100001",
region_id: 11_000_001,
constellation_id: 21_000_001,
region_name: "A-R00001",
constellation_name: "A-C00001",
system_class: @c1,
security: "-1.0",
type_description: "Class 1",
class_title: "C1",
is_shattered: false,
effect_name: nil,
effect_power: nil,
statics: ["H121"],
wandering: [],
triglavian_invasion_status: nil,
sun_type_id: 45041
},
# C2 Wormhole
@wh_system_j100002 => %{
solar_system_id: @wh_system_j100002,
solar_system_name: "J100002",
solar_system_name_lc: "j100002",
region_id: 11_000_001,
constellation_id: 21_000_001,
region_name: "A-R00001",
constellation_name: "A-C00001",
system_class: @c2,
security: "-1.0",
type_description: "Class 2",
class_title: "C2",
is_shattered: false,
effect_name: nil,
effect_power: nil,
statics: ["D382", "L005"],
wandering: [],
triglavian_invasion_status: nil,
sun_type_id: 45041
},
# High-sec system (Halenan 0.6)
@hs_system_halenan => %{
solar_system_id: @hs_system_halenan,
solar_system_name: "Halenan",
solar_system_name_lc: "halenan",
region_id: 10_000_067,
constellation_id: 20_000_901,
region_name: "Devoid",
constellation_name: "Devoid",
system_class: @hs,
security: "0.6",
type_description: "High Security",
class_title: "High Sec",
is_shattered: false,
effect_name: nil,
effect_power: nil,
statics: [],
wandering: [],
triglavian_invasion_status: nil,
sun_type_id: 45041
},
# High-sec system (Mili 0.6)
@hs_system_mili => %{
solar_system_id: @hs_system_mili,
solar_system_name: "Mili",
solar_system_name_lc: "mili",
region_id: 10_000_067,
constellation_id: 20_000_901,
region_name: "Devoid",
constellation_name: "Devoid",
system_class: @hs,
security: "0.6",
type_description: "High Security",
class_title: "High Sec",
is_shattered: false,
effect_name: nil,
effect_power: nil,
statics: [],
wandering: [],
triglavian_invasion_status: nil,
sun_type_id: 45041
},
# Low-sec system (Halmah 0.4)
@ls_system_halmah => %{
solar_system_id: @ls_system_halmah,
solar_system_name: "Halmah",
solar_system_name_lc: "halmah",
region_id: 10_000_067,
constellation_id: 20_000_901,
region_name: "Devoid",
constellation_name: "Devoid",
system_class: @ls,
security: "0.4",
type_description: "Low Security",
class_title: "Low Sec",
is_shattered: false,
effect_name: nil,
effect_power: nil,
statics: [],
wandering: [],
triglavian_invasion_status: nil,
sun_type_id: 45041
},
# Null-sec system
@ns_system_geminate => %{
solar_system_id: @ns_system_geminate,
solar_system_name: "Geminate",
solar_system_name_lc: "geminate",
region_id: 10_000_029,
constellation_id: 20_000_400,
region_name: "Geminate",
constellation_name: "Geminate",
system_class: @ns,
security: "-0.5",
type_description: "Null Security",
class_title: "Null Sec",
is_shattered: false,
effect_name: nil,
effect_power: nil,
statics: [],
wandering: [],
triglavian_invasion_status: nil,
sun_type_id: 45041
}
}
Enum.each(test_systems, fn {solar_system_id, system_info} ->
Cachex.put(:system_static_info_cache, solar_system_id, system_info)
end)
:ok
end
describe "Scope filtering logic tests" do
# These tests verify the filtering logic without full integration
# The actual filtering is tested more comprehensively in map_scopes_test.exs
alias WandererApp.Map.Server.ConnectionsImpl
alias WandererApp.Map.Server.SystemsImpl
test "can_add_location correctly filters high-sec with [:wormholes, :null] scopes" do
# High-sec should NOT be allowed with [:wormholes, :null]
refute ConnectionsImpl.can_add_location([:wormholes, :null], @hs_system_halenan),
"High-sec should be filtered out with [:wormholes, :null] scopes"
refute ConnectionsImpl.can_add_location([:wormholes, :null], @hs_system_mili),
"High-sec should be filtered out with [:wormholes, :null] scopes"
end
test "can_add_location correctly filters low-sec with [:wormholes, :null] scopes" do
# Low-sec should NOT be allowed with [:wormholes, :null]
refute ConnectionsImpl.can_add_location([:wormholes, :null], @ls_system_halmah),
"Low-sec should be filtered out with [:wormholes, :null] scopes"
end
test "can_add_location correctly allows wormholes with [:wormholes, :null] scopes" do
# Wormholes should be allowed
assert ConnectionsImpl.can_add_location([:wormholes, :null], @wh_system_j100001),
"Wormhole should be allowed with [:wormholes, :null] scopes"
assert ConnectionsImpl.can_add_location([:wormholes, :null], @wh_system_j100002),
"Wormhole should be allowed with [:wormholes, :null] scopes"
end
test "can_add_location correctly allows null-sec with [:wormholes, :null] scopes" do
# Null-sec should be allowed
assert ConnectionsImpl.can_add_location([:wormholes, :null], @ns_system_geminate),
"Null-sec should be allowed with [:wormholes, :null] scopes"
end
test "maybe_add_system filters out high-sec when not jumping from wormhole" do
# When scopes is [:wormholes, :null] and NOT jumping from wormhole,
# high-sec systems should be filtered
location = %{solar_system_id: @hs_system_halenan}
# old_location is nil (no previous system)
result = SystemsImpl.maybe_add_system("map_id", location, nil, [], [:wormholes, :null])
assert result == :ok
# 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])
assert result == :ok
end
test "maybe_add_system filters out low-sec when not jumping from wormhole" 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])
assert result == :ok
end
test "maybe_add_system allows border high-sec when jumping FROM wormhole" do
# When jumping FROM a wormhole TO high-sec with :wormholes scope,
# the high-sec should be added as a border system
location = %{solar_system_id: @hs_system_halenan}
old_location = %{solar_system_id: @wh_system_j100001}
# This should attempt to add the system (not filter it out)
# The result will be an error because the map doesn't exist,
# but that proves the filtering logic allowed it through
result = SystemsImpl.maybe_add_system("map_id", location, old_location, [], [:wormholes])
# The function attempts to add (returns error because map doesn't exist)
# This proves border behavior is working - system was NOT filtered out
assert match?({:error, _}, result),
"Border system should attempt to be added (error because map doesn't exist)"
end
test "is_connection_valid allows WH to HS with [:wormholes, :null] (border behavior)" do
# The connection is valid for border behavior - but individual systems are filtered
assert ConnectionsImpl.is_connection_valid(
[:wormholes, :null],
@wh_system_j100001,
@hs_system_halenan
),
"WH to HS connection should be valid (border behavior)"
end
test "is_connection_valid rejects HS to LS with [:wormholes, :null] (no border)" do
# HS to LS should be rejected - neither system matches scopes and no wormhole involved
refute ConnectionsImpl.is_connection_valid(
[:wormholes, :null],
@hs_system_halenan,
@ls_system_halmah
),
"HS to LS connection should be rejected with [:wormholes, :null]"
end
test "is_connection_valid rejects HS to HS with [:wormholes, :null]" do
# HS to HS should be rejected
refute ConnectionsImpl.is_connection_valid(
[:wormholes, :null],
@hs_system_halenan,
@hs_system_mili
),
"HS to HS connection should be rejected with [:wormholes, :null]"
end
end
describe "get_effective_scopes behavior" do
alias WandererApp.Map.Server.CharactersImpl
test "get_effective_scopes returns scopes array when present" do
# Create a map struct with scopes array
map = %{scopes: [:wormholes, :null]}
scopes = CharactersImpl.get_effective_scopes(map)
assert scopes == [:wormholes, :null]
end
test "get_effective_scopes converts legacy :all scope" do
map = %{scope: :all}
scopes = CharactersImpl.get_effective_scopes(map)
assert scopes == [:wormholes, :hi, :low, :null, :pochven]
end
test "get_effective_scopes converts legacy :wormholes scope" do
map = %{scope: :wormholes}
scopes = CharactersImpl.get_effective_scopes(map)
assert scopes == [:wormholes]
end
test "get_effective_scopes converts legacy :stargates scope" do
map = %{scope: :stargates}
scopes = CharactersImpl.get_effective_scopes(map)
assert scopes == [:hi, :low, :null, :pochven]
end
test "get_effective_scopes converts legacy :none scope" do
map = %{scope: :none}
scopes = CharactersImpl.get_effective_scopes(map)
assert scopes == []
end
test "get_effective_scopes defaults to [:wormholes] when no scope" do
map = %{}
scopes = CharactersImpl.get_effective_scopes(map)
assert scopes == [:wormholes]
end
end
describe "WandererApp.Map struct and new/1 function" do
alias WandererApp.Map.Server.CharactersImpl
test "Map struct includes scopes field" do
# Verify the struct has the scopes field
map_struct = %WandererApp.Map{}
assert Map.has_key?(map_struct, :scopes)
assert map_struct.scopes == nil
end
test "Map.new/1 extracts scopes from input" do
# Simulate input from database (Ash resource)
input = %{
id: "test-map-id",
name: "Test Map",
scope: :wormholes,
scopes: [:wormholes, :null],
owner_id: "owner-123",
acls: [],
hubs: []
}
map = WandererApp.Map.new(input)
assert map.map_id == "test-map-id"
assert map.name == "Test Map"
assert map.scope == :wormholes
assert map.scopes == [:wormholes, :null]
end
test "Map.new/1 handles missing scopes (nil)" do
# When scopes is not present in input, it should be nil
input = %{
id: "test-map-id",
name: "Test Map",
scope: :all,
owner_id: "owner-123",
acls: [],
hubs: []
}
map = WandererApp.Map.new(input)
assert map.map_id == "test-map-id"
assert map.scope == :all
assert map.scopes == nil
end
test "get_effective_scopes uses scopes field from Map struct when present" do
# Create map struct with both scope and scopes
input = %{
id: "test-map-id",
name: "Test Map",
scope: :all,
scopes: [:wormholes, :null],
owner_id: "owner-123",
acls: [],
hubs: []
}
map = WandererApp.Map.new(input)
# get_effective_scopes should prioritize scopes over scope
effective = CharactersImpl.get_effective_scopes(map)
assert effective == [:wormholes, :null]
end
test "get_effective_scopes falls back to legacy scope when scopes is nil" do
# Create map struct with only legacy scope
input = %{
id: "test-map-id",
name: "Test Map",
scope: :all,
owner_id: "owner-123",
acls: [],
hubs: []
}
map = WandererApp.Map.new(input)
# get_effective_scopes should convert legacy :all scope
effective = CharactersImpl.get_effective_scopes(map)
assert effective == [:wormholes, :hi, :low, :null, :pochven]
end
test "get_effective_scopes falls back to legacy scope when scopes is empty list" do
# Empty scopes list should fall back to legacy scope
input = %{
id: "test-map-id",
name: "Test Map",
scope: :stargates,
scopes: [],
owner_id: "owner-123",
acls: [],
hubs: []
}
map = WandererApp.Map.new(input)
# get_effective_scopes should fall back to legacy scope conversion
effective = CharactersImpl.get_effective_scopes(map)
assert effective == [:hi, :low, :null, :pochven]
end
test "Map.new/1 extracts all scope variations correctly" do
# Test various scope combinations
test_cases = [
{[:wormholes], [:wormholes]},
{[:hi, :low], [:hi, :low]},
{[:wormholes, :hi, :low, :null, :pochven], [:wormholes, :hi, :low, :null, :pochven]},
{[:null], [:null]}
]
for {input_scopes, expected_scopes} <- test_cases do
input = %{
id: "test-map-id",
name: "Test Map",
scope: :wormholes,
scopes: input_scopes,
owner_id: "owner-123",
acls: [],
hubs: []
}
map = WandererApp.Map.new(input)
effective = CharactersImpl.get_effective_scopes(map)
assert effective == expected_scopes,
"Expected #{inspect(expected_scopes)}, got #{inspect(effective)} for input #{inspect(input_scopes)}"
end
end
end
end

View File

@@ -300,7 +300,7 @@ defmodule WandererAppWeb.Factory do
# Include owner_id in the form data just like the LiveView does
create_attrs =
built_attrs
|> Map.take([:name, :slug, :description, :scope, :only_tracked_characters])
|> Map.take([:name, :slug, :description, :scope, :scopes, :only_tracked_characters])
|> Map.put(:owner_id, owner_id)
# Debug: ensure owner_id is valid