Compare commits

..

25 Commits

Author SHA1 Message Date
CI
8a5f96a847 chore: release version v1.59.1
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / Manual Approval (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-03-26 21:50:35 +00:00
guarzo
149fa57075 fix (doc): improve bot setup instructions (#309) 2025-03-27 01:41:21 +04:00
CI
affe184ccd chore: release version v1.59.0
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / Manual Approval (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-03-23 10:26:30 +00:00
Dmitry Popov
1e5e73c4ae feat(Core): added handling cases when wrong connections created 2025-03-23 11:06:20 +01:00
Tyson GG
c76316da03 feat (api) add map connections endpoint (#301)
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / Manual Approval (push) Blocked by required conditions
Build / 🛠 Build (1.17, 18.x, 27) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-03-23 00:43:32 +04:00
CI
de6205f860 chore: release version v1.58.0
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / Manual Approval (push) Blocked by required conditions
Build / 🛠 Build (1.17, 18.x, 27) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-03-22 08:38:37 +00:00
Dmitry Popov
f994255091 feat(Core): Show online state on map characters page 2025-03-22 09:29:13 +01:00
Tyson GG
6d4981a3db fix (routes) fix query parameter formatting when calling esi routes endpoint (#302) 2025-03-22 11:53:12 +04:00
guarzo
06fef2296f feat (api): update character activity and api to allow date range (#299)
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / Manual Approval (push) Blocked by required conditions
Build / 🛠 Build (1.17, 18.x, 27) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
* feat (api): update character activity and api to allow date range
2025-03-21 21:05:48 +04:00
CI
999a702291 chore: release version v1.57.1
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / Manual Approval (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-03-20 17:51:07 +00:00
Dmitry Popov
020b9bb2c2 chore: added user-agent & ensured cache handled correctly on each request 2025-03-20 18:39:40 +01:00
CI
7713caab51 chore: release version v1.57.0
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / Manual Approval (push) Blocked by required conditions
Build / 🛠 Build (1.17, 18.x, 27) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-03-19 15:33:39 +00:00
guarzo
97a777d729 feat (doc): update bot news (#294) 2025-03-19 19:17:25 +04:00
CI
8241d1f08c chore: release version v1.56.6 2025-03-19 14:45:22 +00:00
Dmitry Popov
2ac85bbfff chore: release version v1.56.5 2025-03-19 15:08:51 +01:00
CI
3f68ae2235 chore: release version v1.56.5
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / Manual Approval (push) Blocked by required conditions
Build / 🛠 Build (1.17, 18.x, 27) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-03-19 13:07:55 +00:00
Dmitry Popov
0f7b6f75df Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-03-19 13:57:58 +01:00
Dmitry Popov
b048e8f5ca chore: added fallback chipher options 2025-03-19 13:55:39 +01:00
CI
9783dc45ff chore: release version v1.56.4 2025-03-19 11:36:46 +00:00
Dmitry Popov
badbefbade Revert "fix: cloak key error behavior (#288)" (#290)
This reverts commit 9b5ea2f84b.
2025-03-19 15:30:07 +04:00
CI
b6a265cfad chore: release version v1.56.3 2025-03-19 07:26:24 +00:00
guarzo
9b5ea2f84b fix: cloak key error behavior (#288) 2025-03-19 11:13:54 +04:00
guarzo
d8acfa5c05 refactor: standalone unit tests (#278)
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / Manual Approval (push) Blocked by required conditions
Build / 🛠 Build (1.17, 18.x, 27) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-03-18 21:37:52 +04:00
CI
2a5b6924eb chore: release version v1.56.2 2025-03-18 16:47:40 +00:00
Dmitry Popov
3b9aee1eb9 fix: show signature tooltip on top 2025-03-18 17:33:18 +01:00
33 changed files with 2677 additions and 1077 deletions

View File

@@ -2,6 +2,84 @@
<!-- changelog -->
## [v1.59.1](https://github.com/wanderer-industries/wanderer/compare/v1.59.0...v1.59.1) (2025-03-26)
### Bug Fixes:
* doc: improve bot setup instructions (#309)
## [v1.59.0](https://github.com/wanderer-industries/wanderer/compare/v1.58.0...v1.59.0) (2025-03-23)
### Features:
* Core: added handling cases when wrong connections created
## [v1.58.0](https://github.com/wanderer-industries/wanderer/compare/v1.57.1...v1.58.0) (2025-03-22)
### Features:
* Core: Show online state on map characters page
* api: update character activity and api to allow date range (#299)
* api: update character activity and api to allow date range
## [v1.57.1](https://github.com/wanderer-industries/wanderer/compare/v1.57.0...v1.57.1) (2025-03-20)
## [v1.57.0](https://github.com/wanderer-industries/wanderer/compare/v1.56.6...v1.57.0) (2025-03-19)
### Features:
* doc: update bot news (#294)
## [v1.56.6](https://github.com/wanderer-industries/wanderer/compare/v1.56.5...v1.56.6) (2025-03-19)
## [v1.56.5](https://github.com/wanderer-industries/wanderer/compare/v1.56.4...v1.56.5) (2025-03-19)
## [v1.56.4](https://github.com/wanderer-industries/wanderer/compare/v1.56.3...v1.56.4) (2025-03-19)
## [v1.56.3](https://github.com/wanderer-industries/wanderer/compare/v1.56.2...v1.56.3) (2025-03-19)
### Bug Fixes:
* cloak key error behavior (#288)
## [v1.56.2](https://github.com/wanderer-industries/wanderer/compare/v1.56.1...v1.56.2) (2025-03-18)
### Bug Fixes:
* show signature tooltip on top
## [v1.56.1](https://github.com/wanderer-industries/wanderer/compare/v1.56.0...v1.56.1) (2025-03-18)

View File

@@ -1,4 +1,4 @@
.PHONY: deploy install cleanup start yarn migrate format test coverage versions
.PHONY: deploy install cleanup start yarn migrate format test coverage versions standalone-tests
ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
SHELL := /bin/bash
@@ -35,6 +35,11 @@ test t:
coverage cover co:
mix test --cover
unit-tests ut:
@echo "Running unit tests..."
@find test/unit -name "*.exs" -exec elixir {} \;
@echo "All unit tests completed."
versions v:
@echo "Tool Versions"
@cat .tool-versions

View File

@@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { PrimeIcons } from 'primereact/api';
import { Column } from 'primereact/column';
import {
DataTable,
DataTableRowClickEvent,
@@ -6,13 +7,9 @@ import {
DataTableStateEvent,
SortOrder,
} from 'primereact/datatable';
import { Column } from 'primereact/column';
import { PrimeIcons } from 'primereact/api';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import useLocalStorageState from 'use-local-storage-state';
import { ExtendedSystemSignature, SignatureGroup, SignatureKind, SystemSignature } from '@/hooks/Mapper/types';
import { SignatureSettings } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings';
import { WdTooltip, WdTooltipHandlers, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
import { SignatureView } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SignatureView';
import {
COMPACT_MAX_WIDTH,
@@ -24,6 +21,9 @@ import {
SIGNATURE_WINDOW_ID,
SignatureSettingsType,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants';
import { SignatureSettings } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings';
import { TooltipPosition, WdTooltip, WdTooltipHandlers, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
import { ExtendedSystemSignature, SignatureGroup, SignatureKind, SystemSignature } from '@/hooks/Mapper/types';
import {
renderAddedTimeLeft,
@@ -32,10 +32,10 @@ import {
renderInfoColumn,
renderUpdatedTimeLeft,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders';
import { useSystemSignaturesData } from '../hooks/useSystemSignaturesData';
import { getSignatureRowClass } from '../helpers/rowStyles';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
import { useClipboard, useHotkey } from '@/hooks/Mapper/hooks';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
import { getSignatureRowClass } from '../helpers/rowStyles';
import { useSystemSignaturesData } from '../hooks/useSystemSignaturesData';
const renderColIcon = (sig: SystemSignature) => renderIcon(sig);
@@ -348,6 +348,7 @@ export const SystemSignaturesContent = ({
<WdTooltip
className="bg-stone-900/95 text-slate-50"
ref={tooltipRef}
position={TooltipPosition.top}
content={
hoveredSignature ? (
<SignatureView signature={hoveredSignature} showCharacterPortrait={showCharacterPortrait} />

View File

@@ -17,6 +17,7 @@ export type ShipTypeRaw = {
export type LocationRaw = {
solar_system_id: number | null;
structure_id: number | null;
station_id: number | null;
};
export type CharacterTypeRaw = {

View File

@@ -104,7 +104,7 @@ defmodule WandererApp.Api.Character do
update :update_location do
require_atomic? false
accept([:solar_system_id, :structure_id])
accept([:solar_system_id, :structure_id, :station_id])
end
update :update_ship do
@@ -141,6 +141,7 @@ defmodule WandererApp.Api.Character do
:ship,
:solar_system_id,
:structure_id,
:station_id,
:access_token,
:refresh_token
])
@@ -150,6 +151,7 @@ defmodule WandererApp.Api.Character do
:ship,
:solar_system_id,
:structure_id,
:station_id,
:access_token,
:refresh_token
])
@@ -185,6 +187,7 @@ defmodule WandererApp.Api.Character do
attribute :location, :string
attribute :solar_system_id, :integer
attribute :structure_id, :integer
attribute :station_id, :integer
attribute :ship, :integer
attribute :ship_name, :string
attribute :corporation_id, :integer

View File

@@ -186,7 +186,7 @@ defmodule WandererApp.Character do
do: %{ship_name: nil, ship_type_info: %{}}
def get_location(
%{solar_system_id: solar_system_id, structure_id: structure_id} =
%{solar_system_id: solar_system_id, structure_id: structure_id, station_id: station_id} =
_character
) do
case WandererApp.CachedInfo.get_system_static_info(solar_system_id) do
@@ -194,6 +194,7 @@ defmodule WandererApp.Character do
%{
solar_system_id: solar_system_id,
structure_id: structure_id,
station_id: station_id,
solar_system_info: system_static_info
}
@@ -201,6 +202,7 @@ defmodule WandererApp.Character do
%{
solar_system_id: solar_system_id,
structure_id: structure_id,
station_id: station_id,
solar_system_info: %{}
}
end

View File

@@ -489,7 +489,7 @@ defmodule WandererApp.Character.Tracker do
defp maybe_update_location(
%{
character_id: character_id,
character_id: character_id
} =
state,
location
@@ -515,11 +515,13 @@ defmodule WandererApp.Character.Tracker do
end
end
{:ok, %{solar_system_id: solar_system_id, structure_id: structure_id} = character} =
{:ok,
%{solar_system_id: solar_system_id, structure_id: structure_id, station_id: station_id} =
character} =
WandererApp.Character.get_character(character_id)
(not is_location_started?(character_id) ||
is_location_updated?(location, solar_system_id, structure_id))
is_location_updated?(location, solar_system_id, structure_id, station_id))
|> case do
true ->
{:ok, _character} = WandererApp.Api.Character.update_location(character, location)
@@ -542,10 +544,38 @@ defmodule WandererApp.Character.Tracker do
false
)
defp is_location_updated?(location, solar_system_id, structure_id),
do:
solar_system_id != location.solar_system_id ||
structure_id != location.structure_id
defp is_location_updated?(
%{solar_system_id: new_solar_system_id, station_id: new_station_id} = _location,
solar_system_id,
structure_id,
station_id
),
do:
solar_system_id != new_solar_system_id ||
not is_nil(structure_id) ||
station_id != new_station_id
defp is_location_updated?(
%{solar_system_id: new_solar_system_id, structure_id: new_structure_id} = _location,
solar_system_id,
structure_id,
station_id
),
do:
solar_system_id != new_solar_system_id ||
structure_id != new_structure_id ||
not is_nil(station_id)
defp is_location_updated?(
%{solar_system_id: new_solar_system_id} = _location,
solar_system_id,
structure_id,
station_id
),
do:
solar_system_id != new_solar_system_id ||
not is_nil(structure_id) ||
not is_nil(station_id)
defp maybe_update_corporation(
state,
@@ -732,13 +762,22 @@ defmodule WandererApp.Character.Tracker do
),
do: state
defp get_location(%{"solar_system_id" => solar_system_id, "structure_id" => structure_id}),
do: %{solar_system_id: solar_system_id, structure_id: structure_id}
defp get_location(%{
"solar_system_id" => solar_system_id,
"station_id" => station_id
}),
do: %{solar_system_id: solar_system_id, structure_id: nil, station_id: station_id}
defp get_location(%{
"solar_system_id" => solar_system_id,
"structure_id" => structure_id
}),
do: %{solar_system_id: solar_system_id, structure_id: structure_id, station_id: nil}
defp get_location(%{"solar_system_id" => solar_system_id}),
do: %{solar_system_id: solar_system_id, structure_id: nil}
do: %{solar_system_id: solar_system_id, structure_id: nil, station_id: nil}
defp get_location(_), do: %{solar_system_id: nil, structure_id: nil}
defp get_location(_), do: %{solar_system_id: nil, structure_id: nil, station_id: nil}
defp get_online(%{"online" => online}), do: %{online: online}

View File

@@ -4,7 +4,12 @@ defmodule WandererApp.Env do
@app :wanderer_app
@decorate cacheable(
cache: WandererApp.Cache,
key: "vsn_version"
)
def vsn(), do: Application.spec(@app)[:vsn]
def git_sha(), do: get_key(:git_sha, "<GIT_SHA>")
def base_url, do: get_key(:web_app_url, "<BASE_URL>")
def custom_route_base_url, do: get_key(:custom_route_base_url, "<CUSTOM_ROUTE_BASE_URL>")

View File

@@ -9,6 +9,7 @@ defmodule WandererApp.Esi.ApiClient do
@routes_ttl :timer.minutes(15)
@base_url "https://esi.evetech.net/latest"
@wanderrer_user_agent "(wanderer-industries@proton.me; +https://github.com/wanderer-industries/wanderer)"
@get_link_pairs_advanced_params [
:include_mass_crit,
@@ -289,14 +290,13 @@ defmodule WandererApp.Esi.ApiClient do
end
end
@decorate cacheable(
cache: Cache,
key: "killmail-#{killmail_id}-#{killmail_hash}",
opts: [ttl: @ttl]
)
cache: Cache,
key: "killmail-#{killmail_id}-#{killmail_hash}",
opts: [ttl: @ttl]
)
def get_killmail(killmail_id, killmail_hash, opts \\ []) do
get("/killmails/#{killmail_id}/#{killmail_hash}/", _with_cache_opts(opts))
get("/killmails/#{killmail_id}/#{killmail_hash}/", opts)
end
@decorate cacheable(
@@ -319,7 +319,7 @@ defmodule WandererApp.Esi.ApiClient do
def get_character_info(eve_id, opts \\ []) do
case get(
"/characters/#{eve_id}/",
opts |> _with_cache_opts()
opts
) do
{:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
{:error, error} -> {:error, error}
@@ -370,10 +370,10 @@ defmodule WandererApp.Esi.ApiClient do
end
@decorate cacheable(
cache: Cache,
key: "search-#{character_eve_id}-#{categories_val}-#{search_val |> Slug.slugify()}",
opts: [ttl: @ttl]
)
cache: Cache,
key: "search-#{character_eve_id}-#{categories_val}-#{search_val |> Slug.slugify()}",
opts: [ttl: @ttl]
)
defp _search(character_eve_id, search_val, categories_val, merged_opts) do
_get_character_auth_data(character_eve_id, "search", merged_opts)
end
@@ -401,27 +401,32 @@ defmodule WandererApp.Esi.ApiClient do
defp _get_routes(origin, destination, params, opts),
do: _get_routes_eve(origin, destination, params, opts)
defp _get_routes_eve(origin, destination, params, opts),
do:
get(
"/route/#{origin}/#{destination}/?#{params |> Plug.Conn.Query.encode()}",
opts |> _with_cache_opts()
)
defp _get_routes_eve(origin, destination, params, opts) do
esi_params = Map.merge(params, %{
connections: params.connections |> Enum.join(","),
avoid: params.avoid |> Enum.join(",")
})
defp _get_auth_opts(opts), do: [auth: {:bearer, opts[:access_token]}]
get(
"/route/#{origin}/#{destination}/?#{esi_params |> Plug.Conn.Query.encode()}",
opts
)
end
defp get_auth_opts(opts), do: [auth: {:bearer, opts[:access_token]}]
defp _get_alliance_info(alliance_eve_id, info_path, opts),
do:
get(
"/alliances/#{alliance_eve_id}/#{info_path}",
opts |> _with_cache_opts()
opts
)
defp _get_corporation_info(corporation_eve_id, info_path, opts),
do:
get(
"/corporations/#{corporation_eve_id}/#{info_path}",
opts |> _with_cache_opts()
opts
)
defp _get_character_auth_data(character_eve_id, info_path, opts) do
@@ -429,7 +434,7 @@ defmodule WandererApp.Esi.ApiClient do
auth_opts =
[params: opts[:params] || []] ++
(opts |> _get_auth_opts() |> _with_cache_opts())
(opts |> get_auth_opts())
character_id = opts |> Keyword.get(:character_id, nil)
@@ -440,7 +445,7 @@ defmodule WandererApp.Esi.ApiClient do
opts
)
else
_get_retry(path, auth_opts, opts)
get_retry(path, auth_opts, opts)
end
end
@@ -458,11 +463,18 @@ defmodule WandererApp.Esi.ApiClient do
get(
"/corporations/#{corporation_eve_id}/#{info_path}",
[params: opts[:params] || []] ++
(opts |> _get_auth_opts() |> _with_cache_opts()),
(opts |> get_auth_opts()),
opts
)
defp _with_cache_opts(opts) do
defp with_user_agent_opts(opts) do
opts
|> Keyword.merge(
headers: [{:user_agent, "Wanderer/#{WandererApp.Env.vsn()} #{@wanderrer_user_agent}"}]
)
end
defp with_cache_opts(opts) do
opts |> Keyword.merge(@cache_opts) |> Keyword.merge(cache_dir: System.tmp_dir!())
end
@@ -470,12 +482,15 @@ defmodule WandererApp.Esi.ApiClient do
do:
post(
"#{@base_url}#{path}",
[params: opts[:params] || []] ++ (opts |> _get_auth_opts())
[params: opts[:params] || []] ++ (opts |> get_auth_opts())
)
defp get(path, api_opts \\ [], opts \\ []) do
try do
case Req.get("#{@base_url}#{path}", api_opts |> Keyword.merge(@retry_opts)) do
case Req.get(
"#{@base_url}#{path}",
api_opts |> with_user_agent_opts() |> with_cache_opts() |> Keyword.merge(@retry_opts)
) do
{:ok, %{status: 200, body: body}} ->
{:ok, body}
@@ -486,10 +501,10 @@ defmodule WandererApp.Esi.ApiClient do
{:error, :not_found}
{:ok, %{status: 403} = _error} ->
_get_retry(path, api_opts, opts)
get_retry(path, api_opts, opts)
{:ok, %{status: 420} = _error} ->
_get_retry(path, api_opts, opts)
get_retry(path, api_opts, opts)
{:ok, %{status: status}} ->
{:error, "Unexpected status: #{status}"}
@@ -507,7 +522,7 @@ defmodule WandererApp.Esi.ApiClient do
defp post(url, opts) do
try do
case Req.post("#{url}", opts) do
case Req.post("#{url}", opts |> with_user_agent_opts()) do
{:ok, %{status: status, body: body}} when status in [200, 201] ->
{:ok, body}
@@ -531,7 +546,7 @@ defmodule WandererApp.Esi.ApiClient do
end
end
defp _get_retry(path, api_opts, opts) do
defp get_retry(path, api_opts, opts) do
refresh_token? = opts |> Keyword.get(:refresh_token?, false)
retry_count = opts |> Keyword.get(:retry_count, 0)
character_id = opts |> Keyword.get(:character_id, nil)
@@ -541,7 +556,7 @@ defmodule WandererApp.Esi.ApiClient do
else
case _refresh_token(character_id) do
{:ok, token} ->
auth_opts = [access_token: token.access_token] |> _get_auth_opts()
auth_opts = [access_token: token.access_token] |> get_auth_opts()
get(
path,

View File

@@ -527,20 +527,22 @@ defmodule WandererApp.Map do
@doc """
Returns the raw activity data that can be processed by WandererApp.Character.Activity.
Only includes characters that are on the map's ACL.
If days parameter is provided, filters activity to that time period.
"""
def get_character_activity(map_id) do
def get_character_activity(map_id, days \\ nil) do
{:ok, map} = WandererApp.Api.Map.by_id(map_id)
_map_with_acls = Ash.load!(map, :acls)
{:ok, jumps} = WandererApp.Api.MapChainPassages.by_map_id(%{map_id: map_id})
thirty_days_ago = DateTime.utc_now() |> DateTime.add(-30 * 24 * 3600, :second)
# Calculate cutoff date if days is provided
cutoff_date = if days, do: DateTime.utc_now() |> DateTime.add(-days * 24 * 3600, :second), else: nil
# Get activity data
connections_activity = get_connections_activity(map_id, thirty_days_ago)
signatures_activity = get_signatures_activity(map_id, thirty_days_ago)
passages_activity = get_passages_activity(map_id, cutoff_date)
connections_activity = get_connections_activity(map_id, cutoff_date)
signatures_activity = get_signatures_activity(map_id, cutoff_date)
# Return raw activity data
jumps
# Return activity data
passages_activity
|> Enum.map(fn passage ->
%{
character: passage.character,
@@ -554,14 +556,40 @@ defmodule WandererApp.Map do
end)
end
defp get_connections_activity(map_id, thirty_days_ago) do
defp get_passages_activity(map_id, nil) do
# Query all map chain passages without time filter
from(p in WandererApp.Api.MapChainPassages,
join: c in assoc(p, :character),
where: p.map_id == ^map_id,
group_by: [c.id],
select: {c, count(p.id)}
)
|> WandererApp.Repo.all()
|> Enum.map(fn {character, count} -> %{character: character, count: count} end)
end
defp get_passages_activity(map_id, cutoff_date) do
# Query map chain passages with time filter
from(p in WandererApp.Api.MapChainPassages,
join: c in assoc(p, :character),
where:
p.map_id == ^map_id and
p.inserted_at > ^cutoff_date,
group_by: [c.id],
select: {c, count(p.id)}
)
|> WandererApp.Repo.all()
|> Enum.map(fn {character, count} -> %{character: character, count: count} end)
end
defp get_connections_activity(map_id, nil) do
# Query all connection activity without time filter
from(ua in WandererApp.Api.UserActivity,
join: c in assoc(ua, :character),
where:
ua.entity_id == ^map_id and
ua.entity_type == :map and
ua.event_type == :map_connection_added and
ua.inserted_at > ^thirty_days_ago,
ua.event_type == :map_connection_added,
group_by: [c.id],
select: {c.id, count(ua.id)}
)
@@ -569,14 +597,43 @@ defmodule WandererApp.Map do
|> Map.new()
end
defp get_signatures_activity(map_id, thirty_days_ago) do
defp get_connections_activity(map_id, cutoff_date) do
from(ua in WandererApp.Api.UserActivity,
join: c in assoc(ua, :character),
where:
ua.entity_id == ^map_id and
ua.entity_type == :map and
ua.event_type == :map_connection_added and
ua.inserted_at > ^cutoff_date,
group_by: [c.id],
select: {c.id, count(ua.id)}
)
|> WandererApp.Repo.all()
|> Map.new()
end
defp get_signatures_activity(map_id, nil) do
# Query all signature activity without time filter
from(ua in WandererApp.Api.UserActivity,
join: c in assoc(ua, :character),
where:
ua.entity_id == ^map_id and
ua.entity_type == :map and
ua.event_type == :signatures_added,
select: {ua.character_id, ua.event_data}
)
|> WandererApp.Repo.all()
|> process_signatures_data()
end
defp get_signatures_activity(map_id, cutoff_date) do
from(ua in WandererApp.Api.UserActivity,
join: c in assoc(ua, :character),
where:
ua.entity_id == ^map_id and
ua.entity_type == :map and
ua.event_type == :signatures_added and
ua.inserted_at > ^thirty_days_ago,
ua.inserted_at > ^cutoff_date,
select: {ua.character_id, ua.event_data}
)
|> WandererApp.Repo.all()

View File

@@ -282,8 +282,10 @@ defmodule WandererApp.Map.Server.CharactersImpl do
:ok =
SystemsImpl.maybe_add_system(map_id, old_location, location, rtree_name, map_opts)
:ok =
ConnectionsImpl.maybe_add_connection(map_id, location, old_location, character_id)
if is_character_in_space?(location) do
:ok =
ConnectionsImpl.maybe_add_connection(map_id, location, old_location, character_id)
end
_ ->
:ok
@@ -291,6 +293,10 @@ defmodule WandererApp.Map.Server.CharactersImpl do
end
end
defp is_character_in_space?(%{station_id: station_id, structure_id: structure_id} = location) do
is_nil(structure_id) and is_nil(station_id)
end
defp track_character(map_id, character_id),
do:
WandererApp.Character.TrackerManager.update_track_settings(character_id, %{
@@ -367,7 +373,8 @@ defmodule WandererApp.Map.Server.CharactersImpl do
{:ok, old_solar_system_id} =
WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:solar_system_id")
{:ok, %{solar_system_id: solar_system_id}} =
{:ok,
%{solar_system_id: solar_system_id, structure_id: structure_id, station_id: station_id}} =
WandererApp.Character.get_character(character_id)
WandererApp.Cache.insert(
@@ -378,9 +385,14 @@ defmodule WandererApp.Map.Server.CharactersImpl do
case solar_system_id != old_solar_system_id do
true ->
[
{:character_location, %{solar_system_id: solar_system_id},
%{solar_system_id: old_solar_system_id}}
{:character_location,
%{
solar_system_id: solar_system_id,
structure_id: structure_id,
station_id: station_id
}, %{solar_system_id: old_solar_system_id}}
]
_ ->
[:skip]
end
@@ -389,7 +401,9 @@ defmodule WandererApp.Map.Server.CharactersImpl do
{:ok, old_solar_system_id} =
WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:solar_system_id")
{:ok, %{solar_system_id: solar_system_id} = _character} =
{:ok,
%{solar_system_id: solar_system_id, structure_id: structure_id, station_id: station_id} =
_character} =
WandererApp.Character.get_character(character_id)
WandererApp.Cache.insert(
@@ -399,7 +413,12 @@ defmodule WandererApp.Map.Server.CharactersImpl do
if is_nil(old_solar_system_id) or solar_system_id != old_solar_system_id do
[
{:character_location, %{solar_system_id: solar_system_id}, %{solar_system_id: nil}}
{:character_location,
%{
solar_system_id: solar_system_id,
structure_id: structure_id,
station_id: station_id
}, %{solar_system_id: nil}}
]
else
[:skip]

View File

@@ -495,8 +495,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
not is_prohibited_system_class?(to_system_static_info.system_class) and
not (@prohibited_systems |> Enum.member?(from_solar_system_id)) and
not (@prohibited_systems |> Enum.member?(to_solar_system_id)) and
known_jumps |> Enum.empty?() and to_solar_system_id != @jita and
from_solar_system_id != @jita
known_jumps |> Enum.empty?()
:stargates ->
not is_prohibited_system_class?(from_system_static_info.system_class) and

View File

@@ -3,20 +3,130 @@ defmodule WandererApp.Vault do
@impl GenServer
def init(config) do
cipher_key = decode_env!("CLOAK_KEY")
fallback_cipher_key = decode_env!("FALLBACK_CLOAK_KEY")
config =
Keyword.put(config, :ciphers,
default: {
Cloak.Ciphers.AES.GCM,
tag: "AES.GCM.V1", key: decode_env!("CLOAK_KEY"), iv_length: 12
tag: "AES.GCM.V1", key: cipher_key, iv_length: 12
},
fallback: {
Cloak.Ciphers.AES.GCM,
tag: "AES.GCM.V1", key: fallback_cipher_key, iv_length: 12
}
)
{:ok, config}
end
defp decode_env!(var) do
@impl Cloak.Vault
def encrypt(plaintext) do
with {:ok, config} <- Cloak.Vault.read_config(@table_name) do
Cloak.Vault.encrypt(config, plaintext)
end
end
@impl Cloak.Vault
def encrypt!(plaintext) do
case Cloak.Vault.read_config(@table_name) do
{:ok, config} ->
Cloak.Vault.encrypt!(config, plaintext)
{:error, error} ->
raise error
end
end
@impl Cloak.Vault
def encrypt(plaintext, label) do
with {:ok, config} <- Cloak.Vault.read_config(@table_name) do
Cloak.Vault.encrypt(config, plaintext, label)
end
end
@impl Cloak.Vault
def encrypt!(plaintext, label) do
case Cloak.Vault.read_config(@table_name) do
{:ok, config} ->
Cloak.Vault.encrypt!(config, plaintext, label)
{:error, error} ->
raise error
end
end
@impl Cloak.Vault
def decrypt(ciphertext) do
with {:ok, config} <- Cloak.Vault.read_config(@table_name) do
decrypt(config, ciphertext)
end
end
@impl Cloak.Vault
def decrypt!(ciphertext) do
case Cloak.Vault.read_config(@table_name) do
{:ok, config} ->
decrypt!(config, ciphertext)
{:error, error} ->
raise error
end
end
defp decode_env!(var, fallback_key \\ "OtPJXGfKNyOMWI7TdpcWgOlyNtD9AGSfoAdvEuTQIno=") do
var
|> System.get_env("OtPJXGfKNyOMWI7TdpcWgOlyNtD9AGSfoAdvEuTQIno=")
|> System.get_env(fallback_key)
|> Base.decode64!()
end
@doc false
def decrypt(config, ciphertext) do
case find_module_to_decrypt(config, ciphertext) do
nil ->
{:error, Cloak.MissingCipher.exception(vault: config[:vault], ciphertext: ciphertext)}
{_label, {module, opts}} ->
case module.decrypt(ciphertext, opts) do
{:ok, :error} ->
case find_fallback_module_to_decrypt(config, ciphertext) do
nil ->
{:ok, :error}
{_label, {module, opts}} ->
module.decrypt(ciphertext, opts)
end
{:ok, plaintext} ->
{:ok, plaintext}
error ->
error
end
end
end
@doc false
def decrypt!(config, ciphertext) do
case decrypt(config, ciphertext) do
{:ok, plaintext} ->
plaintext
{:error, error} ->
raise error
end
end
defp find_module_to_decrypt(config, ciphertext) do
Enum.find(config[:ciphers], fn {_label, {module, opts}} ->
module.can_decrypt?(ciphertext, opts)
end)
end
defp find_fallback_module_to_decrypt(config, ciphertext) do
Enum.find(config[:ciphers], fn {label, _} ->
label == :fallback
end)
end
end

View File

@@ -7,6 +7,7 @@ defmodule WandererAppWeb.MapAPIController do
alias WandererApp.Api
alias WandererApp.Api.Character
alias WandererApp.MapConnectionRepo
alias WandererApp.MapSystemRepo
alias WandererApp.MapCharacterSettingsRepo
@@ -61,6 +62,44 @@ defmodule WandererAppWeb.MapAPIController do
required: ["data"]
}
# For operation :list_connections
@map_connection_schema %OpenApiSpex.Schema{
type: :object,
properties: %{
id: %OpenApiSpex.Schema{type: :string},
map_id: %OpenApiSpex.Schema{type: :string},
solar_system_source: %OpenApiSpex.Schema{type: :integer},
solar_system_target: %OpenApiSpex.Schema{type: :integer},
mass_status: %OpenApiSpex.Schema{type: :integer},
time_status: %OpenApiSpex.Schema{type: :integer},
ship_size_type: %OpenApiSpex.Schema{type: :integer},
type: %OpenApiSpex.Schema{type: :integer},
wormhole_type: %OpenApiSpex.Schema{type: :string},
inserted_at: %OpenApiSpex.Schema{type: :string, format: :date_time},
updated_at: %OpenApiSpex.Schema{type: :string, format: :date_time}
},
required: ["id", "map_id", "solar_system_source", "solar_system_target", "type", "inserted_at", "updated_at"]
}
@list_map_connections_response_schema %OpenApiSpex.Schema{
type: :object,
properties: %{
data: %OpenApiSpex.Schema{
type: :array,
items: @map_connection_schema
}
},
required: ["data"]
}
@show_map_system_response_schema %OpenApiSpex.Schema{
type: :object,
properties: %{
data: @map_connection_schema
},
required: ["data"]
}
# For operation :tracked_characters_with_info
@character_schema %OpenApiSpex.Schema{
type: :object,
@@ -353,6 +392,70 @@ defmodule WandererAppWeb.MapAPIController do
end
end
@doc """
GET /api/map/connections
Requires either `?map_id=<UUID>` **OR** `?slug=<map-slug>` in the query params.
Examples:
GET /api/map/connections?map_id=466e922b-e758-485e-9b86-afae06b88363
GET /api/map/connections?slug=my-unique-wormhole-map
"""
@spec list_connections(Plug.Conn.t(), map()) :: Plug.Conn.t()
operation :list_connections,
summary: "List Map Connections",
description: "Lists all connections for a map. Requires either 'map_id' or 'slug' as a query parameter to identify the map.",
parameters: [
map_id: [
in: :query,
description: "Map identifier (UUID) - Either map_id or slug must be provided",
type: :string,
required: false,
example: ""
],
slug: [
in: :query,
description: "Map slug - Either map_id or slug must be provided",
type: :string,
required: false,
example: "map-name"
]
],
responses: [
ok: {
"List of map connections",
"application/json",
@list_map_connections_response_schema
},
bad_request: {"Error", "application/json", %OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{type: :string}
},
required: ["error"],
example: %{
"error" => "Must provide either ?map_id=UUID or ?slug=SLUG"
}
}}
]
def list_connections(conn, params) do
with {:ok, map_id} <- Util.fetch_map_id(params),
{:ok, systems} <- MapConnectionRepo.get_by_map(map_id) do
data = Enum.map(systems, &connection_to_json/1)
json(conn, %{data: data})
else
{:error, msg} when is_binary(msg) ->
conn
|> put_status(:bad_request)
|> json(%{error: msg})
{:error, reason} ->
conn
|> put_status(:not_found)
|> json(%{error: "Could not fetch connections: #{inspect(reason)}"})
end
end
@doc """
GET /api/map/tracked_characters_with_info
@@ -648,15 +751,17 @@ defmodule WandererAppWeb.MapAPIController do
Returns character activity data for a map.
Requires either `?map_id=<UUID>` or `?slug=<map-slug>`.
Optional `days` parameter to filter activity to a specific time period.
Example:
GET /api/map/character_activity?map_id=<uuid>
GET /api/map/character_activity?slug=<map-slug>
GET /api/map/character_activity?map_id=<uuid>&days=7
"""
@spec character_activity(Plug.Conn.t(), map()) :: Plug.Conn.t()
operation :character_activity,
summary: "Get Character Activity",
description: "Returns character activity data for a map. Requires either 'map_id' or 'slug' as a query parameter to identify the map.",
description: "Returns character activity data for a map. If days parameter is provided, filters activity to that time period, otherwise returns all activity. Requires either 'map_id' or 'slug' as a query parameter to identify the map.",
parameters: [
map_id: [
in: :query,
@@ -671,6 +776,13 @@ defmodule WandererAppWeb.MapAPIController do
type: :string,
required: false,
example: "map-name"
],
days: [
in: :query,
description: "Optional: Number of days to look back for activity data. If not provided, returns all activity history.",
type: :integer,
required: false,
example: "7"
]
],
responses: [
@@ -691,9 +803,10 @@ defmodule WandererAppWeb.MapAPIController do
}}
]
def character_activity(conn, params) do
with {:ok, map_id} <- Util.fetch_map_id(params) do
# Get raw activity data directly from the Map module instead of the Activity processor
raw_activity = WandererApp.Map.get_character_activity(map_id)
with {:ok, map_id} <- Util.fetch_map_id(params),
{:ok, days} <- parse_days(params["days"]) do
# Get raw activity data (filtered by days if provided, otherwise all activity)
raw_activity = WandererApp.Map.get_character_activity(map_id, days)
# Group activities by user_id and summarize
summarized_result =
@@ -744,6 +857,15 @@ defmodule WandererAppWeb.MapAPIController do
end
end
# Parse days parameter, return nil if not provided to show all activity
defp parse_days(nil), do: {:ok, nil}
defp parse_days(days_str) do
case Integer.parse(days_str) do
{days, ""} when days > 0 -> {:ok, days}
_ -> {:ok, nil} # Return nil if invalid to show all activity
end
end
# If hours_str is present and valid, parse it. Otherwise return nil (no filter).
defp parse_hours_ago(nil), do: nil
defp parse_hours_ago(hours_str) do
@@ -950,6 +1072,22 @@ defmodule WandererAppWeb.MapAPIController do
end
end
defp connection_to_json(c) do
Map.take(c, [
:id,
:map_id,
:solar_system_source,
:solar_system_target,
:mass_status,
:time_status,
:ship_size_type,
:type,
:wormhole_type,
:inserted_at,
:updated_at
])
end
defp character_to_json(ch) do
WandererAppWeb.MapEventHandler.map_ui_character_stat(ch)
end

View File

@@ -54,14 +54,20 @@ defmodule WandererAppWeb.MapCharacters do
>
Tracked
</span>
<span :if={is_online?(@character.id)} class="text-green-500 rounded-full px-2 py-1">
Online
</span>
<span :if={not is_online?(@character.id)} class="text-red-500 rounded-full px-2 py-1">
Offline
</span>
<div class="avatar">
<div class="rounded-md w-8 h-8">
<img src={member_icon_url(@character.eve_id)} alt={@character.name} />
</div>
</div>
<span><%= @character.name %></span>
<span :if={@character.alliance_ticker}>[<%= @character.alliance_ticker %>]</span>
<span :if={@character.corporation_ticker}>[<%= @character.corporation_ticker %>]</span>
<span>{@character.name}</span>
<span :if={@character.alliance_ticker}>[{@character.alliance_ticker}]</span>
<span :if={@character.corporation_ticker}>[{@character.corporation_ticker}]</span>
</div>
"""
end
@@ -79,4 +85,8 @@ defmodule WandererAppWeb.MapCharacters do
end)
end
defp is_online?(character_id) do
{:ok, state} = WandererApp.Character.get_character_state(character_id)
state.is_online
end
end

View File

@@ -126,7 +126,13 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
}
} = socket
) do
case WandererApp.Character.TrackingUtils.toggle_track(map_id, character_eve_id, current_user.id, self(), only_tracked_characters) do
case WandererApp.Character.TrackingUtils.toggle_track(
map_id,
character_eve_id,
current_user.id,
self(),
only_tracked_characters
) do
{:ok, tracking_data, event} ->
# Send the appropriate event based on the result from toggle_track
Process.send_after(self(), event, 10)
@@ -171,14 +177,21 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
%{"character_id" => clicked_char_id},
%{assigns: %{current_user: current_user, map_id: map_id}} = socket
) do
case WandererApp.Character.TrackingUtils.toggle_follow(map_id, clicked_char_id, current_user.id, self()) do
case WandererApp.Character.TrackingUtils.toggle_follow(
map_id,
clicked_char_id,
current_user.id,
self()
) do
{:ok, tracking_data, event} ->
# Send the appropriate event based on the result from toggle_follow
Process.send_after(self(), event, 10)
{:noreply,
socket
|> MapEventHandler.push_map_event("tracking_characters_data", %{characters: tracking_data})}
|> MapEventHandler.push_map_event("tracking_characters_data", %{
characters: tracking_data
})}
{:error, reason} ->
Logger.error("Failed to toggle follow: #{inspect(reason)}")
@@ -210,7 +223,11 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|> Map.put_new(:location, get_location(character))
defp get_location(character),
do: %{solar_system_id: character.solar_system_id, structure_id: character.structure_id}
do: %{
solar_system_id: character.solar_system_id,
structure_id: character.structure_id,
station_id: character.station_id
}
@doc """
Initializes character tracking for a map. This is called when the map is first loaded.
@@ -292,8 +309,17 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
end
defp handle_tracking_event({:track_characters, map_characters, track_character}, socket, map_id) do
:ok = WandererApp.Character.TrackingUtils.track_characters(map_characters, map_id, track_character, self())
:ok = WandererApp.Character.TrackingUtils.add_characters(map_characters, map_id, track_character)
:ok =
WandererApp.Character.TrackingUtils.track_characters(
map_characters,
map_id,
track_character,
self()
)
:ok =
WandererApp.Character.TrackingUtils.add_characters(map_characters, map_id, track_character)
socket
end

View File

@@ -10,6 +10,14 @@
id="map-character-list"
class="pt-20 w-full h-full col-span-2 lg:col-span-1 p-4 pl-20 pb-20 overflow-auto"
>
<div class="mb-6 p-4 border rounded-md flex gap-2 items-center">
<.icon name="hero-information-circle-mini" class="h-5 w-5" />
<p>
'Untrack' characters leading to completely remove them from the map, required manually enable the tracking by users later.
</p>
</div>
<div class="flex flex-col gap-4 w-full">
<.live_component
module={MapCharacters}

View File

@@ -208,6 +208,7 @@ defmodule WandererAppWeb.Router do
get "/audit", MapAuditAPIController, :index
get "/systems", MapAPIController, :list_systems
get "/system", MapAPIController, :show_system
get "/connections", MapAPIController, :list_connections
get "/characters", MapAPIController, :tracked_characters_with_info
get "/structure-timers", MapAPIController, :show_structure_timers
get "/character-activity", MapAPIController, :character_activity

View File

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

View File

@@ -218,7 +218,49 @@ curl "https://wanderer.example.com/api/common/system-static-info?id=31002229"
}
```
### 4. List Tracked Characters
### 4. List Connections
```bash
GET /api/map/connections?map_id=<UUID>
GET /api/map/connections?slug=<map-slug>
```
- **Description:** Retrieves a list of connections associated with the specified map.
- **Authentication:** Requires Map API Token.
- **Parameters:**
- `map_id` (optional if `slug` is provided) — the UUID of the map.
- `slug` (optional if `map_id` is provided) — the slug identifier of the map.
#### Example Request
```bash
curl -H "Authorization: Bearer <REDACTED_TOKEN>" \
"https://wanderer.example.com/api/map/connections?slug=some-slug"
```
#### Example Response
```json
{
"data": [
{
"id": "<REDACTED_ID>",
"type": 0,
"mass_status": 0,
"ship_size_type": 2,
"time_status": 0,
"map_id": "<REDACTED_ID>",
"inserted_at": "2025-02-27T01:59:51.632416Z",
"updated_at": "2025-02-27T01:59:51.632416Z",
"solar_system_target": 30003071,
"solar_system_source": 31000747,
"wormhole_type": null
},
...
]
}
```
### 5. List Tracked Characters
```bash
GET /api/map/characters?map_id=<UUID>
@@ -261,7 +303,7 @@ curl -H "Authorization: Bearer <REDACTED_TOKEN>" \
}
```
### 5. Kills Activity
### 6. Kills Activity
```bash
GET /api/map/systems-kills?map_id=<UUID>
@@ -333,11 +375,12 @@ curl -H "Authorization: Bearer <REDACTED_TOKEN>" \
}
```
### 6. Character Activity
### 7. Character Activity
```bash
GET /api/map/character-activity?map_id=<UUID>
GET /api/map/character-activity?slug=<map-slug>
GET /api/map/character-activity?map_id=<UUID>&days=7
```
- **Description:** Retrieves character activity data for a map, including passages, connections, and signatures.
@@ -345,12 +388,13 @@ GET /api/map/character-activity?slug=<map-slug>
- **Parameters:**
- `map_id` (optional if `slug` is provided) — the UUID of the map.
- `slug` (optional if `map_id` is provided) — the slug identifier of the map.
- `days` (optional) — if provided, filters activity data to only include records from the specified number of days. If not provided, returns all activity history.
#### Example Request
```bash
curl -H "Authorization: Bearer <REDACTED_TOKEN>" \
"https://wanderer.example.com/api/map/character-activity?slug=some-slug"
"https://wanderer.example.com/api/map/character-activity?slug=some-slug&days=7"
```
#### Example Response
@@ -377,7 +421,7 @@ curl -H "Authorization: Bearer <REDACTED_TOKEN>" \
}
```
### 7. Structure Timers
### 8. Structure Timers
```bash
GET /api/map/structure-timers?map_id=<UUID>
@@ -859,7 +903,7 @@ curl -X DELETE \
{ "ok": true }
```
---
----
## Conclusion
@@ -876,9 +920,9 @@ For the most up-to-date and interactive documentation, we recommend using the Sw
If you have any questions or need assistance with the API, please reach out to the Wanderer Team.
---
----
Fly safe,
Fly safe,
**The Wanderer Team**
---
----

View File

@@ -1,39 +1,19 @@
%{
title: "Get Real-Time Notifications with Wanderer Notifier",
author: "Wanderer Team",
cover_image_uri: "/images/news/03-18-bots/dashboard.png",
tags: ~w(notifier discord notifications docker user-guide),
description: "Download and run Wanderer Notifier to receive real-time notifications in your Discord channel. Learn how to get started with our Docker image and discover the different alerts you'll receive."
title: "Get Real-Time Notifications with Wanderer Notifier",
author: "Wanderer Team",
cover_image_uri: "/images/news/03-18-bots/dashboard.png",
tags: ~w(notifier discord notifications docker user-guide),
description: "Download and run Wanderer Notifier to receive real-time notifications in your Discord channel. Learn how to get started with our Docker image and discover the different alerts you'll receive."
}
---
# Get Real-Time Notifications with Wanderer Notifier
Wanderer Notifier delivers real-time alerts directly to your Discord channel, ensuring you never miss critical in-game events. Whether it's a significant kill, a newly tracked character, or a fresh system discovery, our notifier keeps you informed with rich, detailed notifications.
[Wanderer Notifier](https://guarzo.github.io/wanderer-notifier/) delivers real-time alerts directly to your Discord channel, ensuring you never miss critical in-game events. Whether it's a significant kill, a newly tracked character, or a fresh system discovery, our notifier keeps you informed with rich, detailed notifications.
In the fast-paced universe of EVE Online, timely information can mean the difference between success and failure. When a hostile fleet enters your territory, when a high-value target appears in your hunting grounds, or when a new wormhole connection opens up valuable opportunities - knowing immediately gives you the edge. Wanderer Notifier bridges this information gap, bringing critical intel directly to your Discord where your team is already coordinating.
## Table of Contents
- [Prerequisites](#prerequisites)
- [How to Get Started](#how-to-get-started)
- [Quick Install Option](#quick-install-option)
- [Manual Setup](#manual-setup)
- [Notification Types](#notification-types)
- [Kill Notifications](#kill-notifications)
- [Character Tracking Notifications](#character-tracking-notifications)
- [System Notifications](#system-notifications)
- [Map Subscription Features & Limitations](#map-subscription-features--limitations)
- [Free Version Features](#free-version-features)
- [Premium Map Subscription Enhancements](#premium-map-subscription-enhancements)
- [How to Subscribe](#how-to-subscribe)
- [Feature Comparison](#feature-comparison)
- [Web Dashboard](#web-dashboard)
- [Configuration Options](#configuration-options)
- [Troubleshooting](#troubleshooting)
- [Updating Wanderer Notifier](#updating-wanderer-notifier)
- [Conclusion](#conclusion)
## Prerequisites
Before setting up Wanderer Notifier, ensure you have the following:
@@ -87,10 +67,9 @@ LICENSE_KEY=your_map_license_key # Provided with your map subscription
# Notification Control (all enabled by default)
# ENABLE_KILL_NOTIFICATIONS=true
# ENABLE_CHARACTER_TRACKING=true
# ENABLE_CHARACTER_NOTIFICATIONS=true
# ENABLE_SYSTEM_NOTIFICATIONS=true
# TRACK_ALL_SYSTEMS=false
# ENABLE_TRACK_KSPACE_SYSTEMS=false # Set to 'true' to track K-Space systems in addition to wormholes
```
> **Note:** If you don't have a Discord bot yet, follow our [guide on creating a Discord bot](https://gist.github.com/guarzo/a4d238b932b6a168ad1c5f0375c4a561) or search the web for more information.
@@ -102,7 +81,7 @@ Create a file named `docker-compose.yml` with the following content:
```yaml
services:
wanderer_notifier:
image: guarzo/wanderer-notifier:latest
image: guarzo/wanderer-notifier:v1
container_name: wanderer_notifier
restart: unless-stopped
environment:
@@ -116,7 +95,14 @@ services:
volumes:
- wanderer_data:/app/data
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:${PORT:-4000}/health"]
test:
[
"CMD",
"wget",
"-q",
"--spider",
"http://localhost:${PORT:-4000}/health",
]
interval: 30s
timeout: 3s
retries: 3
@@ -155,6 +141,7 @@ When a kill occurs in a tracked system or involves a tracked character:
- **With Premium Map Subscription:**
Receives a rich embed that includes:
- Ship thumbnail image
- Detailed information about both victim and attacker
- Links to zKillboard profiles
@@ -167,6 +154,7 @@ When a kill occurs in a tracked system or involves a tracked character:
- **With Free Map:**
Displays a basic text notification containing:
- Victim name
- Ship type lost
- System name
@@ -179,6 +167,7 @@ When a new character is added to your tracked list:
- **With Premium Map Subscription:**
You get a rich embed featuring:
- Character portrait
- Corporation details
- Direct link to the zKillboard profile
@@ -188,6 +177,7 @@ When a new character is added to your tracked list:
- **With Free Map:**
Receives a simple text notification that includes:
- Character name
- Corporation name (if available)
@@ -199,6 +189,7 @@ When a new system is discovered or added to your map:
- **With Premium Map Subscription:**
Shows a rich embed with:
- System name (including aliases/temporary names)
- System type icon
- Region information or wormhole statics
@@ -210,6 +201,7 @@ When a new system is discovered or added to your map:
- **With Free Map:**
Provides a basic text notification including:
- Original system name (for wormholes)
- System name (for k-space)
@@ -240,21 +232,21 @@ Wanderer Notifier offers enhanced functionality with a premium map subscription
To unlock the enhanced features of Wanderer Notifier:
1. Visit our [Map Subscriptions page](/map-subscriptions) to learn about subscription options
1. Visit our [Map Subscriptions page](https://wanderer.ltd/news/map-subscriptions) to learn about subscription options
2. Subscribe to any premium map tier to receive your map subscription key
3. Add your map subscription key to the LICENSE_KEY field in your `.env` file
4. Restart the notifier to apply your subscription benefits
For more details on map subscription tiers and pricing, see our [complete guide to map subscriptions](/map-subscriptions).
For more details on map subscription tiers and pricing, see our [complete guide to map subscriptions](https://wanderer.ltd/news/map-subscriptions).
### Feature Comparison
| Feature | Free Map | Premium Map Subscription |
|--------------------------|----------|--------------------------|
| Kill Tracking | Unlimited| Unlimited |
| System Tracking | Unlimited| Unlimited |
| Character Tracking | Unlimited| Unlimited |
| Notification Format | Basic Text| Rich Embeds |
| Feature | Free Map | Premium Map Subscription |
| ------------------- | ---------- | ------------------------ |
| Kill Tracking | Unlimited | Unlimited |
| System Tracking | Unlimited | Unlimited |
| Character Tracking | Unlimited | Unlimited |
| Notification Format | Basic Text | Rich Embeds |
---
@@ -280,13 +272,9 @@ Customize your notification experience with several configuration options availa
### Notification Control Variables
- **ENABLE_KILL_NOTIFICATIONS:** Enable/disable kill notifications (default: true).
- **ENABLE_CHARACTER_TRACKING:** Enable/disable character tracking (default: true).
- **ENABLE_CHARACTER_NOTIFICATIONS:** Enable/disable notifications when new characters are added (default: true).
- **ENABLE_SYSTEM_NOTIFICATIONS:** Enable/disable system notifications (default: true).
> **Note:**
> - **Character Tracking:** Determines whether the application monitors characters.
> - **Character Notifications:** Controls whether you receive Discord alerts when new characters are added.
- **ENABLE_TRACK_KSPACE_SYSTEMS:** Enable/disable tracking of K-Space (non-wormhole) systems (default: false).
To disable a notification type, set the corresponding variable to `false` or `0` in your `.env` file:

View File

@@ -0,0 +1,21 @@
defmodule WandererApp.Repo.Migrations.AddCharacterStationId do
@moduledoc """
Updates resources based on their most recent snapshots.
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""
use Ecto.Migration
def up do
alter table(:character_v1) do
add :encrypted_station_id, :binary
end
end
def down do
alter table(:character_v1) do
remove :encrypted_station_id
end
end
end

View File

@@ -0,0 +1,323 @@
{
"attributes": [
{
"allow_nil?": false,
"default": "fragment(\"gen_random_uuid()\")",
"generated?": false,
"primary_key?": true,
"references": null,
"size": null,
"source": "id",
"type": "uuid"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "eve_id",
"type": "text"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "name",
"type": "text"
},
{
"allow_nil?": true,
"default": "false",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "online",
"type": "boolean"
},
{
"allow_nil?": true,
"default": "false",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "deleted",
"type": "boolean"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "scopes",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "character_owner_hash",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "token_type",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "expires_at",
"type": "bigint"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "ship_name",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "corporation_id",
"type": "bigint"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "corporation_name",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "corporation_ticker",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "alliance_id",
"type": "bigint"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "alliance_name",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "alliance_ticker",
"type": "text"
},
{
"allow_nil?": false,
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "inserted_at",
"type": "utc_datetime_usec"
},
{
"allow_nil?": false,
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "updated_at",
"type": "utc_datetime_usec"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": {
"deferrable": false,
"destination_attribute": "id",
"destination_attribute_default": null,
"destination_attribute_generated": null,
"index?": false,
"match_type": null,
"match_with": null,
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"name": "character_v1_user_id_fkey",
"on_delete": null,
"on_update": null,
"primary_key?": true,
"schema": "public",
"table": "user_v1"
},
"size": null,
"source": "user_id",
"type": "uuid"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "encrypted_eve_wallet_balance",
"type": "binary"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "encrypted_location",
"type": "binary"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "encrypted_ship",
"type": "binary"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "encrypted_solar_system_id",
"type": "binary"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "encrypted_structure_id",
"type": "binary"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "encrypted_station_id",
"type": "binary"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "encrypted_access_token",
"type": "binary"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "encrypted_refresh_token",
"type": "binary"
}
],
"base_filter": null,
"check_constraints": [],
"custom_indexes": [],
"custom_statements": [],
"has_create_action": true,
"hash": "0027E0C4A584B6AB536CE623A060E27EC57E6602422A4B9D2DED931EF2337804",
"identities": [
{
"all_tenants?": false,
"base_filter": null,
"index_name": "character_v1_unique_eve_id_index",
"keys": [
{
"type": "atom",
"value": "eve_id"
}
],
"name": "unique_eve_id",
"nils_distinct?": true,
"where": null
}
],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.WandererApp.Repo",
"schema": null,
"table": "character_v1"
}

View File

@@ -1,13 +0,0 @@
# Wanderer API Testing Tool Configuration
# Generated on Thu Mar 6 14:52:00 UTC 2025
# Base configuration
HOST="http://localhost:4444"
MAP_SLUG="flygd"
MAP_API_KEY="589016d9-c9ac-48ef-ae74-7a55483b3cc2"
ACL_API_KEY=""
# Selected IDs
SELECTED_ACL_ID=""
SELECTED_SYSTEM_ID=""
CHARACTER_EVE_ID=""

View File

@@ -1,13 +0,0 @@
# Wanderer API Testing Tool Configuration
# Example configuration file - Copy to .api_test_config and modify as needed
# Base configuration
HOST="http://localhost:4000"
MAP_SLUG="flygd"
MAP_API_KEY="589016d9-c9ac-48ef-ae74-7a55483b3cc2"
ACL_API_KEY="acl-api-key-here"
# Selected IDs
SELECTED_ACL_ID="123"
SELECTED_SYSTEM_ID="31002019"
CHARACTER_EVE_ID="456"

View File

@@ -1,13 +0,0 @@
# Wanderer API Testing Tool Configuration
# Generated on Thu Mar 6 18:44:20 UTC 2025
# Base configuration
HOST="http://localhost:4444"
MAP_SLUG="flygd"
MAP_API_KEY="589016d9-c9ac-48ef-ae74-7a55483b3cc2"
ACL_API_KEY="116bd70e-2bbf-4a99-97ed-1869c09ab5bf"
# Selected IDs
SELECTED_ACL_ID="9c91d283-f49f-4f45-a21d-9bf53ce9d1fd"
SELECTED_SYSTEM_ID="30002768"
CHARACTER_EVE_ID="2115754172"

View File

@@ -1,894 +0,0 @@
#!/bin/bash
#==============================================================================
# Wanderer API Automated Testing Tool
#
# This script tests various endpoints of the Wanderer API.
#
# Features:
# - Uses strict mode (set -euo pipefail) for robust error handling.
# - Contains a DEBUG mode for extra logging (set DEBUG=1 to enable).
# - Validates configuration including a reachability test for the HOST.
# - Outputs a summary in plain text and optionally as JSON.
# - Exits with a nonzero code if any test fails.
#
# Usage:
# ./auto_test_api.sh
#
#==============================================================================
set -euo pipefail
IFS=$'\n\t'
# Set DEBUG=1 to enable extra logging
DEBUG=0
# Set VERBOSE=1 to print raw JSON responses for every test (default 0)
VERBOSE=0
# Set VERBOSE_SUMMARY=1 to output a JSON summary at the end (default 0)
VERBOSE_SUMMARY=0
# Colors for output
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Configuration file and default configuration
CONFIG_FILE=".auto_api_test_config"
HOST="http://localhost:4444" # Default host
MAP_SLUG=""
MAP_API_KEY=""
ACL_API_KEY=""
SELECTED_ACL_ID=""
SELECTED_SYSTEM_ID=""
CHARACTER_EVE_ID=""
TEST_RESULTS=()
FAILED_TESTS=()
# Global variables for last API response
LAST_JSON_RESPONSE=""
LAST_HTTP_CODE=""
#------------------------------------------------------------------------------
# Helper Functions
#------------------------------------------------------------------------------
debug() {
if [ "$DEBUG" -eq 1 ]; then
echo -e "${YELLOW}[DEBUG] $*${NC}" >&2
fi
}
print_header() {
echo -e "\n${BLUE}=== $1 ===${NC}\n"
}
print_success() {
echo -e "${GREEN}$1${NC}"
}
print_warning() {
echo -e "${YELLOW}$1${NC}" >&2
}
print_error() {
echo -e "${RED}$1${NC}"
}
# Check if the host is reachable; accept any HTTP status code 200-399.
check_host_reachable() {
debug "Checking if host $HOST is reachable..."
local status
status=$(curl -s -o /dev/null -w "%{http_code}" "$HOST")
debug "HTTP status code for host: $status"
if [[ "$status" -ge 200 && "$status" -lt 400 ]]; then
print_success "Host $HOST is reachable."
else
print_error "Host $HOST is not reachable (HTTP code: $status). Please check the host URL."
exit 1
fi
}
# Load configuration from file
load_config() {
if [ -f "$CONFIG_FILE" ]; then
print_success "Loading configuration from $CONFIG_FILE"
source "$CONFIG_FILE"
return 0
else
print_warning "No configuration file found. Using default values."
return 1
fi
}
# Save configuration to file
save_config() {
print_success "Saving configuration to $CONFIG_FILE"
cat > "$CONFIG_FILE" << EOF
# Wanderer API Testing Tool Configuration
# Generated on $(date)
# Base configuration
HOST="$HOST"
MAP_SLUG="$MAP_SLUG"
MAP_API_KEY="$MAP_API_KEY"
ACL_API_KEY="$ACL_API_KEY"
# Selected IDs
SELECTED_ACL_ID="$SELECTED_ACL_ID"
SELECTED_SYSTEM_ID="$SELECTED_SYSTEM_ID"
CHARACTER_EVE_ID="$CHARACTER_EVE_ID"
EOF
chmod 600 "$CONFIG_FILE"
print_success "Configuration saved successfully."
}
# Make an API call using curl and capture response and HTTP code
call_api() {
local method=$1
local endpoint=$2
local api_key=$3
local data=${4:-""}
local curl_cmd=(curl -s -w "\n%{http_code}" -X "$method" -H "Content-Type: application/json")
if [ -n "$api_key" ]; then
curl_cmd+=(-H "Authorization: Bearer $api_key")
fi
if [ -n "$data" ]; then
curl_cmd+=(-d "$data")
fi
curl_cmd+=("$HOST$endpoint")
# Print debug command (mask API key)
local debug_cmd
debug_cmd=$(printf "%q " "${curl_cmd[@]}")
debug_cmd=$(echo "$debug_cmd" | sed "s/$api_key/API_KEY_HIDDEN/g")
print_warning "Executing: $debug_cmd"
local output
output=$("${curl_cmd[@]}")
LAST_HTTP_CODE=$(echo "$output" | tail -n1)
local response
response=$(echo "$output" | sed '$d')
echo "$response"
}
# Check that required variables are set
check_required_vars() {
local missing=false
if [ $# -eq 0 ]; then
if [ -z "$HOST" ]; then
print_error "HOST is not set. Please set it first."
missing=true
fi
if [ -z "$MAP_SLUG" ]; then
print_error "MAP_SLUG is not set. Please set it first."
missing=true
fi
if [ -z "$MAP_API_KEY" ]; then
print_error "MAP_API_KEY is not set. Please set it first."
missing=true
fi
else
for var in "$@"; do
if [ -z "${!var}" ]; then
print_error "$var is not set. Please set it first."
missing=true
fi
done
fi
$missing && return 1 || return 0
}
# Record a test result
record_test_result() {
local endpoint=$1
local status=$2
local message=$3
if [ "$status" = "success" ]; then
TEST_RESULTS+=("${GREEN}${NC} $endpoint - $message")
else
TEST_RESULTS+=("${RED}${NC} $endpoint - $message")
FAILED_TESTS+=("$endpoint - $message")
fi
}
# Process and validate the JSON response
check_response() {
local response=$1
local endpoint=$2
if [ -z "$(echo "$response" | xargs)" ]; then
if [ "$LAST_HTTP_CODE" = "200" ] || [ "$LAST_HTTP_CODE" = "204" ]; then
print_success "Received empty response, which is valid"
LAST_JSON_RESPONSE="{}"
return 0
else
record_test_result "$endpoint" "failure" "Empty response with HTTP code $LAST_HTTP_CODE"
return 1
fi
fi
if [ "$VERBOSE" -eq 1 ]; then
echo "Raw response from $endpoint:"
echo "$response" | head -n 20
fi
if echo "$response" | jq . > /dev/null 2>&1; then
LAST_JSON_RESPONSE="$response"
return 0
fi
local json_part
json_part=$(echo "$response" | grep -o '{.*}' || echo "")
if [ -z "$json_part" ] || ! echo "$json_part" | jq . > /dev/null 2>&1; then
json_part=$(echo "$response" | sed -n '/^{/,$p' | tr -d '\n')
fi
if [ -z "$json_part" ] || ! echo "$json_part" | jq . > /dev/null 2>&1; then
json_part=$(echo "$response" | sed -n '/{/,/}/p' | tr -d '\n')
fi
if [ -z "$json_part" ] || ! echo "$json_part" | jq . > /dev/null 2>&1; then
json_part=$(echo "$response" | awk '!(/^[<>*]/) {print}' | tr -d '\n')
fi
if [ -z "$json_part" ] || ! echo "$json_part" | jq . > /dev/null 2>&1; then
echo "Raw response from $endpoint:"
echo "$response"
record_test_result "$endpoint" "failure" "Invalid JSON response"
return 1
fi
local error
error=$(echo "$json_part" | jq -r '.error // empty')
if [ -n "$error" ]; then
echo "Raw response from $endpoint:"
echo "$response"
echo "Parsed JSON response from $endpoint:"
echo "$json_part" | jq '.'
record_test_result "$endpoint" "failure" "Error: $error"
return 1
fi
LAST_JSON_RESPONSE="$json_part"
return 0
}
# Get a random item from a JSON array using a jq path
get_random_item() {
local json=$1
local jq_path=$2
local count
count=$(echo "$json" | jq "$jq_path | length")
if [ "$count" -eq 0 ]; then
echo ""
return 1
fi
local random_index=$((RANDOM % count))
echo "$json" | jq -r "$jq_path[$random_index]"
}
#------------------------------------------------------------------------------
# API Test Functions
#------------------------------------------------------------------------------
test_list_characters() {
print_header "Testing GET /api/characters"
print_success "Calling API: GET /api/characters"
local response
response=$(call_api "GET" "/api/characters" "$MAP_API_KEY")
if ! check_response "$response" "GET /api/characters"; then
return 1
fi
local character_count
character_count=$(echo "$LAST_JSON_RESPONSE" | jq '.data | length')
if [ "$character_count" -gt 0 ]; then
record_test_result "GET /api/characters" "success" "Found $character_count characters"
if [ -z "$CHARACTER_EVE_ID" ]; then
local random_index=$((RANDOM % character_count))
print_success "Selecting character at index $random_index"
local random_character
random_character=$(echo "$LAST_JSON_RESPONSE" | jq ".data[$random_index]")
CHARACTER_EVE_ID=$(echo "$random_character" | jq -r '.eve_id')
local character_name
character_name=$(echo "$random_character" | jq -r '.name')
print_success "Selected random character: $character_name (EVE ID: $CHARACTER_EVE_ID)"
fi
return 0
else
record_test_result "GET /api/characters" "success" "No characters found"
return 0
fi
}
test_map_systems() {
print_header "Testing GET /api/map/systems"
if ! check_required_vars "MAP_SLUG" "MAP_API_KEY"; then
record_test_result "GET /api/map/systems" "failure" "Missing required variables"
return 1
fi
print_success "Calling API: GET /api/map/systems?slug=$MAP_SLUG"
local response
response=$(call_api "GET" "/api/map/systems?slug=$MAP_SLUG" "$MAP_API_KEY")
if ! check_response "$response" "GET /api/map/systems"; then
return 1
fi
local system_count
system_count=$(echo "$LAST_JSON_RESPONSE" | jq '.data | length')
print_success "System count: $system_count"
if [ "$system_count" -gt 0 ]; then
record_test_result "GET /api/map/systems" "success" "Found $system_count systems"
local random_index=$((RANDOM % system_count))
print_success "Selecting system at index $random_index"
echo "Data structure:"
echo "$LAST_JSON_RESPONSE" | jq '.data[0]'
local random_system
random_system=$(echo "$LAST_JSON_RESPONSE" | jq ".data[$random_index]")
echo "Selected system JSON:"
echo "$random_system"
SELECTED_SYSTEM_ID=$(echo "$random_system" | jq -r '.solar_system_id')
if [ -z "$SELECTED_SYSTEM_ID" ] || [ "$SELECTED_SYSTEM_ID" = "null" ]; then
SELECTED_SYSTEM_ID=$(echo "$random_system" | jq -r '.id // .system_id // empty')
if [ -z "$SELECTED_SYSTEM_ID" ] || [ "$SELECTED_SYSTEM_ID" = "null" ]; then
print_error "Could not find system ID in the response"
echo "Available fields:"
echo "$random_system" | jq 'keys'
record_test_result "GET /api/map/systems" "failure" "Could not extract system ID"
return 1
fi
fi
local system_name
system_name=$(echo "$random_system" | jq -r '.name // "Unknown"')
print_success "Selected random system: $system_name (ID: $SELECTED_SYSTEM_ID)"
return 0
else
record_test_result "GET /api/map/systems" "failure" "No systems found"
return 1
fi
}
test_map_system() {
print_header "Testing GET /api/map/system"
if [[ -z "$MAP_SLUG" || -z "$SELECTED_SYSTEM_ID" || -z "$MAP_API_KEY" ]]; then
record_test_result "GET /api/map/system" "failure" "Missing required variables"
return
fi
local response
response=$(call_api "GET" "/api/map/system?slug=$MAP_SLUG&id=$SELECTED_SYSTEM_ID" "$MAP_API_KEY")
print_warning "Response: $response"
local trimmed_response
trimmed_response=$(echo "$response" | xargs)
if [[ "$trimmed_response" == "{}" || "$trimmed_response" == '{"data":{}}' ]]; then
print_success "Received empty JSON response, which is valid"
record_test_result "GET /api/map/system" "success" "Received valid empty response"
return
fi
if ! check_response "$response" "GET /api/map/system"; then
return
fi
local json_data="$LAST_JSON_RESPONSE"
local has_data
has_data=$(echo "$json_data" | jq 'has("data")')
if [ "$has_data" != "true" ]; then
print_error "Response does not contain 'data' field"
echo "JSON Response:"
echo "$json_data" | jq .
record_test_result "GET /api/map/system" "failure" "Response does not contain 'data' field"
return
fi
local system_data
system_data=$(echo "$json_data" | jq -r '.data // empty')
if [ -z "$system_data" ] || [ "$system_data" = "null" ]; then
print_error "Could not find system data in response"
echo "JSON Response:"
echo "$json_data" | jq .
record_test_result "GET /api/map/system" "failure" "Could not find system data in response"
return
fi
local system_id
system_id=$(echo "$json_data" | jq -r '.data.solar_system_id // empty')
if [ -z "$system_id" ] || [ "$system_id" = "null" ]; then
print_error "Could not find solar_system_id in the system data"
echo "System Data:"
echo "$system_data" | jq .
record_test_result "GET /api/map/system" "failure" "Could not find solar_system_id in system data"
return
fi
print_success "Found system data with ID: $system_id"
record_test_result "GET /api/map/system" "success" "Found system data with ID: $system_id"
}
test_map_characters() {
print_header "Testing GET /api/map/characters"
if ! check_required_vars "MAP_SLUG" "MAP_API_KEY"; then
record_test_result "GET /api/map/characters" "failure" "Missing required variables"
return 1
fi
print_success "Calling API: GET /api/map/characters?slug=$MAP_SLUG"
local response
response=$(call_api "GET" "/api/map/characters?slug=$MAP_SLUG" "$MAP_API_KEY")
if ! check_response "$response" "GET /api/map/characters"; then
return 1
fi
local character_count
character_count=$(echo "$LAST_JSON_RESPONSE" | jq '.data | length')
record_test_result "GET /api/map/characters" "success" "Found $character_count tracked characters"
return 0
}
test_map_structure_timers() {
print_header "Testing GET /api/map/structure-timers"
if [[ -z "$MAP_SLUG" || -z "$MAP_API_KEY" ]]; then
record_test_result "GET /api/map/structure-timers" "failure" "Missing required variables"
return
fi
local response
response=$(call_api "GET" "/api/map/structure-timers?slug=$MAP_SLUG" "$MAP_API_KEY")
local trimmed_response
trimmed_response=$(echo "$response" | xargs)
if [[ "$trimmed_response" == '{"data":[]}' ]]; then
print_success "Found 0 structure timers"
record_test_result "GET /api/map/structure-timers" "success" "Found 0 structure timers"
fi
if ! check_response "$response" "GET /api/map/structure-timers"; then
return
fi
local timer_count
timer_count=$(echo "$LAST_JSON_RESPONSE" | jq '.data | length')
print_success "Found $timer_count structure timers"
record_test_result "GET /api/map/structure-timers" "success" "Found $timer_count structure timers"
if [ -n "$SELECTED_SYSTEM_ID" ]; then
print_header "Testing GET /api/map/structure-timers (filtered)"
local filtered_response
filtered_response=$(call_api "GET" "/api/map/structure-timers?slug=$MAP_SLUG&system_id=$SELECTED_SYSTEM_ID" "$MAP_API_KEY")
print_warning "(Structure Timers) - Filtered response: $filtered_response"
local trimmed_filtered
trimmed_filtered=$(echo "$filtered_response" | xargs)
if [[ "$trimmed_filtered" == '{"data":[]}' ]]; then
print_success "Found 0 filtered structure timers"
record_test_result "GET /api/map/structure-timers (filtered)" "success" "Found 0 filtered structure timers"
return
fi
if ! check_response "$filtered_response" "GET /api/map/structure-timers (filtered)"; then
return
fi
local filtered_count
filtered_count=$(echo "$LAST_JSON_RESPONSE" | jq '.data | length')
print_success "Found $filtered_count filtered structure timers"
record_test_result "GET /api/map/structure-timers (filtered)" "success" "Found $filtered_count filtered structure timers"
fi
}
test_map_systems_kills() {
print_header "Testing GET /api/map/systems-kills"
if [[ -z "$MAP_SLUG" || -z "$MAP_API_KEY" ]]; then
record_test_result "GET /api/map/systems-kills" "failure" "Missing required variables"
return
fi
# Use the correct parameter name: hours
local response
response=$(call_api "GET" "/api/map/systems-kills?slug=$MAP_SLUG&hours=1" "$MAP_API_KEY")
print_warning "(Systems Kills) - Response: $response"
if ! check_response "$response" "GET /api/map/systems-kills"; then
return
fi
local json_data="$LAST_JSON_RESPONSE"
if [ "$VERBOSE" -eq 1 ]; then
echo "JSON Response:"; echo "$json_data" | jq .
fi
local has_data
has_data=$(echo "$json_data" | jq 'has("data")')
if [ "$has_data" != "true" ]; then
print_error "Response does not contain 'data' field"
if [ "$VERBOSE" -eq 1 ]; then
echo "JSON Response:"; echo "$json_data" | jq .
fi
record_test_result "GET /api/map/systems-kills" "failure" "Response does not contain 'data' field"
return
fi
local systems_count
systems_count=$(echo "$json_data" | jq '.data | length')
print_success "Found kill data for $systems_count systems"
record_test_result "GET /api/map/systems-kills" "success" "Found kill data for $systems_count systems"
print_header "Testing GET /api/map/systems-kills (filtered)"
local filter_url="/api/map/systems-kills?slug=$MAP_SLUG&hours=1"
if [ -n "$SELECTED_SYSTEM_ID" ]; then
filter_url="$filter_url&system_id=$SELECTED_SYSTEM_ID"
print_success "Using system_id filter to reduce response size"
fi
local filtered_response
filtered_response=$(call_api "GET" "$filter_url" "$MAP_API_KEY")
local trimmed_filtered
trimmed_filtered=$(echo "$filtered_response" | xargs)
if [[ "$trimmed_filtered" == '{"data":[]}' ]]; then
print_success "Found 0 filtered systems with kill data"
record_test_result "GET /api/map/systems-kills (filtered)" "success" "Found 0 filtered systems with kill data"
return
fi
if [[ "$trimmed_filtered" == '{"data":'* ]]; then
print_success "Received valid JSON response (large data)"
record_test_result "GET /api/map/systems-kills (filtered)" "success" "Received valid JSON response with kill data"
return
fi
if ! check_response "$filtered_response" "GET /api/map/systems-kills (filtered)"; then
return
fi
local filtered_count
filtered_count=$(echo "$LAST_JSON_RESPONSE" | jq '.data | length')
print_success "Found filtered kill data for $filtered_count systems"
record_test_result "GET /api/map/systems-kills (filtered)" "success" "Found filtered kill data for $filtered_count systems"
}
test_map_acls() {
print_header "Testing GET /api/map/acls"
if ! check_required_vars "MAP_SLUG" "MAP_API_KEY"; then
record_test_result "GET /api/map/acls" "failure" "Missing required variables"
return 1
fi
print_success "Calling API: GET /api/map/acls?slug=$MAP_SLUG"
local response
response=$(call_api "GET" "/api/map/acls?slug=$MAP_SLUG" "$MAP_API_KEY")
if ! check_response "$response" "GET /api/map/acls"; then
return 1
fi
local acl_count
acl_count=$(echo "$LAST_JSON_RESPONSE" | jq '.data | length')
record_test_result "GET /api/map/acls" "success" "Found $acl_count ACLs"
if [ "$acl_count" -gt 0 ]; then
local random_acl
random_acl=$(get_random_item "$LAST_JSON_RESPONSE" ".data")
SELECTED_ACL_ID=$(echo "$random_acl" | jq -r '.id')
local acl_name
acl_name=$(echo "$random_acl" | jq -r '.name')
print_success "Selected random ACL: $acl_name (ID: $SELECTED_ACL_ID)"
else
print_warning "No ACLs found to select for future tests"
fi
return 0
}
test_create_acl() {
print_header "Testing POST /api/map/acls"
if ! check_required_vars "MAP_SLUG" "MAP_API_KEY"; then
record_test_result "POST /api/map/acls" "failure" "Missing required variables"
return 1
fi
if [ -z "$CHARACTER_EVE_ID" ]; then
print_warning "No character EVE ID selected. Fetching characters..."
print_success "Calling API: GET /api/characters"
local characters_response
characters_response=$(call_api "GET" "/api/characters" "$MAP_API_KEY")
if ! check_response "$characters_response" "GET /api/characters"; then
record_test_result "POST /api/map/acls" "failure" "Failed to get characters"
return 1
fi
local character_count
character_count=$(echo "$LAST_JSON_RESPONSE" | jq '.data | length')
if [ "$character_count" -eq 0 ]; then
record_test_result "POST /api/map/acls" "failure" "No characters found"
return 1
fi
local random_index=$((RANDOM % character_count))
print_success "Selecting character at index $random_index"
local random_character
random_character=$(echo "$LAST_JSON_RESPONSE" | jq ".data[$random_index]")
CHARACTER_EVE_ID=$(echo "$random_character" | jq -r '.eve_id')
local character_name
character_name=$(echo "$random_character" | jq -r '.name')
print_success "Selected random character: $character_name (EVE ID: $CHARACTER_EVE_ID)"
fi
local acl_name="Auto Test ACL $(date +%s)"
local acl_description="Created by auto_test_api.sh on $(date)"
local data="{\"acl\": {\"name\": \"$acl_name\", \"owner_eve_id\": $CHARACTER_EVE_ID, \"description\": \"$acl_description\"}}"
print_success "Calling API: POST /api/map/acls?slug=$MAP_SLUG"
print_success "Data: $data"
local response
response=$(call_api "POST" "/api/map/acls?slug=$MAP_SLUG" "$MAP_API_KEY" "$data")
if ! check_response "$response" "POST /api/map/acls"; then
return 1
fi
local new_acl_id
new_acl_id=$(echo "$LAST_JSON_RESPONSE" | jq -r '.data.id // empty')
local new_api_key
new_api_key=$(echo "$LAST_JSON_RESPONSE" | jq -r '.data.api_key // empty')
if [ -n "$new_acl_id" ] && [ -n "$new_api_key" ]; then
record_test_result "POST /api/map/acls" "success" "Created new ACL with ID: $new_acl_id"
SELECTED_ACL_ID=$new_acl_id
ACL_API_KEY=$new_api_key
print_success "Using the new ACL (ID: $SELECTED_ACL_ID) and its API key for further operations"
save_config
return 0
else
record_test_result "POST /api/map/acls" "failure" "Failed to extract ACL ID or API key from response"
return 1
fi
}
test_show_acl() {
print_header "Testing GET /api/acls/:id"
if [ -z "$SELECTED_ACL_ID" ] || [ -z "$ACL_API_KEY" ]; then
record_test_result "GET /api/acls/:id" "failure" "Missing ACL ID or API key"
return 1
fi
print_success "Calling API: GET /api/acls/$SELECTED_ACL_ID"
local response
response=$(call_api "GET" "/api/acls/$SELECTED_ACL_ID" "$ACL_API_KEY")
if ! check_response "$response" "GET /api/acls/:id"; then
return 1
fi
local acl_name
acl_name=$(echo "$LAST_JSON_RESPONSE" | jq -r '.data.name // empty')
if [ -n "$acl_name" ]; then
record_test_result "GET /api/acls/:id" "success" "Found ACL: $acl_name"
return 0
else
record_test_result "GET /api/acls/:id" "failure" "ACL data not found"
return 1
fi
}
test_update_acl() {
print_header "Testing PUT /api/acls/:id"
if [ -z "$SELECTED_ACL_ID" ] || [ -z "$ACL_API_KEY" ]; then
record_test_result "PUT /api/acls/:id" "failure" "Missing ACL ID or API key"
return 1
fi
local new_name="Updated Auto Test ACL $(date +%s)"
local new_description="Updated by auto_test_api.sh on $(date)"
local data="{\"acl\": {\"name\": \"$new_name\", \"description\": \"$new_description\"}}"
print_success "Calling API: PUT /api/acls/$SELECTED_ACL_ID"
print_success "Data: $data"
local response
response=$(call_api "PUT" "/api/acls/$SELECTED_ACL_ID" "$ACL_API_KEY" "$data")
if ! check_response "$response" "PUT /api/acls/:id"; then
return 1
fi
local updated_name
updated_name=$(echo "$LAST_JSON_RESPONSE" | jq -r '.data.name // empty')
if [ "$updated_name" = "$new_name" ]; then
record_test_result "PUT /api/acls/:id" "success" "Updated ACL name to: $updated_name"
return 0
else
record_test_result "PUT /api/acls/:id" "failure" "Failed to update ACL name"
return 1
fi
}
test_create_acl_member() {
print_header "Testing POST /api/acls/:acl_id/members"
if [ -z "$SELECTED_ACL_ID" ] || [ -z "$ACL_API_KEY" ]; then
record_test_result "POST /api/acls/:acl_id/members" "failure" "Missing ACL ID or API key"
return 1
fi
if [ -z "$CHARACTER_EVE_ID" ]; then
print_warning "No character EVE ID selected. Fetching characters..."
print_success "Calling API: GET /api/characters"
local characters_response
characters_response=$(call_api "GET" "/api/characters" "$MAP_API_KEY")
if ! check_response "$characters_response" "GET /api/characters"; then
record_test_result "POST /api/acls/:acl_id/members" "failure" "Failed to get characters"
return 1
fi
local character_count
character_count=$(echo "$LAST_JSON_RESPONSE" | jq '.data | length')
if [ "$character_count" -eq 0 ]; then
record_test_result "POST /api/acls/:acl_id/members" "failure" "No characters found"
return 1
fi
local random_index=$((RANDOM % character_count))
print_success "Selecting character at index $random_index"
local random_character
random_character=$(echo "$LAST_JSON_RESPONSE" | jq ".data[$random_index]")
CHARACTER_EVE_ID=$(echo "$random_character" | jq -r '.eve_id')
local character_name
character_name=$(echo "$random_character" | jq -r '.name')
print_success "Selected random character: $character_name (EVE ID: $CHARACTER_EVE_ID)"
fi
local data="{\"member\": {\"eve_character_id\": $CHARACTER_EVE_ID, \"role\": \"member\"}}"
print_success "Calling API: POST /api/acls/$SELECTED_ACL_ID/members"
print_success "Data: $data"
local response
response=$(call_api "POST" "/api/acls/$SELECTED_ACL_ID/members" "$ACL_API_KEY" "$data")
if ! check_response "$response" "POST /api/acls/:acl_id/members"; then
return 1
fi
local member_id
member_id=$(echo "$LAST_JSON_RESPONSE" | jq -r '.data.id // empty')
if [ -n "$member_id" ]; then
record_test_result "POST /api/acls/:acl_id/members" "success" "Created new member with ID: $member_id"
MEMBER_ID=$CHARACTER_EVE_ID
return 0
else
record_test_result "POST /api/acls/:acl_id/members" "failure" "Failed to create member"
return 1
fi
}
test_update_acl_member() {
print_header "Testing PUT /api/acls/:acl_id/members/:member_id"
if [ -z "$SELECTED_ACL_ID" ] || [ -z "$ACL_API_KEY" ] || [ -z "$MEMBER_ID" ]; then
record_test_result "PUT /api/acls/:acl_id/members/:member_id" "failure" "Missing ACL ID, API key, or member ID"
return 1
fi
local data="{\"member\": {\"role\": \"member\"}}"
print_success "Calling API: PUT /api/acls/$SELECTED_ACL_ID/members/$MEMBER_ID"
print_success "Data: $data"
local response
response=$(call_api "PUT" "/api/acls/$SELECTED_ACL_ID/members/$MEMBER_ID" "$ACL_API_KEY" "$data")
if ! check_response "$response" "PUT /api/acls/:acl_id/members/:member_id"; then
return 1
fi
local updated_role
updated_role=$(echo "$LAST_JSON_RESPONSE" | jq -r '.data.role // empty')
if [ "$updated_role" = "member" ]; then
record_test_result "PUT /api/acls/:acl_id/members/:member_id" "success" "Updated member role to: $updated_role"
return 0
else
record_test_result "PUT /api/acls/:acl_id/members/:member_id" "failure" "Failed to update member role"
return 1
fi
}
test_delete_acl_member() {
print_header "Testing DELETE /api/acls/:acl_id/members/:member_id"
if [ -z "$SELECTED_ACL_ID" ] || [ -z "$ACL_API_KEY" ] || [ -z "$MEMBER_ID" ]; then
record_test_result "DELETE /api/acls/:acl_id/members/:member_id" "failure" "Missing ACL ID, API key, or member ID"
return 1
fi
print_success "Calling API: DELETE /api/acls/$SELECTED_ACL_ID/members/$MEMBER_ID"
local response
response=$(call_api "DELETE" "/api/acls/$SELECTED_ACL_ID/members/$MEMBER_ID" "$ACL_API_KEY")
if ! check_response "$response" "DELETE /api/acls/:acl_id/members/:member_id"; then
return 1
fi
record_test_result "DELETE /api/acls/:acl_id/members/:member_id" "success" "Deleted member with ID: $MEMBER_ID"
MEMBER_ID=""
return 0
}
test_system_static_info() {
print_header "Testing GET /api/common/system-static-info"
if [ -z "$SELECTED_SYSTEM_ID" ]; then
record_test_result "GET /api/common/system-static-info" "failure" "No system ID selected"
return 1
fi
print_success "Calling API: GET /api/common/system-static-info?id=$SELECTED_SYSTEM_ID"
local response
response=$(call_api "GET" "/api/common/system-static-info?id=$SELECTED_SYSTEM_ID" "$MAP_API_KEY")
if ! check_response "$response" "GET /api/common/system-static-info"; then
return 1
fi
local system_count
system_count=$(echo "$LAST_JSON_RESPONSE" | jq 'length')
record_test_result "GET /api/common/system-static-info" "success" "Found static info for $system_count systems"
return 0
}
#------------------------------------------------------------------------------
# Configuration and Main Menu Functions
#------------------------------------------------------------------------------
set_config() {
print_header "Configuration"
echo -e "Current configuration:"
[ -n "$HOST" ] && echo -e " Host: ${BLUE}$HOST${NC}"
[ -n "$MAP_SLUG" ] && echo -e " Map Slug: ${BLUE}$MAP_SLUG${NC}"
[ -n "$MAP_API_KEY" ] && echo -e " Map API Key: ${BLUE}${MAP_API_KEY:0:8}...${NC}"
read -p "Enter host (default: $HOST): " input_host
[ -n "$input_host" ] && HOST="$input_host"
read -p "Enter map slug: " input_map_slug
[ -n "$input_map_slug" ] && MAP_SLUG="$input_map_slug"
read -p "Enter map API key: " input_map_api_key
[ -n "$input_map_api_key" ] && MAP_API_KEY="$input_map_api_key"
# Reset IDs to force fresh data
SELECTED_SYSTEM_ID=""
SELECTED_ACL_ID=""
ACL_API_KEY=""
CHARACTER_EVE_ID=""
save_config
}
run_all_tests() {
print_header "Running all API tests"
TEST_RESULTS=()
FAILED_TESTS=()
if ! command -v jq &> /dev/null; then
print_error "jq is required for this script to work. Please install it first."
exit 1
fi
if ! check_required_vars "MAP_SLUG" "MAP_API_KEY"; then
print_error "Please set MAP_SLUG and MAP_API_KEY before running tests."
exit 1
fi
check_host_reachable
test_list_characters
if test_map_systems; then
test_map_system
else
print_error "Skipping test_map_system because test_map_systems failed"
record_test_result "GET /api/map/system" "failure" "Skipped because test_map_systems failed"
fi
test_map_characters
test_map_structure_timers
test_map_systems_kills
test_map_acls
if test_create_acl; then
test_show_acl
test_update_acl
if test_create_acl_member; then
test_update_acl_member
test_delete_acl_member
else
print_error "Skipping ACL member tests because test_create_acl_member failed"
record_test_result "PUT /api/acls/:acl_id/members/:member_id" "failure" "Skipped because test_create_acl_member failed"
record_test_result "DELETE /api/acls/:acl_id/members/:member_id" "failure" "Skipped because test_create_acl_member failed"
fi
else
print_error "Skipping ACL tests because test_create_acl failed"
record_test_result "GET /api/acls/:id" "failure" "Skipped because test_create_acl failed"
record_test_result "PUT /api/acls/:id" "failure" "Skipped because test_create_acl failed"
record_test_result "POST /api/acls/:acl_id/members" "failure" "Skipped because test_create_acl failed"
record_test_result "PUT /api/acls/:acl_id/members/:member_id" "failure" "Skipped because test_create_acl failed"
record_test_result "DELETE /api/acls/:acl_id/members/:member_id" "failure" "Skipped because test_create_acl failed"
fi
test_system_static_info
print_header "Test Results"
for result in "${TEST_RESULTS[@]}"; do
echo -e "$result"
done
local total_tests=${#TEST_RESULTS[@]}
local failed_tests=${#FAILED_TESTS[@]}
local passed_tests=$((total_tests - failed_tests))
print_header "Summary"
echo -e "Total tests: $total_tests"
echo -e "Passed: ${GREEN}$passed_tests${NC}"
echo -e "Failed: ${RED}$failed_tests${NC}"
if [ $failed_tests -gt 0 ]; then
print_header "Failed Tests"
for failed in "${FAILED_TESTS[@]}"; do
echo -e "${RED}${NC} $failed"
done
fi
if [ "$VERBOSE_SUMMARY" -eq 1 ]; then
summary_json=$(jq -n --arg total "$total_tests" --arg passed "$passed_tests" --arg failed "$failed_tests" \
'{total_tests: $total_tests|tonumber, passed: $passed|tonumber, failed: $failed|tonumber}')
echo "JSON Summary:"; echo "$summary_json" | jq .
fi
save_config
if [ $failed_tests -gt 0 ]; then
exit 1
else
exit 0
fi
}
#------------------------------------------------------------------------------
# Main Menu and Entry Point
#------------------------------------------------------------------------------
main() {
print_header "Wanderer API Automated Testing Tool"
load_config
if [ -z "$MAP_SLUG" ] || [ -z "$MAP_API_KEY" ]; then
print_warning "MAP_SLUG or MAP_API_KEY not set. Let's configure them now."
set_config
fi
echo -e "What would you like to do?"
echo "1) Run all tests"
echo "2) Set configuration"
echo "3) Exit"
read -p "Enter your choice: " choice
case $choice in
1) run_all_tests ;;
2) set_config ;;
3) exit 0 ;;
*) print_error "Invalid choice"; main ;;
esac
}
# Start the script
main

View File

@@ -0,0 +1,334 @@
# Standalone test for the CharacterAPIController
#
# This file can be run directly with:
# elixir test/standalone/character_api_controller_test.exs
#
# It doesn't require any database connections or external dependencies.
# Start ExUnit
ExUnit.start()
defmodule CharacterAPIControllerTest do
use ExUnit.Case
# Mock modules to simulate the behavior of the controller's dependencies
defmodule MockUtil do
def require_param(params, key) do
case params[key] do
nil -> {:error, "Missing required param: #{key}"}
"" -> {:error, "Param #{key} cannot be empty"}
val -> {:ok, val}
end
end
def parse_int(str) do
case Integer.parse(str) do
{num, ""} -> {:ok, num}
_ -> {:error, "Invalid integer for param id=#{str}"}
end
end
def parse_bool(str) do
case str do
"true" -> {:ok, true}
"false" -> {:ok, false}
_ -> {:error, "Invalid boolean value: #{str}"}
end
end
end
defmodule MockCharacterRepo do
# In-memory storage for character tracking data
def init_storage do
:ets.new(:character_tracking, [:set, :public, :named_table])
# Initialize with some test data
:ets.insert(:character_tracking, {"user1", [
%{eve_id: "123456", name: "Character One", tracked: true, followed: true},
%{eve_id: "234567", name: "Character Two", tracked: true, followed: false},
%{eve_id: "345678", name: "Character Three", tracked: false, followed: false}
]})
:ets.insert(:character_tracking, {"user2", [
%{eve_id: "456789", name: "Character Four", tracked: true, followed: true}
]})
end
def get_tracking_data(user_id) do
case :ets.lookup(:character_tracking, user_id) do
[{^user_id, data}] -> {:ok, data}
[] -> {:ok, []}
end
end
def update_tracking_data(user_id, new_data) do
:ets.insert(:character_tracking, {user_id, new_data})
{:ok, new_data}
end
def toggle_character_follow(user_id, character_id, follow_state) do
case get_tracking_data(user_id) do
{:ok, data} ->
# Find the character and update its followed state
updated_data = Enum.map(data, fn char ->
if char.eve_id == character_id do
%{char | followed: follow_state}
else
char
end
end)
# Update the storage
update_tracking_data(user_id, updated_data)
# Return the updated character
updated_char = Enum.find(updated_data, fn char -> char.eve_id == character_id end)
{:ok, updated_char}
error -> error
end
end
def toggle_character_track(user_id, character_id, track_state) do
case get_tracking_data(user_id) do
{:ok, data} ->
# Find the character and update its tracked state
updated_data = Enum.map(data, fn char ->
if char.eve_id == character_id do
%{char | tracked: track_state}
else
char
end
end)
# Update the storage
update_tracking_data(user_id, updated_data)
# Return the updated character
updated_char = Enum.find(updated_data, fn char -> char.eve_id == character_id end)
{:ok, updated_char}
error -> error
end
end
end
defmodule MockTrackingUtils do
def check_tracking_consistency(tracking_data) do
# Log warnings for characters that are followed but not tracked
inconsistent_chars = Enum.filter(tracking_data, fn char ->
char[:followed] == true && char[:tracked] == false
end)
if length(inconsistent_chars) > 0 do
Enum.each(inconsistent_chars, fn char ->
eve_id = Map.get(char, :eve_id, "unknown")
name = Map.get(char, :name, "Unknown Character")
IO.puts("WARNING: Inconsistent state detected - Character (ID: #{eve_id}, Name: #{name}) is followed but not tracked")
end)
end
# Return the original data unchanged
tracking_data
end
end
# Mock controller that uses our mock dependencies
defmodule MockCharacterAPIController do
# Simplified version of toggle_follow from CharacterAPIController
def toggle_follow(params, user_id) do
with {:ok, character_id} <- MockUtil.require_param(params, "character_id"),
{:ok, follow_str} <- MockUtil.require_param(params, "follow"),
{:ok, follow} <- MockUtil.parse_bool(follow_str) do
case MockCharacterRepo.toggle_character_follow(user_id, character_id, follow) do
{:ok, updated_char} ->
# Get all tracking data to check consistency
{:ok, all_tracking} = MockCharacterRepo.get_tracking_data(user_id)
# Check for inconsistencies (characters followed but not tracked)
MockTrackingUtils.check_tracking_consistency(all_tracking)
# Return the updated character
{:ok, %{data: updated_char}}
{:error, reason} ->
{:error, :internal_server_error, "Failed to update character: #{reason}"}
end
else
{:error, msg} ->
{:error, :bad_request, msg}
end
end
# Simplified version of toggle_track from CharacterAPIController
def toggle_track(params, user_id) do
with {:ok, character_id} <- MockUtil.require_param(params, "character_id"),
{:ok, track_str} <- MockUtil.require_param(params, "track"),
{:ok, track} <- MockUtil.parse_bool(track_str) do
# If we're untracking a character, we should also unfollow it
result = if track == false do
# First unfollow if needed
MockCharacterRepo.toggle_character_follow(user_id, character_id, false)
# Then untrack
MockCharacterRepo.toggle_character_track(user_id, character_id, false)
else
# Just track
MockCharacterRepo.toggle_character_track(user_id, character_id, true)
end
case result do
{:ok, updated_char} ->
# Get all tracking data to check consistency
{:ok, all_tracking} = MockCharacterRepo.get_tracking_data(user_id)
# Check for inconsistencies (characters followed but not tracked)
MockTrackingUtils.check_tracking_consistency(all_tracking)
# Return the updated character
{:ok, %{data: updated_char}}
{:error, reason} ->
{:error, :internal_server_error, "Failed to update character: #{reason}"}
end
else
{:error, msg} ->
{:error, :bad_request, msg}
end
end
# Simplified version of list_tracking from CharacterAPIController
def list_tracking(user_id) do
case MockCharacterRepo.get_tracking_data(user_id) do
{:ok, tracking_data} ->
# Check for inconsistencies
checked_data = MockTrackingUtils.check_tracking_consistency(tracking_data)
# Return the data
{:ok, %{data: checked_data}}
{:error, reason} ->
{:error, :internal_server_error, "Failed to get tracking data: #{reason}"}
end
end
end
# Setup for tests
setup do
# Initialize the mock storage
MockCharacterRepo.init_storage()
:ok
end
describe "toggle_follow/2" do
test "follows a character successfully" do
params = %{"character_id" => "345678", "follow" => "true"}
result = MockCharacterAPIController.toggle_follow(params, "user1")
assert {:ok, %{data: data}} = result
assert data.eve_id == "345678"
assert data.name == "Character Three"
assert data.followed == true
assert data.tracked == false
# This should have created an inconsistency (followed but not tracked)
# The check_tracking_consistency function should have logged a warning
end
test "unfollows a character successfully" do
params = %{"character_id" => "123456", "follow" => "false"}
result = MockCharacterAPIController.toggle_follow(params, "user1")
assert {:ok, %{data: data}} = result
assert data.eve_id == "123456"
assert data.followed == false
assert data.tracked == true
end
test "returns error when character_id is missing" do
params = %{"follow" => "true"}
result = MockCharacterAPIController.toggle_follow(params, "user1")
assert {:error, :bad_request, message} = result
assert message == "Missing required param: character_id"
end
test "returns error when follow is not a valid boolean" do
params = %{"character_id" => "123456", "follow" => "not-a-boolean"}
result = MockCharacterAPIController.toggle_follow(params, "user1")
assert {:error, :bad_request, message} = result
assert message =~ "Invalid boolean value"
end
end
describe "toggle_track/2" do
test "tracks a character successfully" do
params = %{"character_id" => "345678", "track" => "true"}
result = MockCharacterAPIController.toggle_track(params, "user1")
assert {:ok, %{data: data}} = result
assert data.eve_id == "345678"
assert data.tracked == true
end
test "untracks and unfollows a character" do
# First, make sure the character is followed
follow_params = %{"character_id" => "123456", "follow" => "true"}
MockCharacterAPIController.toggle_follow(follow_params, "user1")
# Now untrack the character
params = %{"character_id" => "123456", "track" => "false"}
result = MockCharacterAPIController.toggle_track(params, "user1")
assert {:ok, %{data: data}} = result
assert data.eve_id == "123456"
assert data.tracked == false
assert data.followed == false # Should also be unfollowed
end
test "returns error when character_id is missing" do
params = %{"track" => "true"}
result = MockCharacterAPIController.toggle_track(params, "user1")
assert {:error, :bad_request, message} = result
assert message == "Missing required param: character_id"
end
test "returns error when track is not a valid boolean" do
params = %{"character_id" => "123456", "track" => "not-a-boolean"}
result = MockCharacterAPIController.toggle_track(params, "user1")
assert {:error, :bad_request, message} = result
assert message =~ "Invalid boolean value"
end
end
describe "list_tracking/1" do
test "returns tracking data for a user" do
result = MockCharacterAPIController.list_tracking("user1")
assert {:ok, %{data: data}} = result
assert length(data) == 3
# Check that the data contains the expected characters
char_one = Enum.find(data, fn char -> char.eve_id == "123456" end)
assert char_one.name == "Character One"
assert char_one.tracked == true
assert char_one.followed == true
char_two = Enum.find(data, fn char -> char.eve_id == "234567" end)
assert char_two.name == "Character Two"
assert char_two.tracked == true
assert char_two.followed == false
end
test "returns empty list for user with no tracking data" do
result = MockCharacterAPIController.list_tracking("non-existent-user")
assert {:ok, %{data: data}} = result
assert data == []
end
end
end

View File

@@ -0,0 +1,297 @@
# Standalone test for the CommonAPIController
#
# This file can be run directly with:
# elixir test/standalone/common_api_controller_test.exs
#
# It doesn't require any database connections or external dependencies.
# Start ExUnit
ExUnit.start()
defmodule CommonAPIControllerTest do
use ExUnit.Case
# Mock modules to simulate the behavior of the controller's dependencies
defmodule MockUtil do
def require_param(params, key) do
case params[key] do
nil -> {:error, "Missing required param: #{key}"}
"" -> {:error, "Param #{key} cannot be empty"}
val -> {:ok, val}
end
end
def parse_int(str) do
case Integer.parse(str) do
{num, ""} -> {:ok, num}
_ -> {:error, "Invalid integer for param id=#{str}"}
end
end
end
defmodule MockCachedInfo do
def get_system_static_info(30000142) do
{:ok, %{
solar_system_id: 30000142,
region_id: 10000002,
constellation_id: 20000020,
solar_system_name: "Jita",
solar_system_name_lc: "jita",
constellation_name: "Kimotoro",
region_name: "The Forge",
system_class: 0,
security: "0.9",
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
}}
end
def get_system_static_info(31000005) do
{:ok, %{
solar_system_id: 31000005,
region_id: 11000000,
constellation_id: 21000000,
solar_system_name: "J123456",
solar_system_name_lc: "j123456",
constellation_name: "Unknown",
region_name: "Wormhole Space",
system_class: 1,
security: "-0.9",
type_description: "Wormhole",
class_title: "Class 1",
is_shattered: false,
effect_name: "Wolf-Rayet Star",
effect_power: 1,
statics: ["N110"],
wandering: ["K162"],
triglavian_invasion_status: nil,
sun_type_id: 45042
}}
end
def get_system_static_info(_) do
{:error, :not_found}
end
def get_wormhole_types do
{:ok, [
%{
name: "N110",
dest: 1,
lifetime: "16h",
total_mass: 500000000,
max_mass_per_jump: 20000000,
mass_regen: 0
}
]}
end
def get_wormhole_classes! do
[
%{
id: 1,
title: "Class 1 Wormhole",
short_name: "C1"
}
]
end
end
# Mock controller that uses our mock dependencies
defmodule MockCommonAPIController do
# Simplified version of show_system_static from CommonAPIController
def show_system_static(params) do
with {:ok, solar_system_str} <- MockUtil.require_param(params, "id"),
{:ok, solar_system_id} <- MockUtil.parse_int(solar_system_str) do
case MockCachedInfo.get_system_static_info(solar_system_id) do
{:ok, system} ->
# Get basic system data
data = static_system_to_json(system)
# Enhance with wormhole type information if statics exist
enhanced_data = enhance_with_static_details(data)
# Return the enhanced data
{:ok, %{data: enhanced_data}}
{:error, :not_found} ->
{:error, :not_found, "System not found"}
end
else
{:error, msg} ->
{:error, :bad_request, msg}
end
end
# Helper function to convert a system to JSON format
defp static_system_to_json(system) do
system
|> Map.take([
:solar_system_id,
:region_id,
:constellation_id,
:solar_system_name,
:solar_system_name_lc,
:constellation_name,
:region_name,
:system_class,
:security,
:type_description,
:class_title,
:is_shattered,
:effect_name,
:effect_power,
:statics,
:wandering,
:triglavian_invasion_status,
:sun_type_id
])
end
# Helper function to enhance system data with wormhole type information
defp enhance_with_static_details(data) do
if data[:statics] && length(data[:statics]) > 0 do
# Add the enhanced static details to the response
Map.put(data, :static_details, get_static_details(data[:statics]))
else
# No statics, return the original data
data
end
end
# Helper function to get detailed information for each static wormhole
defp get_static_details(statics) do
# Get wormhole data from CachedInfo
{:ok, wormhole_types} = MockCachedInfo.get_wormhole_types()
wormhole_classes = MockCachedInfo.get_wormhole_classes!()
# Create a map of wormhole classes by ID for quick lookup
classes_by_id = Enum.reduce(wormhole_classes, %{}, fn class, acc ->
Map.put(acc, class.id, class)
end)
# Find detailed information for each static
Enum.map(statics, fn static_name ->
# Find the wormhole type by name
wh_type = Enum.find(wormhole_types, fn type -> type.name == static_name end)
if wh_type do
create_wormhole_details(wh_type, classes_by_id)
else
create_fallback_wormhole_details(static_name)
end
end)
end
# Helper function to create detailed wormhole information
defp create_wormhole_details(wh_type, classes_by_id) do
# Get destination class info
dest_class = Map.get(classes_by_id, wh_type.dest)
# Create enhanced static info
%{
name: wh_type.name,
destination: %{
id: to_string(wh_type.dest),
name: (if dest_class, do: dest_class.title, else: wh_type.dest),
short_name: (if dest_class, do: dest_class.short_name, else: wh_type.dest)
},
properties: %{
lifetime: wh_type.lifetime,
max_mass: wh_type.total_mass,
max_jump_mass: wh_type.max_mass_per_jump,
mass_regeneration: wh_type.mass_regen
}
}
end
# Helper function to create fallback information
defp create_fallback_wormhole_details(static_name) do
%{
name: static_name,
destination: %{
id: nil,
name: "Unknown",
short_name: "?"
},
properties: %{
lifetime: nil,
max_mass: nil,
max_jump_mass: nil,
mass_regeneration: nil
}
}
end
end
describe "show_system_static/1" do
test "returns system static info for a high-sec system" do
params = %{"id" => "30000142"}
result = MockCommonAPIController.show_system_static(params)
assert {:ok, %{data: data}} = result
assert data.solar_system_id == 30000142
assert data.solar_system_name == "Jita"
assert data.region_name == "The Forge"
assert data.security == "0.9"
assert data.type_description == "High Security"
refute Map.has_key?(data, :static_details)
end
test "returns system static info with static details for a wormhole system" do
params = %{"id" => "31000005"}
result = MockCommonAPIController.show_system_static(params)
assert {:ok, %{data: data}} = result
assert data.solar_system_id == 31000005
assert data.solar_system_name == "J123456"
assert data.region_name == "Wormhole Space"
assert data.system_class == 1
assert data.security == "-0.9"
assert data.type_description == "Wormhole"
assert data.effect_name == "Wolf-Rayet Star"
# Check static details
assert Map.has_key?(data, :static_details)
assert length(data.static_details) == 1
static = List.first(data.static_details)
assert static.name == "N110"
assert static.destination.id == "1"
assert static.destination.name == "Class 1 Wormhole"
assert static.destination.short_name == "C1"
assert static.properties.lifetime == "16h"
assert static.properties.max_mass == 500000000
end
test "returns error when system is not found" do
params = %{"id" => "99999999"}
result = MockCommonAPIController.show_system_static(params)
assert {:error, :not_found, "System not found"} = result
end
test "returns error when system_id is not provided" do
params = %{}
result = MockCommonAPIController.show_system_static(params)
assert {:error, :bad_request, message} = result
assert message == "Missing required param: id"
end
test "returns error when system_id is not a valid integer" do
params = %{"id" => "not-an-integer"}
result = MockCommonAPIController.show_system_static(params)
assert {:error, :bad_request, message} = result
assert message =~ "Invalid integer for param id"
end
end
end

View File

@@ -0,0 +1,213 @@
# Standalone test for the MapAPIController
#
# This file can be run directly with:
# elixir test/standalone/map_api_controller_test.exs
#
# It doesn't require any database connections or external dependencies.
# Start ExUnit
ExUnit.start()
defmodule MapAPIControllerTest do
use ExUnit.Case
# Mock modules to simulate the behavior of the controller's dependencies
defmodule MockUtil do
def require_param(params, key) do
case params[key] do
nil -> {:error, "Missing required param: #{key}"}
"" -> {:error, "Param #{key} cannot be empty"}
val -> {:ok, val}
end
end
def parse_int(str) do
case Integer.parse(str) do
{num, ""} -> {:ok, num}
_ -> {:error, "Invalid integer for param id=#{str}"}
end
end
def fetch_map_id(params) do
cond do
params["map_id"] ->
case parse_int(params["map_id"]) do
{:ok, map_id} -> {:ok, map_id}
{:error, _} -> {:error, "Invalid map_id format"}
end
params["slug"] ->
# In a real app, this would look up the map by slug
# For testing, we'll just use a simple mapping
case params["slug"] do
"test-map" -> {:ok, 1}
"another-map" -> {:ok, 2}
_ -> {:error, "Map not found"}
end
true ->
{:error, "Missing required param: map_id or slug"}
end
end
end
defmodule MockMapSystemRepo do
def get_visible_systems_by_map_id(1) do
[
%{id: 30000142, name: "Jita", security: 0.9, region_id: 10000002},
%{id: 30002659, name: "Dodixie", security: 0.9, region_id: 10000032},
%{id: 30002187, name: "Amarr", security: 1.0, region_id: 10000043}
]
end
def get_visible_systems_by_map_id(_) do
[]
end
def get_system_by_id(1, 30000142) do
%{id: 30000142, name: "Jita", security: 0.9, region_id: 10000002}
end
def get_system_by_id(1, 30002659) do
%{id: 30002659, name: "Dodixie", security: 0.9, region_id: 10000032}
end
def get_system_by_id(1, 30002187) do
%{id: 30002187, name: "Amarr", security: 1.0, region_id: 10000043}
end
def get_system_by_id(_, _) do
nil
end
end
defmodule MockMapSolarSystem do
def get_name_by_id(30000142), do: "Jita"
def get_name_by_id(30002659), do: "Dodixie"
def get_name_by_id(30002187), do: "Amarr"
def get_name_by_id(_), do: nil
end
# Mock controller that uses our mock dependencies
defmodule MockMapAPIController do
# Simplified version of list_systems from MapAPIController
def list_systems(params) do
with {:ok, map_id} <- MockUtil.fetch_map_id(params) do
systems = MockMapSystemRepo.get_visible_systems_by_map_id(map_id)
if systems == [] do
{:error, :not_found, "No systems found for this map"}
else
# Format the response
formatted_systems = Enum.map(systems, fn system ->
%{
id: system.id,
name: system.name,
security: system.security
}
end)
{:ok, %{data: formatted_systems}}
end
else
{:error, msg} ->
{:error, :bad_request, msg}
end
end
# Simplified version of show_system from MapAPIController
def show_system(params) do
with {:ok, map_id} <- MockUtil.fetch_map_id(params),
{:ok, system_id_str} <- MockUtil.require_param(params, "id"),
{:ok, system_id} <- MockUtil.parse_int(system_id_str) do
system = MockMapSystemRepo.get_system_by_id(map_id, system_id)
if system == nil do
{:error, :not_found, "System not found"}
else
# Format the response
formatted_system = %{
id: system.id,
name: system.name,
security: system.security
}
{:ok, %{data: formatted_system}}
end
else
{:error, msg} ->
{:error, :bad_request, msg}
end
end
end
describe "list_systems/1" do
test "returns systems with valid map_id" do
params = %{"map_id" => "1"}
result = MockMapAPIController.list_systems(params)
assert {:ok, %{data: data}} = result
assert length(data) == 3
# Check that the data contains the expected systems
jita = Enum.find(data, fn system -> system.id == 30000142 end)
assert jita.name == "Jita"
assert jita.security == 0.9
dodixie = Enum.find(data, fn system -> system.id == 30002659 end)
assert dodixie.name == "Dodixie"
assert dodixie.security == 0.9
end
test "returns systems with valid slug" do
params = %{"slug" => "test-map"}
result = MockMapAPIController.list_systems(params)
assert {:ok, %{data: data}} = result
assert length(data) == 3
end
test "returns error when no systems found" do
params = %{"map_id" => "2"}
result = MockMapAPIController.list_systems(params)
assert {:error, :not_found, message} = result
assert message == "No systems found for this map"
end
test "returns error when map_id is missing" do
params = %{}
result = MockMapAPIController.list_systems(params)
assert {:error, :bad_request, message} = result
assert message == "Missing required param: map_id or slug"
end
test "returns error when invalid map_id is provided" do
params = %{"slug" => "non-existent-map"}
result = MockMapAPIController.list_systems(params)
assert {:error, :bad_request, message} = result
assert message == "Map not found"
end
end
describe "show_system/1" do
test "returns system with valid parameters" do
params = %{"map_id" => "1", "id" => "30000142"}
result = MockMapAPIController.show_system(params)
assert {:ok, %{data: data}} = result
assert data.id == 30000142
assert data.name == "Jita"
assert data.security == 0.9
end
test "returns error when system is not found" do
params = %{"map_id" => "1", "id" => "99999999"}
result = MockMapAPIController.show_system(params)
assert {:error, :not_found, message} = result
assert message == "System not found"
end
end
end

View File

@@ -0,0 +1,321 @@
# Standalone test for the MapAPIController route functionality
#
# This file can be run directly with:
# elixir test/standalone/map_route_api_controller_test.exs
#
# It doesn't require any database connections or external dependencies.
# Start ExUnit
ExUnit.start()
defmodule MapRouteAPIControllerTest do
use ExUnit.Case
# Mock modules to simulate the behavior of the controller's dependencies
defmodule MockUtil do
def require_param(params, key) do
case params[key] do
nil -> {:error, "Missing required param: #{key}"}
"" -> {:error, "Param #{key} cannot be empty"}
val -> {:ok, val}
end
end
def parse_int(str) do
case Integer.parse(str) do
{num, ""} -> {:ok, num}
_ -> {:error, "Invalid integer for param id=#{str}"}
end
end
def fetch_map_id(params) do
cond do
params["map_id"] ->
case parse_int(params["map_id"]) do
{:ok, map_id} -> {:ok, map_id}
{:error, _} -> {:error, "Invalid map_id format"}
end
params["slug"] ->
# In a real app, this would look up the map by slug
# For testing, we'll just use a simple mapping
case params["slug"] do
"test-map" -> {:ok, 1}
"another-map" -> {:ok, 2}
_ -> {:error, "Map not found"}
end
true ->
{:error, "Missing required param: map_id or slug"}
end
end
end
defmodule MockMapSystemRepo do
# Mock data for systems
def get_systems_by_ids(map_id, system_ids) when map_id == 1 do
systems = %{
30000142 => %{id: 30000142, name: "Jita", security: 0.9, region_id: 10000002},
30002659 => %{id: 30002659, name: "Dodixie", security: 0.9, region_id: 10000032},
30002187 => %{id: 30002187, name: "Amarr", security: 1.0, region_id: 10000043}
}
Enum.map(system_ids, fn id -> Map.get(systems, id) end)
|> Enum.filter(&(&1 != nil))
end
def get_systems_by_ids(_, _), do: []
# Mock data for connections
def get_connections_between(map_id, _system_ids) when map_id == 1 do
[
%{source_id: 30000142, target_id: 30002659, distance: 15},
%{source_id: 30002659, target_id: 30002187, distance: 12},
%{source_id: 30000142, target_id: 30002187, distance: 20}
]
end
def get_connections_between(_, _), do: []
end
defmodule MockRouteCalculator do
# Simplified route calculator that just returns a predefined route
def calculate_route(systems, _connections, source_id, target_id, _options \\ []) do
cond do
source_id == 30000142 and target_id == 30002187 ->
# Direct route from Jita to Amarr
route = [
Enum.find(systems, fn s -> s.id == 30000142 end),
Enum.find(systems, fn s -> s.id == 30002187 end)
]
{:ok, %{route: route, jumps: 1, distance: 20}}
source_id == 30000142 and target_id == 30002659 ->
# Direct route from Jita to Dodixie
route = [
Enum.find(systems, fn s -> s.id == 30000142 end),
Enum.find(systems, fn s -> s.id == 30002659 end)
]
{:ok, %{route: route, jumps: 1, distance: 15}}
source_id == 30002659 and target_id == 30002187 ->
# Direct route from Dodixie to Amarr
route = [
Enum.find(systems, fn s -> s.id == 30002659 end),
Enum.find(systems, fn s -> s.id == 30002187 end)
]
{:ok, %{route: route, jumps: 1, distance: 12}}
source_id == 30002659 and target_id == 30000142 ->
# Direct route from Dodixie to Jita
route = [
Enum.find(systems, fn s -> s.id == 30002659 end),
Enum.find(systems, fn s -> s.id == 30000142 end)
]
{:ok, %{route: route, jumps: 1, distance: 15}}
true ->
{:error, "No route found"}
end
end
end
# Mock controller that uses our mock dependencies
defmodule MockMapAPIController do
# Simplified version of calculate_route from MapAPIController
def calculate_route(params) do
with {:ok, map_id} <- MockUtil.fetch_map_id(params),
{:ok, source_id_str} <- MockUtil.require_param(params, "source"),
{:ok, source_id} <- MockUtil.parse_int(source_id_str),
{:ok, target_id_str} <- MockUtil.require_param(params, "target"),
{:ok, target_id} <- MockUtil.parse_int(target_id_str) do
# Get the systems involved in the route
systems = MockMapSystemRepo.get_systems_by_ids(map_id, [source_id, target_id])
# Check if both systems exist
source_system = Enum.find(systems, fn s -> s.id == source_id end)
target_system = Enum.find(systems, fn s -> s.id == target_id end)
if source_system == nil do
{:error, :not_found, "Source system not found"}
else
if target_system == nil do
{:error, :not_found, "Target system not found"}
else
# Get connections between systems
connections = MockMapSystemRepo.get_connections_between(map_id, [source_id, target_id])
# Calculate the route
case MockRouteCalculator.calculate_route(systems, connections, source_id, target_id) do
{:ok, route_data} ->
# Format the response
formatted_route = format_route_response(route_data)
{:ok, %{data: formatted_route}}
{:error, reason} ->
{:error, :not_found, reason}
end
end
end
else
{:error, msg} ->
{:error, :bad_request, msg}
end
end
# Helper function to format the route response
defp format_route_response(route_data) do
%{
route: Enum.map(route_data.route, fn system ->
%{
id: system.id,
name: system.name,
security: system.security
}
end),
jumps: route_data.jumps,
distance: route_data.distance
}
end
end
describe "calculate_route/1" do
test "calculates route between two systems successfully" do
params = %{
"map_id" => "1",
"source" => "30000142", # Jita
"target" => "30002187" # Amarr
}
result = MockMapAPIController.calculate_route(params)
assert {:ok, %{data: data}} = result
assert length(data.route) == 2
assert Enum.at(data.route, 0).id == 30000142
assert Enum.at(data.route, 0).name == "Jita"
assert Enum.at(data.route, 1).id == 30002187
assert Enum.at(data.route, 1).name == "Amarr"
assert data.jumps == 1
assert data.distance == 20
end
test "calculates route using map slug" do
params = %{
"slug" => "test-map",
"source" => "30000142", # Jita
"target" => "30002659" # Dodixie
}
result = MockMapAPIController.calculate_route(params)
assert {:ok, %{data: data}} = result
assert length(data.route) == 2
assert Enum.at(data.route, 0).id == 30000142
assert Enum.at(data.route, 0).name == "Jita"
assert Enum.at(data.route, 1).id == 30002659
assert Enum.at(data.route, 1).name == "Dodixie"
assert data.jumps == 1
assert data.distance == 15
end
test "returns error when source system is not found" do
params = %{
"map_id" => "1",
"source" => "99999999", # Non-existent system
"target" => "30002187" # Amarr
}
result = MockMapAPIController.calculate_route(params)
assert {:error, :not_found, message} = result
assert message == "Source system not found"
end
test "returns error when target system is not found" do
params = %{
"map_id" => "1",
"source" => "30000142", # Jita
"target" => "99999999" # Non-existent system
}
result = MockMapAPIController.calculate_route(params)
assert {:error, :not_found, message} = result
assert message == "Target system not found"
end
test "returns error when map is not found" do
params = %{
"slug" => "non-existent-map",
"source" => "30000142", # Jita
"target" => "30002187" # Amarr
}
result = MockMapAPIController.calculate_route(params)
assert {:error, :bad_request, message} = result
assert message == "Map not found"
end
test "returns error when source parameter is missing" do
params = %{
"map_id" => "1",
"target" => "30002187" # Amarr
}
result = MockMapAPIController.calculate_route(params)
assert {:error, :bad_request, message} = result
assert message == "Missing required param: source"
end
test "returns error when target parameter is missing" do
params = %{
"map_id" => "1",
"source" => "30000142" # Jita
}
result = MockMapAPIController.calculate_route(params)
assert {:error, :bad_request, message} = result
assert message == "Missing required param: target"
end
test "returns error when map_id and slug are both missing" do
params = %{
"source" => "30000142", # Jita
"target" => "30002187" # Amarr
}
result = MockMapAPIController.calculate_route(params)
assert {:error, :bad_request, message} = result
assert message == "Missing required param: map_id or slug"
end
test "returns error when source is not a valid integer" do
params = %{
"map_id" => "1",
"source" => "not-an-integer",
"target" => "30002187" # Amarr
}
result = MockMapAPIController.calculate_route(params)
assert {:error, :bad_request, message} = result
assert message =~ "Invalid integer for param id"
end
test "returns error when target is not a valid integer" do
params = %{
"map_id" => "1",
"source" => "30000142", # Jita
"target" => "not-an-integer"
}
result = MockMapAPIController.calculate_route(params)
assert {:error, :bad_request, message} = result
assert message =~ "Invalid integer for param id"
end
end
end

View File

@@ -0,0 +1,332 @@
# Test for the check_tracking_consistency function in WandererApp.Character.TrackingUtils
#
# This file can be run directly with:
# elixir test/unit/tracking_consistency_test.exs
#
# It doesn't require any database connections or external dependencies.
# Start ExUnit
ExUnit.start()
defmodule WandererApp.Character.TrackingConsistencyTest do
use ExUnit.Case
require Logger
import ExUnit.CaptureIO
# This is a copy of the function from TrackingUtils
def check_tracking_consistency(tracking_data) do
# Find any characters that are followed but not tracked
inconsistent_characters = Enum.filter(tracking_data, fn data ->
data.followed && !data.tracked
end)
# Log a warning for each inconsistent character
Enum.each(inconsistent_characters, fn data ->
character = data.character
# Use IO.puts instead of Logger to avoid dependencies
eve_id = Map.get(character, :eve_id, "unknown")
name = Map.get(character, :name, "unknown")
IO.puts("WARNING: Inconsistent state detected: Character is followed but not tracked. Character ID: #{eve_id}, Name: #{name}")
end)
# Return the original tracking data
tracking_data
end
describe "check_tracking_consistency/1" do
test "logs a warning when a character is followed but not tracked" do
# Create test data with inconsistent state
tracking_data = [
%{
character: %{eve_id: "test-eve-id", name: "Test Character"},
tracked: false,
followed: true
}
]
# Call the function and capture output
output = capture_io(fn ->
check_tracking_consistency(tracking_data)
end)
# Assert that the warning was logged
assert output =~ "Inconsistent state detected: Character is followed but not tracked"
assert output =~ "test-eve-id"
assert output =~ "Test Character"
end
test "does not log a warning when all followed characters are also tracked" do
# Create test data with consistent state
tracking_data = [
%{
character: %{eve_id: "test-eve-id", name: "Test Character"},
tracked: true,
followed: true
}
]
# Call the function and capture output
output = capture_io(fn ->
check_tracking_consistency(tracking_data)
end)
# Assert that no warning was logged
refute output =~ "Inconsistent state detected"
end
test "does not log a warning when no characters are followed" do
# Create test data with no followed characters
tracking_data = [
%{
character: %{eve_id: "test-eve-id", name: "Test Character"},
tracked: true,
followed: false
}
]
# Call the function and capture output
output = capture_io(fn ->
check_tracking_consistency(tracking_data)
end)
# Assert that no warning was logged
refute output =~ "Inconsistent state detected"
end
test "handles multiple characters with mixed states correctly" do
# Create test data with multiple characters in different states
tracking_data = [
%{
character: %{eve_id: "character-1", name: "Character 1"},
tracked: true,
followed: true
},
%{
character: %{eve_id: "character-2", name: "Character 2"},
tracked: false,
followed: true
},
%{
character: %{eve_id: "character-3", name: "Character 3"},
tracked: true,
followed: false
},
%{
character: %{eve_id: "character-4", name: "Character 4"},
tracked: false,
followed: false
}
]
# Call the function and capture output
output = capture_io(fn ->
check_tracking_consistency(tracking_data)
end)
# Assert that only the inconsistent character triggered a warning
assert output =~ "Inconsistent state detected: Character is followed but not tracked"
assert output =~ "character-2"
assert output =~ "Character 2"
refute output =~ "character-1"
refute output =~ "character-3"
refute output =~ "character-4"
end
test "returns the original tracking data unchanged" do
# Create test data
tracking_data = [
%{
character: %{eve_id: "test-eve-id", name: "Test Character"},
tracked: false,
followed: true
}
]
# Call the function and get the result
result = check_tracking_consistency(tracking_data)
# Assert that the returned data is the same as the input data
assert result == tracking_data
end
test "handles empty tracking data without errors" do
# Create empty tracking data
tracking_data = []
# Call the function and capture output
output = capture_io(fn ->
result = check_tracking_consistency(tracking_data)
# Assert that the returned data is the same as the input data
assert result == tracking_data
end)
# Assert that no warning was logged
refute output =~ "Inconsistent state detected"
end
test "handles multiple inconsistent characters correctly" do
# Create test data with multiple inconsistent characters
tracking_data = [
%{
character: %{eve_id: "character-1", name: "Character 1"},
tracked: false,
followed: true
},
%{
character: %{eve_id: "character-2", name: "Character 2"},
tracked: false,
followed: true
},
%{
character: %{eve_id: "character-3", name: "Character 3"},
tracked: true,
followed: true
}
]
# Call the function and capture output
output = capture_io(fn ->
check_tracking_consistency(tracking_data)
end)
# Assert that warnings were logged for both inconsistent characters
assert output =~ "Character ID: character-1"
assert output =~ "Name: Character 1"
assert output =~ "Character ID: character-2"
assert output =~ "Name: Character 2"
refute output =~ "Character ID: character-3"
end
test "handles characters with missing fields gracefully" do
# Create test data with missing fields
tracking_data = [
%{
character: %{eve_id: "character-1"}, # Missing name
tracked: false,
followed: true
},
%{
character: %{name: "Character 2"}, # Missing eve_id
tracked: false,
followed: true
}
]
# Call the function and capture output
output = capture_io(fn ->
result = check_tracking_consistency(tracking_data)
# Assert that the returned data is the same as the input data
assert result == tracking_data
end)
# Assert that warnings were logged with available information
assert output =~ "Character ID: character-1"
assert output =~ "Name: unknown"
assert output =~ "Character ID: unknown"
assert output =~ "Name: Character 2"
end
test "handles characters with nil tracked or followed values" do
# Create test data with nil values
tracking_data = [
%{
character: %{eve_id: "character-1", name: "Character 1"},
tracked: nil,
followed: true
},
%{
character: %{eve_id: "character-2", name: "Character 2"},
tracked: false,
followed: nil
}
]
# Call the function and capture output
output = capture_io(fn ->
result = check_tracking_consistency(tracking_data)
# Assert that the returned data is the same as the input data
assert result == tracking_data
end)
# Assert that a warning was logged for the first character (nil tracked is treated as false)
assert output =~ "Character ID: character-1"
assert output =~ "Name: Character 1"
# No warning for the second character (nil followed is treated as false)
refute output =~ "Character ID: character-2"
end
test "handles malformed tracking data gracefully" do
# Create malformed tracking data (missing required fields)
tracking_data = [
%{
# Missing character field
tracked: false,
followed: true
}
]
# Call the function and capture output, expecting it to handle errors gracefully
assert_raise(KeyError, fn ->
check_tracking_consistency(tracking_data)
end)
end
end
# Additional tests for edge cases in the filter logic
describe "filter logic in check_tracking_consistency/1" do
test "correctly identifies characters that are followed but not tracked" do
# Create test data with various combinations
tracking_data = [
%{
character: %{eve_id: "char-1", name: "Character 1"},
tracked: false,
followed: true
},
%{
character: %{eve_id: "char-2", name: "Character 2"},
tracked: true,
followed: true
},
%{
character: %{eve_id: "char-3", name: "Character 3"},
tracked: false,
followed: false
},
%{
character: %{eve_id: "char-4", name: "Character 4"},
tracked: true,
followed: false
}
]
# Extract the filter logic from the function
inconsistent_characters = Enum.filter(tracking_data, fn data ->
data.followed && !data.tracked
end)
# Assert that only the first character is identified as inconsistent
assert length(inconsistent_characters) == 1
assert hd(inconsistent_characters).character.eve_id == "char-1"
end
test "handles boolean-like values correctly in filter logic" do
# Create test data with various boolean-like values
tracking_data = [
%{
character: %{eve_id: "char-1", name: "Character 1"},
tracked: false,
followed: "true" # String instead of boolean - in Elixir, only false and nil are falsy
}
]
# Extract the filter logic from the function
inconsistent_characters = Enum.filter(tracking_data, fn data ->
data.followed && !data.tracked
end)
# Assert that the character is identified as inconsistent
# (since in Elixir, only false and nil are falsy, everything else is truthy)
assert length(inconsistent_characters) == 1
end
end
end

View File

@@ -0,0 +1,143 @@
# Standalone test for the UtilAPIController
#
# This file can be run directly with:
# elixir test/standalone/util_api_controller_test.exs
#
# It doesn't require any database connections or external dependencies.
# Start ExUnit
ExUnit.start()
defmodule UtilAPIControllerTest do
use ExUnit.Case
# Mock controller that implements the functions we want to test
defmodule MockUtilAPIController do
# Simplified version of fetch_map_id from UtilAPIController
def fetch_map_id(params) do
cond do
params["map_id"] ->
case Integer.parse(params["map_id"]) do
{map_id, ""} -> {:ok, map_id}
_ -> {:error, "Invalid map_id format"}
end
params["slug"] ->
# In a real app, this would look up the map by slug
# For testing, we'll just use a simple mapping
case params["slug"] do
"test-map" -> {:ok, 1}
"another-map" -> {:ok, 2}
_ -> {:error, "Map not found"}
end
true ->
{:error, "Missing required param: map_id or slug"}
end
end
# Simplified version of require_param from UtilAPIController
def require_param(params, key) do
case params[key] do
nil -> {:error, "Missing required param: #{key}"}
"" -> {:error, "Param #{key} cannot be empty"}
val -> {:ok, val}
end
end
# Simplified version of parse_int from UtilAPIController
def parse_int(str) do
case Integer.parse(str) do
{num, ""} -> {:ok, num}
_ -> {:error, "Invalid integer for param id=#{str}"}
end
end
end
describe "fetch_map_id/1" do
test "returns map_id when valid map_id is provided" do
params = %{"map_id" => "123"}
result = MockUtilAPIController.fetch_map_id(params)
assert {:ok, 123} = result
end
test "returns map_id when valid slug is provided" do
params = %{"slug" => "test-map"}
result = MockUtilAPIController.fetch_map_id(params)
assert {:ok, 1} = result
end
test "returns error when map_id is invalid format" do
params = %{"map_id" => "not-a-number"}
result = MockUtilAPIController.fetch_map_id(params)
assert {:error, "Invalid map_id format"} = result
end
test "returns error when slug is not found" do
params = %{"slug" => "non-existent-map"}
result = MockUtilAPIController.fetch_map_id(params)
assert {:error, "Map not found"} = result
end
test "returns error when neither map_id nor slug is provided" do
params = %{}
result = MockUtilAPIController.fetch_map_id(params)
assert {:error, "Missing required param: map_id or slug"} = result
end
test "prioritizes map_id over slug when both are provided" do
params = %{"map_id" => "123", "slug" => "test-map"}
result = MockUtilAPIController.fetch_map_id(params)
assert {:ok, 123} = result
end
end
describe "require_param/2" do
test "returns value when param exists" do
params = %{"key" => "value"}
result = MockUtilAPIController.require_param(params, "key")
assert {:ok, "value"} = result
end
test "returns error when param is missing" do
params = %{}
result = MockUtilAPIController.require_param(params, "key")
assert {:error, "Missing required param: key"} = result
end
test "returns error when param is empty string" do
params = %{"key" => ""}
result = MockUtilAPIController.require_param(params, "key")
assert {:error, "Param key cannot be empty"} = result
end
end
describe "parse_int/1" do
test "returns integer when string is valid integer" do
result = MockUtilAPIController.parse_int("123")
assert {:ok, 123} = result
end
test "returns error when string is not a valid integer" do
result = MockUtilAPIController.parse_int("not-an-integer")
assert {:error, message} = result
assert message =~ "Invalid integer for param id"
end
test "returns error when string contains integer with extra characters" do
result = MockUtilAPIController.parse_int("123abc")
assert {:error, message} = result
assert message =~ "Invalid integer for param id"
end
end
end