mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-05 07:15:34 +00:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a5f96a847 | ||
|
|
149fa57075 | ||
|
|
affe184ccd | ||
|
|
1e5e73c4ae | ||
|
|
c76316da03 | ||
|
|
de6205f860 | ||
|
|
f994255091 | ||
|
|
6d4981a3db | ||
|
|
06fef2296f | ||
|
|
999a702291 | ||
|
|
020b9bb2c2 | ||
|
|
7713caab51 | ||
|
|
97a777d729 | ||
|
|
8241d1f08c | ||
|
|
2ac85bbfff | ||
|
|
3f68ae2235 | ||
|
|
0f7b6f75df | ||
|
|
b048e8f5ca | ||
|
|
9783dc45ff | ||
|
|
badbefbade | ||
|
|
b6a265cfad | ||
|
|
9b5ea2f84b | ||
|
|
d8acfa5c05 | ||
|
|
2a5b6924eb | ||
|
|
3b9aee1eb9 |
78
CHANGELOG.md
78
CHANGELOG.md
@@ -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)
|
||||
|
||||
|
||||
|
||||
7
Makefile
7
Makefile
@@ -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
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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>")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
2
mix.exs
2
mix.exs
@@ -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
|
||||
[
|
||||
|
||||
@@ -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**
|
||||
|
||||
---
|
||||
----
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
323
priv/resource_snapshots/repo/character_v1/20250323093826.json
Normal file
323
priv/resource_snapshots/repo/character_v1/20250323093826.json
Normal 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"
|
||||
}
|
||||
@@ -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=""
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
334
test/unit/character_api_controller_test.exs
Normal file
334
test/unit/character_api_controller_test.exs
Normal 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
|
||||
297
test/unit/common_api_controller_test.exs
Normal file
297
test/unit/common_api_controller_test.exs
Normal 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
|
||||
213
test/unit/map_api_controller_test.exs
Normal file
213
test/unit/map_api_controller_test.exs
Normal 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
|
||||
321
test/unit/map_route_api_controller_test.exs
Normal file
321
test/unit/map_route_api_controller_test.exs
Normal 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
|
||||
332
test/unit/tracking_utils_test.exs
Normal file
332
test/unit/tracking_utils_test.exs
Normal 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
|
||||
143
test/unit/util_api_controller_test.exs
Normal file
143
test/unit/util_api_controller_test.exs
Normal 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
|
||||
Reference in New Issue
Block a user