Compare commits

..

29 Commits

Author SHA1 Message Date
CI
999a702291 chore: release version v1.57.1
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / Manual Approval (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-03-20 17:51:07 +00:00
Dmitry Popov
020b9bb2c2 chore: added user-agent & ensured cache handled correctly on each request 2025-03-20 18:39:40 +01:00
CI
7713caab51 chore: release version v1.57.0
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / Manual Approval (push) Blocked by required conditions
Build / 🛠 Build (1.17, 18.x, 27) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-03-19 15:33:39 +00:00
guarzo
97a777d729 feat (doc): update bot news (#294) 2025-03-19 19:17:25 +04:00
CI
8241d1f08c chore: release version v1.56.6 2025-03-19 14:45:22 +00:00
Dmitry Popov
2ac85bbfff chore: release version v1.56.5 2025-03-19 15:08:51 +01:00
CI
3f68ae2235 chore: release version v1.56.5
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / Manual Approval (push) Blocked by required conditions
Build / 🛠 Build (1.17, 18.x, 27) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-03-19 13:07:55 +00:00
Dmitry Popov
0f7b6f75df Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-03-19 13:57:58 +01:00
Dmitry Popov
b048e8f5ca chore: added fallback chipher options 2025-03-19 13:55:39 +01:00
CI
9783dc45ff chore: release version v1.56.4 2025-03-19 11:36:46 +00:00
Dmitry Popov
badbefbade Revert "fix: cloak key error behavior (#288)" (#290)
This reverts commit 9b5ea2f84b.
2025-03-19 15:30:07 +04:00
CI
b6a265cfad chore: release version v1.56.3 2025-03-19 07:26:24 +00:00
guarzo
9b5ea2f84b fix: cloak key error behavior (#288) 2025-03-19 11:13:54 +04:00
guarzo
d8acfa5c05 refactor: standalone unit tests (#278)
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / Manual Approval (push) Blocked by required conditions
Build / 🛠 Build (1.17, 18.x, 27) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-03-18 21:37:52 +04:00
CI
2a5b6924eb chore: release version v1.56.2 2025-03-18 16:47:40 +00:00
Dmitry Popov
3b9aee1eb9 fix: show signature tooltip on top 2025-03-18 17:33:18 +01:00
CI
83801c9063 chore: release version v1.56.1
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / Manual Approval (push) Blocked by required conditions
Build / 🛠 Build (1.17, 18.x, 27) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-03-18 09:34:32 +00:00
guarzo
0f34350c58 fix: update activity api (#284) 2025-03-18 11:51:29 +04:00
guarzo
1c4c0f0715 fix: qol updates for dev (#283) 2025-03-18 11:50:33 +04:00
Dmitry Popov
3825fc831a refactor: removal of legacy event
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / Manual Approval (push) Blocked by required conditions
Build / 🛠 Build (1.17, 18.x, 27) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-03-17 22:51:29 +01:00
guarzo
654670cbc8 refactor: removal of legacy event (#277)
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / Manual Approval (push) Blocked by required conditions
Build / 🛠 Build (1.17, 18.x, 27) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-03-18 00:11:32 +04:00
CI
947570072c chore: release version v1.56.0 2025-03-17 18:25:38 +00:00
Dmitry Popov
01b6b45380 fix: character activity hide error 2025-03-17 18:54:22 +01:00
guarzo
b9dc1f8357 fix: character added to map on follow (#272) 2025-03-17 19:42:42 +04:00
guarzo
b4bd810c9d refactor: updates to track and follow (#270) 2025-03-17 17:36:08 +04:00
guarzo
490b037920 refactor: simplify track and follow (#265)
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / Manual Approval (push) Blocked by required conditions
Build / 🛠 Build (1.17, 18.x, 27) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🛠 Build Docker Images (linux/arm64) (push) Blocked by required conditions
Build / merge (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-03-17 16:08:45 +04:00
guarzo
cdff5458bc feat: add static wh info (#262)
* feat: add static wh info
2025-03-17 16:03:57 +04:00
guarzo
09314a09e9 feat [doc]: new bot release (#234) 2025-03-17 11:43:27 +04:00
guarzo
49ea8edb27 feat (api): add character activity api (#263)
* feat (api): add character activity api
2025-03-17 11:36:45 +04:00
47 changed files with 21286 additions and 1519 deletions

View File

@@ -1,6 +1,27 @@
FROM elixir:1.17-otp-27
RUN apt install -yq curl gnupg
# Install OS packages and Node.js (via nodesource),
# plus inotify-tools and yarn
RUN apt-get update && apt-get install -y --no-install-recommends \
sudo \
curl \
make \
git \
bash \
build-essential \
ca-certificates \
jq \
vim \
net-tools \
procps \
# Optionally add any other tools you need, e.g. vim, wget...
&& curl -sL https://deb.nodesource.com/setup_18.x | bash - \
&& apt-get install -y --no-install-recommends nodejs inotify-tools \
&& npm install -g yarn \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
RUN apt --fix-broken install
RUN mix local.hex --force

View File

@@ -1,20 +1,30 @@
{
"name": "wanderer-dev",
"dockerComposeFile": ["./docker-compose.yml"],
"extensions": [
"jakebecker.elixir-ls",
"JakeBecker.elixir-ls",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
],
"customizations": {
"vscode": {
"extensions": [
"jakebecker.elixir-ls",
"JakeBecker.elixir-ls",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
],
"settings": {
"editor.formatOnSave": true,
"search.exclude": {
"**/doc": true
},
"elixirLS.dialyzerEnabled": false
}
}
},
"service": "wanderer",
"workspaceFolder": "/app",
"shutdownAction": "stopCompose",
"settings": {
"editor.formatOnSave": true,
"search.exclude": {
"**/doc": true
},
"elixirLS.dialyzerEnabled": false
}
"features": {
"ghcr.io/devcontainers/features/common-utils:2": {
"networkArgs": ["--add-host=host.docker.internal:host-gateway"]
}
},
"forwardPorts": [4444]
}

View File

@@ -14,15 +14,15 @@ services:
wanderer:
environment:
PORT: 8000
PORT: 4444
DB_HOST: db
WEB_APP_URL: "http://localhost:8000"
WEB_APP_URL: "http://localhost:4444"
ERL_AFLAGS: "-kernel shell_history enabled"
build:
context: .
dockerfile: Dockerfile
ports:
- 8000:8000
- 4444:4444
volumes:
- ..:/app:delegated
- ~/.gitconfig:/root/.gitconfig

View File

@@ -2,6 +2,85 @@
<!-- changelog -->
## [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)
### Bug Fixes:
* update activity api (#284)
* qol updates for dev (#283)
## [v1.56.0](https://github.com/wanderer-industries/wanderer/compare/v1.55.2...v1.56.0) (2025-03-17)
### Features:
* add static wh info (#262)
* add static wh info
* api: add character activity api (#263)
* api: add character activity api
### Bug Fixes:
* character activity hide error
* character added to map on follow (#272)
## [v1.55.2](https://github.com/wanderer-industries/wanderer/compare/v1.55.1...v1.55.2) (2025-03-16)

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
import { useEffect, useMemo, useState } from 'react';
import { useCallback, useMemo } from 'react';
import { Dialog } from 'primereact/dialog';
import { VirtualScroller } from 'primereact/virtualscroller';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { TrackingCharacter } from './types';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { TrackingCharacterWrapper } from './TrackingCharacterWrapper';
import { TrackingCharacter } from './types';
interface TrackAndFollowProps {
visible: boolean;
@@ -12,80 +12,50 @@ interface TrackAndFollowProps {
}
export const TrackAndFollow = ({ visible, onHide }: TrackAndFollowProps) => {
const [trackedCharacters, setTrackedCharacters] = useState<string[]>([]);
const [followedCharacter, setFollowedCharacter] = useState<string | null>(null);
const { outCommand, data } = useMapRootState();
const { trackingCharactersData } = data;
const characters = useMemo(() => trackingCharactersData || [], [trackingCharactersData]);
useEffect(() => {
if (trackingCharactersData) {
const newTrackedCharacters = trackingCharactersData.filter(tc => tc.tracked).map(tc => tc.character.eve_id);
setTrackedCharacters(newTrackedCharacters);
const followedChar = trackingCharactersData.find(tc => tc.followed);
if (followedChar?.character?.eve_id !== followedCharacter) {
setFollowedCharacter(followedChar?.character?.eve_id || null);
}
}
}, [followedCharacter, trackingCharactersData]);
const handleTrackToggle = (characterId: string) => {
const isCurrentlyTracked = trackedCharacters.includes(characterId);
if (isCurrentlyTracked) {
setTrackedCharacters(prev => prev.filter(id => id !== characterId));
} else {
setTrackedCharacters(prev => [...prev, characterId]);
}
outCommand({
type: OutCommand.toggleTrack,
data: { 'character-id': characterId },
});
};
const handleFollowToggle = async (characterEveId: string) => {
const isCurrentlyFollowed = followedCharacter === characterEveId;
const isCurrentlyTracked = trackedCharacters.includes(characterEveId);
// If not followed and not tracked, we need to track it first
if (!isCurrentlyFollowed && !isCurrentlyTracked) {
setTrackedCharacters(prev => [...prev, characterEveId]);
// Send track command first
await outCommand({
type: OutCommand.toggleTrack,
data: { 'character-id': characterEveId },
});
// Then send follow command after a short delay
setTimeout(() => {
outCommand({
type: OutCommand.toggleFollow,
data: { 'character-id': characterEveId },
const handleTrackToggle = useCallback(
async (characterId: string) => {
try {
await outCommand({
type: OutCommand.toggleTrack,
data: { character_id: characterId },
});
}, 100);
} else {
// Otherwise just toggle follow
await outCommand({
type: OutCommand.toggleFollow,
data: { 'character-id': characterEveId },
});
}
};
} catch (error) {
console.error('Error toggling track:', error);
}
},
[outCommand],
);
const handleFollowToggle = useCallback(
async (characterId: string) => {
try {
await outCommand({
type: OutCommand.toggleFollow,
data: { character_id: characterId },
});
} catch (error) {
console.error('Error toggling follow:', error);
}
},
[outCommand],
);
const rowTemplate = (tc: TrackingCharacter) => {
const characterEveId = tc.character.eve_id;
return (
<TrackingCharacterWrapper
key={tc.character.eve_id}
key={characterEveId}
character={tc.character}
isTracked={trackedCharacters.includes(tc.character.eve_id)}
isFollowed={followedCharacter === tc.character.eve_id}
onTrackToggle={() => handleTrackToggle(tc.character.eve_id)}
onFollowToggle={() => handleFollowToggle(tc.character.eve_id)}
isTracked={tc.tracked}
isFollowed={tc.followed}
onTrackToggle={() => handleTrackToggle(characterEveId)}
onFollowToggle={() => handleFollowToggle(characterEveId)}
/>
);
};

View File

@@ -1,7 +1,9 @@
import { WdCheckbox } from '@/hooks/Mapper/components/ui-kit/WdCheckbox/WdCheckbox';
import WdRadioButton from '@/hooks/Mapper/components/ui-kit/WdRadioButton';
import { CharacterCard, TooltipPosition, WdTooltipWrapper } from '../../../ui-kit';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit/WdTooltip/WdTooltip';
import { CharacterCard } from '@/hooks/Mapper/components/ui-kit';
import { CharacterTypeRaw } from '@/hooks/Mapper/types';
import WdRadioButton from '@/hooks/Mapper/components/ui-kit/WdRadioButton';
interface TrackingCharacterWrapperProps {
character: CharacterTypeRaw;
@@ -26,7 +28,7 @@ export const TrackingCharacterWrapper = ({
<div className="flex justify-center items-center p-0.5 text-center">
<WdTooltipWrapper content="Track this character on the map" position={TooltipPosition.top}>
<div className="flex justify-center items-center w-full">
<WdCheckbox id={trackCheckboxId} label="" value={isTracked} onChange={() => onTrackToggle()} />
<WdCheckbox id={trackCheckboxId} label="" value={isTracked} onChange={onTrackToggle} />
</div>
</WdTooltipWrapper>
</div>

View File

@@ -18,13 +18,7 @@ export const useCharacterActivityHandlers = () => {
...state,
showCharacterActivity: false,
}));
// Send the command to the server
outCommand({
type: OutCommand.hideActivity,
data: {},
});
}, [outCommand, update]);
}, [update]);
/**
* Handle showing the character activity dialog
@@ -56,7 +50,7 @@ export const useCharacterActivityHandlers = () => {
// Update local state with the activity data
update(state => ({
...state,
characterActivityData: activityData.activity,
characterActivityData: activityData,
showCharacterActivity: true,
}));
},

View File

@@ -13,18 +13,12 @@ export const useTrackAndFollowHandlers = () => {
* Handle hiding the track and follow dialog
*/
const handleHideTracking = useCallback(() => {
// Send the command to the server first
outCommand({
type: OutCommand.hideTracking,
data: {},
});
// Then update local state to hide the dialog
update(state => ({
...state,
showTrackAndFollow: false,
}));
}, [outCommand, update]);
}, [update]);
/**
* Handle showing the track and follow dialog
@@ -101,7 +95,6 @@ export const useTrackAndFollowHandlers = () => {
[outCommand],
);
/**
* Handle user settings updates
*/

View File

@@ -45,16 +45,9 @@ export const useCommandsActivity = () => {
}));
}, []);
const hideTracking = useCallback(() => {
ref.current.update((state: MapRootData) => ({
...state,
showTrackAndFollow: false,
}));
}, []);
const userSettingsUpdated = useCallback((data: CommandUserSettingsUpdated) => {
emitMapEvent({ name: Commands.userSettingsUpdated, data });
}, []);
return { characterActivityData, trackingCharactersData, userSettingsUpdated, hideActivity, hideTracking };
return { characterActivityData, trackingCharactersData, userSettingsUpdated, hideActivity };
};

View File

@@ -241,7 +241,6 @@ export enum OutCommand {
// Only UI commands
openSettings = 'open_settings',
showActivity = 'show_activity',
hideTracking = 'hide_tracking',
showTracking = 'show_tracking',
getUserSettings = 'get_user_settings',
updateUserSettings = 'update_user_settings',

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 524 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -112,6 +112,31 @@ defmodule WandererApp.CachedInfo do
end
end
def get_wormhole_classes() do
case WandererApp.Cache.lookup(:wormhole_classes) do
{:ok, nil} ->
wormhole_classes = WandererApp.EveDataService.load_wormhole_classes()
cache_items(wormhole_classes, :wormhole_classes)
{:ok, wormhole_classes}
{:ok, wormhole_classes} ->
{:ok, wormhole_classes}
end
end
def get_wormhole_classes!() do
case get_wormhole_classes() do
{:ok, wormhole_classes} ->
wormhole_classes
error ->
Logger.error("Error loading wormhole classes: #{inspect(error)}")
error
end
end
def get_effects() do
case WandererApp.Cache.lookup(:effects) do
{:ok, nil} ->

View File

@@ -0,0 +1,374 @@
defmodule WandererApp.Character.TrackingUtils do
@moduledoc """
Utility functions for handling character tracking and following operations.
"""
require Logger
alias WandererApp.MapCharacterSettingsRepo
@doc """
Toggles the tracking state for a character on a map.
Returns the updated tracking data for all characters with access to the map.
"""
def toggle_track(map_id, character_id, current_user_id, caller_pid \\ nil, only_tracked_characters \\ false) do
with current_user when not is_nil(current_user) <- WandererApp.User.load(current_user_id),
{:ok, character} <- WandererApp.Character.find_character_by_eve_id(current_user, character_id),
{:ok, map} <- WandererApp.Api.Map.by_id(map_id),
map <- Ash.load!(map, :acls) do
# Check if the character is currently tracked before toggling
{:ok, existing_settings} = MapCharacterSettingsRepo.get_by_map(map_id, character.id)
was_tracked = existing_settings && existing_settings.tracked
# Toggle the tracking state
with {:ok, _updated_settings} <- do_toggle_character_tracking(character, map_id, caller_pid) do
# Get updated tracking data
{:ok, tracking_data} = build_tracking_data(map_id, current_user_id)
# Determine which event to send based on tracking mode and previous state
event = case {only_tracked_characters, was_tracked} do
{true, true} -> :not_all_characters_tracked # Untracking in tracked-only mode
_ -> %{event: :refresh_user_characters} # All other cases
end
{:ok, tracking_data, event}
else
error ->
Logger.error("Failed to toggle tracking: #{inspect(error)}")
{:error, "Failed to toggle tracking"}
end
else
nil ->
Logger.error("User not found when toggling track")
{:error, "User not found"}
error ->
Logger.error("Failed to toggle track: #{inspect(error)}")
{:error, "Failed to toggle track"}
end
end
@doc """
Toggles the follow state for a character on a map.
Returns the updated tracking data for all characters with access to the map.
"""
def toggle_follow(map_id, character_id, current_user_id, caller_pid \\ nil) do
# Get all settings before the operation to see the followed state
{:ok, all_settings_before} = MapCharacterSettingsRepo.get_all_by_map(map_id)
# Check if the clicked character is already followed
is_already_followed =
all_settings_before
|> Enum.any?(fn setting ->
setting.followed && "#{setting.character_id}" == "#{character_id}"
end)
with current_user when not is_nil(current_user) <- WandererApp.User.load(current_user_id),
{:ok, clicked_char} <- WandererApp.Character.find_character_by_eve_id(current_user, character_id),
{:ok, _updated_settings} <- do_toggle_character_follow(map_id, clicked_char, is_already_followed, caller_pid) do
# Get updated tracking data
{:ok, tracking_data} = build_tracking_data(map_id, current_user_id)
# Always send refresh_user_characters for follow operations
{:ok, tracking_data, %{event: :refresh_user_characters}}
else
nil ->
Logger.error("User not found when toggling follow")
{:error, "User not found"}
error ->
Logger.error("Failed to toggle follow: #{inspect(error)}")
{:error, "Failed to toggle follow"}
end
end
@doc """
Builds tracking data for all characters with access to a map.
"""
def build_tracking_data(map_id, current_user_id) do
with current_user when not is_nil(current_user) <- WandererApp.User.load(current_user_id),
{:ok, map} <- WandererApp.Api.Map.by_id(map_id),
map <- Ash.load!(map, :acls),
{:ok, character_settings} <- MapCharacterSettingsRepo.get_all_by_map(map_id),
{:ok, %{characters: characters_with_access}} <-
WandererApp.Maps.load_characters(map, character_settings, current_user.id) do
# Map characters to tracking data
tracking_data = build_character_tracking_data(characters_with_access, character_settings)
# Check for inconsistent state
check_tracking_consistency(tracking_data)
{:ok, tracking_data}
else
nil ->
Logger.warning("User not found when building tracking data", %{user_id: current_user_id})
{:error, "User not found"}
error ->
Logger.error("Error building tracking data: #{inspect(error)}")
{:error, "Failed to build tracking data"}
end
end
# Helper to build tracking data for each character
defp build_character_tracking_data(characters, character_settings) do
Enum.map(characters, fn char ->
setting = Enum.find(character_settings, &(&1.character_id == char.id))
%{
character: char |> WandererAppWeb.MapEventHandler.map_ui_character_stat(),
tracked: setting && setting.tracked || false,
followed: setting && setting.followed || false
}
end)
end
# Helper to check for inconsistent tracking state
defp check_tracking_consistency(tracking_data) do
followed_in_data = Enum.find(tracking_data, &(&1.followed))
followed_but_not_tracked = followed_in_data && !Enum.find(tracking_data, &(&1.followed && &1.tracked))
if followed_but_not_tracked do
Logger.warning("Inconsistent state detected: Character is followed but not tracked", %{
character_id: followed_in_data.character.eve_id
})
end
end
# Private implementation of toggle character tracking
defp do_toggle_character_tracking(character, map_id, caller_pid) do
with false <- is_nil(caller_pid),
{:ok, existing_settings} <- MapCharacterSettingsRepo.get_by_map(map_id, character.id) do
case existing_settings.tracked do
# Untracking flow
true ->
with {:ok, updated_settings} <- untrack_character_settings(existing_settings),
:ok <- untrack_characters([character], map_id, caller_pid),
:ok <- remove_characters([character], map_id) do
{:ok, updated_settings}
end
# Tracking flow
false ->
with {:ok, updated_settings} <- MapCharacterSettingsRepo.track(existing_settings),
:ok <- track_characters([character], map_id, true, caller_pid),
:ok <- add_characters([character], map_id, true) do
{:ok, updated_settings}
end
end
else
true ->
Logger.error("caller_pid is required for toggling character tracking")
{:error, "caller_pid is required"}
{:error, :not_found} ->
# Create new settings with tracking enabled
create_character_settings(character.id, map_id, true, false, caller_pid)
error -> error
end
end
# Helper to untrack character settings, handling the followed state
defp untrack_character_settings(settings) do
case settings.followed do
true ->
# First unfollow, then untrack
with {:ok, unfollowed_settings} <- MapCharacterSettingsRepo.unfollow(settings) do
MapCharacterSettingsRepo.untrack(unfollowed_settings)
end
false ->
# Just untrack
MapCharacterSettingsRepo.untrack(settings)
end
end
# Private implementation of toggle character follow
defp do_toggle_character_follow(map_id, clicked_char, is_already_followed, caller_pid) do
with false <- is_nil(caller_pid),
{:ok, clicked_char_settings} <- MapCharacterSettingsRepo.get_by_map(map_id, clicked_char.id) do
case {is_already_followed, clicked_char_settings.tracked} do
# Case 1: Already followed and not tracked - unfollow and remove
{true, false} ->
with {:ok, _} <- MapCharacterSettingsRepo.unfollow(clicked_char_settings),
:ok <- remove_characters([clicked_char], map_id) do
{:ok, clicked_char_settings}
end
# Case 2: Already followed and tracked - just unfollow
{true, true} ->
MapCharacterSettingsRepo.unfollow(clicked_char_settings)
# Case 3: Not followed - ensure tracked and followed
{false, _} ->
ensure_character_tracked_and_followed(map_id, clicked_char, clicked_char_settings, caller_pid)
end
else
true ->
Logger.error("caller_pid is required for toggling character following")
{:error, "caller_pid is required"}
{:error, :not_found} ->
# Create new settings with both tracking and following enabled
create_character_settings(clicked_char.id, map_id, true, true, caller_pid, clicked_char)
end
end
# Consolidated helper function to ensure a character is tracked when followed
defp ensure_character_tracked_and_followed(map_id, character, settings, caller_pid) do
with false <- is_nil(caller_pid) do
# Toggle the followed state
followed = !settings.followed
case {followed, settings.tracked} do
# Case 1: Following and not tracked - need to track and follow
{true, false} ->
# Unfollow all others first
:ok = maybe_unfollow_others(map_id, character.id, true)
# Track and follow
with {:ok, tracked_settings} <- MapCharacterSettingsRepo.track(settings),
{:ok, updated_settings} <- update_follow(tracked_settings, true),
:ok <- track_characters([character], map_id, true, caller_pid),
:ok <- add_characters([character], map_id, true) do
{:ok, updated_settings}
end
# Case 2: Following and already tracked - just follow
{true, true} ->
# Unfollow all others first
:ok = maybe_unfollow_others(map_id, character.id, true)
# Update follow status
update_follow(settings, true)
# Case 3: Unfollowing - just update follow status
{false, _} ->
update_follow(settings, false)
end
else
true ->
Logger.error("caller_pid is required for ensuring character is tracked and followed")
{:error, "caller_pid is required"}
end
end
# Helper function to create character settings with specified tracking and following states
defp create_character_settings(character_id, map_id, tracked, followed, caller_pid, character \\ nil) do
with false <- is_nil(caller_pid) do
# Unfollow others if needed
case followed do
true -> :ok = maybe_unfollow_others(map_id, character_id, true)
false -> :ok
end
# Create the settings
{:ok, settings} = MapCharacterSettingsRepo.create(%{
character_id: character_id,
map_id: map_id,
tracked: tracked,
followed: followed
})
# Handle tracking based on character presence and tracking flag
case {character, tracked} do
{nil, _} ->
# No character provided, just return settings
{:ok, settings}
{_, false} ->
# Character provided but not tracking
{:ok, settings}
{_, true} ->
# Character provided and tracking enabled
with :ok <- track_characters([character], map_id, true, caller_pid),
:ok <- add_characters([character], map_id, true) do
{:ok, settings}
end
end
else
true ->
Logger.error("caller_pid is required for creating character settings")
{:error, "caller_pid is required"}
end
end
defp maybe_unfollow_others(_map_id, _char_id, false), do: :ok
defp maybe_unfollow_others(map_id, char_id, true) do
# Unfollow all other characters when setting a character as followed
with {:ok, all_settings} <- MapCharacterSettingsRepo.get_all_by_map(map_id) do
all_settings
|> Enum.filter(&(&1.character_id != char_id && &1.followed))
|> Enum.each(&MapCharacterSettingsRepo.unfollow/1)
:ok
end
end
defp update_follow(settings, true), do: MapCharacterSettingsRepo.follow(settings)
defp update_follow(settings, false), do: MapCharacterSettingsRepo.unfollow(settings)
# Helper functions for character tracking
def track_characters(_, _, false, _), do: :ok
def track_characters([], _map_id, _is_track_character?, _), do: :ok
def track_characters([character | characters], map_id, true, caller_pid) do
with :ok <- track_character(character, map_id, caller_pid) do
track_characters(characters, map_id, true, caller_pid)
end
end
def track_character(%{id: character_id, eve_id: eve_id, corporation_id: corporation_id, alliance_id: alliance_id}, map_id, caller_pid) do
with false <- is_nil(caller_pid) do
WandererAppWeb.Presence.track(caller_pid, map_id, character_id, %{})
cache_key = "#{inspect(caller_pid)}_map_#{map_id}:character_#{character_id}:tracked"
case WandererApp.Cache.lookup!(cache_key, false) do
true ->
:ok
_ ->
:ok = Phoenix.PubSub.subscribe(WandererApp.PubSub, "character:#{eve_id}")
:ok = WandererApp.Cache.put(cache_key, true)
end
:ok = WandererApp.Character.TrackerManager.start_tracking(character_id)
else
true ->
Logger.error("caller_pid is required for tracking characters")
{:error, "caller_pid is required"}
end
end
def untrack_characters(characters, map_id, caller_pid) do
with false <- is_nil(caller_pid) do
characters
|> Enum.each(fn character ->
WandererAppWeb.Presence.untrack(caller_pid, map_id, character.id)
WandererApp.Cache.put("#{inspect(caller_pid)}_map_#{map_id}:character_#{character.id}:tracked", false)
:ok = Phoenix.PubSub.unsubscribe(WandererApp.PubSub, "character:#{character.eve_id}")
end)
:ok
else
true ->
Logger.error("caller_pid is required for untracking characters")
{:error, "caller_pid is required"}
end
end
def add_characters([], _map_id, _track_character), do: :ok
def add_characters([character | characters], map_id, track_character) do
:ok = WandererApp.Map.Server.add_character(map_id, character, track_character)
add_characters(characters, map_id, track_character)
end
def remove_characters([], _map_id), do: :ok
def remove_characters([character | characters], map_id) do
:ok = WandererApp.Map.Server.remove_character(map_id, character.id)
remove_characters(characters, map_id)
end
end

View File

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

View File

@@ -9,6 +9,7 @@ defmodule WandererApp.Esi.ApiClient do
@routes_ttl :timer.minutes(15)
@base_url "https://esi.evetech.net/latest"
@wanderrer_user_agent "(wanderer-industries@proton.me; +https://github.com/wanderer-industries/wanderer)"
@get_link_pairs_advanced_params [
:include_mass_crit,
@@ -289,14 +290,13 @@ defmodule WandererApp.Esi.ApiClient do
end
end
@decorate cacheable(
cache: Cache,
key: "killmail-#{killmail_id}-#{killmail_hash}",
opts: [ttl: @ttl]
)
cache: Cache,
key: "killmail-#{killmail_id}-#{killmail_hash}",
opts: [ttl: @ttl]
)
def get_killmail(killmail_id, killmail_hash, opts \\ []) do
get("/killmails/#{killmail_id}/#{killmail_hash}/", _with_cache_opts(opts))
get("/killmails/#{killmail_id}/#{killmail_hash}/", opts)
end
@decorate cacheable(
@@ -319,7 +319,7 @@ defmodule WandererApp.Esi.ApiClient do
def get_character_info(eve_id, opts \\ []) do
case get(
"/characters/#{eve_id}/",
opts |> _with_cache_opts()
opts
) do
{:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
{:error, error} -> {:error, error}
@@ -370,10 +370,10 @@ defmodule WandererApp.Esi.ApiClient do
end
@decorate cacheable(
cache: Cache,
key: "search-#{character_eve_id}-#{categories_val}-#{search_val |> Slug.slugify()}",
opts: [ttl: @ttl]
)
cache: Cache,
key: "search-#{character_eve_id}-#{categories_val}-#{search_val |> Slug.slugify()}",
opts: [ttl: @ttl]
)
defp _search(character_eve_id, search_val, categories_val, merged_opts) do
_get_character_auth_data(character_eve_id, "search", merged_opts)
end
@@ -405,23 +405,23 @@ defmodule WandererApp.Esi.ApiClient do
do:
get(
"/route/#{origin}/#{destination}/?#{params |> Plug.Conn.Query.encode()}",
opts |> _with_cache_opts()
opts
)
defp _get_auth_opts(opts), do: [auth: {:bearer, opts[:access_token]}]
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 +429,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 +440,7 @@ defmodule WandererApp.Esi.ApiClient do
opts
)
else
_get_retry(path, auth_opts, opts)
get_retry(path, auth_opts, opts)
end
end
@@ -458,11 +458,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 +477,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 +496,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 +517,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 +541,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 +551,7 @@ defmodule WandererApp.Esi.ApiClient do
else
case _refresh_token(character_id) do
{:ok, token} ->
auth_opts = [access_token: token.access_token] |> _get_auth_opts()
auth_opts = [access_token: token.access_token] |> get_auth_opts()
get(
path,

View File

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

View File

@@ -15,13 +15,14 @@ defmodule WandererAppWeb.CharactersAPIController do
items: %OpenApiSpex.Schema{
type: :object,
properties: %{
id: %OpenApiSpex.Schema{type: :string},
eve_id: %OpenApiSpex.Schema{type: :string},
name: %OpenApiSpex.Schema{type: :string},
corporation_name: %OpenApiSpex.Schema{type: :string},
alliance_name: %OpenApiSpex.Schema{type: :string}
corporation_id: %OpenApiSpex.Schema{type: :string},
corporation_ticker: %OpenApiSpex.Schema{type: :string},
alliance_id: %OpenApiSpex.Schema{type: :string},
alliance_ticker: %OpenApiSpex.Schema{type: :string}
},
required: ["id", "eve_id", "name"]
required: ["eve_id", "name"]
}
}
},
@@ -47,13 +48,7 @@ defmodule WandererAppWeb.CharactersAPIController do
{:ok, characters} ->
result =
characters
|> Enum.map(&%{
id: &1.id,
eve_id: &1.eve_id,
name: &1.name,
corporation_name: &1.corporation_name,
alliance_name: &1.alliance_name
})
|> Enum.map(&WandererAppWeb.MapEventHandler.map_ui_character_stat/1)
json(conn, %{data: result})

View File

@@ -4,6 +4,7 @@ defmodule WandererAppWeb.CommonAPIController do
alias WandererApp.CachedInfo
alias WandererAppWeb.UtilAPIController, as: Util
alias WandererApp.EveDataService
@system_static_response_schema %OpenApiSpex.Schema{
type: :object,
@@ -26,6 +27,32 @@ defmodule WandererAppWeb.CommonAPIController do
effect_name: %OpenApiSpex.Schema{type: :string},
effect_power: %OpenApiSpex.Schema{type: :integer},
statics: %OpenApiSpex.Schema{type: :array, items: %OpenApiSpex.Schema{type: :string}},
static_details: %OpenApiSpex.Schema{
type: :array,
items: %OpenApiSpex.Schema{
type: :object,
properties: %{
name: %OpenApiSpex.Schema{type: :string},
destination: %OpenApiSpex.Schema{
type: :object,
properties: %{
id: %OpenApiSpex.Schema{type: :string},
name: %OpenApiSpex.Schema{type: :string},
short_name: %OpenApiSpex.Schema{type: :string}
}
},
properties: %OpenApiSpex.Schema{
type: :object,
properties: %{
lifetime: %OpenApiSpex.Schema{type: :string},
max_mass: %OpenApiSpex.Schema{type: :integer},
max_jump_mass: %OpenApiSpex.Schema{type: :integer},
mass_regeneration: %OpenApiSpex.Schema{type: :integer}
}
}
}
}
},
wandering: %OpenApiSpex.Schema{type: :array, items: %OpenApiSpex.Schema{type: :string}},
triglavian_invasion_status: %OpenApiSpex.Schema{type: :string},
sun_type_id: %OpenApiSpex.Schema{type: :integer}
@@ -64,8 +91,14 @@ defmodule WandererAppWeb.CommonAPIController do
{:ok, solar_system_id} <- Util.parse_int(solar_system_str) do
case CachedInfo.get_system_static_info(solar_system_id) do
{:ok, system} ->
# Get basic system data
data = static_system_to_json(system)
json(conn, %{data: data})
# Enhance with wormhole type information if statics exist
enhanced_data = enhance_with_static_details(data)
# Return the enhanced data
json(conn, %{data: enhanced_data})
{:error, :not_found} ->
conn
@@ -80,6 +113,11 @@ defmodule WandererAppWeb.CommonAPIController do
end
end
@doc """
Converts a system map to a JSON-friendly format.
Takes only the fields that are needed for the API response.
"""
defp static_system_to_json(system) do
system
|> Map.take([
@@ -103,4 +141,97 @@ defmodule WandererAppWeb.CommonAPIController do
:sun_type_id
])
end
@doc """
Enhances system data with wormhole type information.
If the system has static wormholes, adds detailed information about each static.
Otherwise, returns the original data unchanged.
"""
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
@doc """
Gets detailed information for each static wormhole.
Uses the CachedInfo to get both wormhole type data and wormhole class data.
"""
defp get_static_details(statics) do
# Get wormhole data from CachedInfo
{:ok, wormhole_types} = CachedInfo.get_wormhole_types()
wormhole_classes = CachedInfo.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
@doc """
Creates detailed wormhole information when the wormhole type is found.
Includes information about the destination and properties of the wormhole.
Ensures that destination.id is always a string to match the OpenAPI schema.
"""
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
@doc """
Creates fallback information when a wormhole type is not found.
Provides a placeholder structure with nil values for unknown wormhole types.
"""
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

View File

@@ -65,19 +65,14 @@ defmodule WandererAppWeb.MapAPIController do
@character_schema %OpenApiSpex.Schema{
type: :object,
properties: %{
id: %OpenApiSpex.Schema{type: :string},
eve_id: %OpenApiSpex.Schema{type: :string},
name: %OpenApiSpex.Schema{type: :string},
corporation_id: %OpenApiSpex.Schema{type: :string},
corporation_name: %OpenApiSpex.Schema{type: :string},
corporation_ticker: %OpenApiSpex.Schema{type: :string},
alliance_id: %OpenApiSpex.Schema{type: :string},
alliance_name: %OpenApiSpex.Schema{type: :string},
alliance_ticker: %OpenApiSpex.Schema{type: :string},
inserted_at: %OpenApiSpex.Schema{type: :string, format: :date_time},
updated_at: %OpenApiSpex.Schema{type: :string, format: :date_time}
alliance_ticker: %OpenApiSpex.Schema{type: :string}
},
required: ["id", "eve_id", "name"]
required: ["eve_id", "name"]
}
@tracked_char_schema %OpenApiSpex.Schema{
@@ -174,6 +169,31 @@ defmodule WandererAppWeb.MapAPIController do
required: ["data"]
}
# For operation :character_activity
@character_activity_item_schema %OpenApiSpex.Schema{
type: :object,
description: "Character activity data",
properties: %{
character: @character_schema,
passages: %OpenApiSpex.Schema{type: :integer, description: "Number of passages through systems"},
connections: %OpenApiSpex.Schema{type: :integer, description: "Number of connections created"},
signatures: %OpenApiSpex.Schema{type: :integer, description: "Number of signatures added"},
timestamp: %OpenApiSpex.Schema{type: :string, format: :date_time, description: "Timestamp of the activity"}
},
required: ["character", "passages", "connections", "signatures"]
}
@character_activity_response_schema %OpenApiSpex.Schema{
type: :object,
properties: %{
data: %OpenApiSpex.Schema{
type: :array,
items: @character_activity_item_schema
}
},
required: ["data"]
}
# -----------------------------------------------------------------
# MAP endpoints
# -----------------------------------------------------------------
@@ -622,6 +642,107 @@ defmodule WandererAppWeb.MapAPIController do
end
end
@doc """
GET /api/map/character_activity
Returns character activity data for a map.
Requires either `?map_id=<UUID>` or `?slug=<map-slug>`.
Example:
GET /api/map/character_activity?map_id=<uuid>
GET /api/map/character_activity?slug=<map-slug>
"""
@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.",
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: {
"Character activity data",
"application/json",
@character_activity_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 as a query parameter"
}
}}
]
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)
# Group activities by user_id and summarize
summarized_result =
if raw_activity == [] do
# Return empty list if there's no data
[]
else
raw_activity
|> Enum.group_by(fn activity ->
# Get user_id from the character
activity.character.user_id
end)
|> Enum.map(fn {_user_id, user_activities} ->
# Get the most active or followed character for this user
representative_activity =
user_activities
|> Enum.max_by(fn activity ->
activity.passages + activity.connections + activity.signatures
end)
# Sum up all activities for this user
total_passages = Enum.sum(Enum.map(user_activities, & &1.passages))
total_connections = Enum.sum(Enum.map(user_activities, & &1.connections))
total_signatures = Enum.sum(Enum.map(user_activities, & &1.signatures))
# Return summarized activity with the mapped character
%{
character: character_to_json(representative_activity.character),
passages: total_passages,
connections: total_connections,
signatures: total_signatures,
timestamp: representative_activity.timestamp
}
end)
end
json(conn, %{data: summarized_result})
else
{:error, msg} when is_binary(msg) ->
conn
|> put_status(:bad_request)
|> json(%{error: msg})
{:error, reason} ->
conn
|> put_status(:internal_server_error)
|> json(%{error: "Could not fetch character activity: #{inspect(reason)}"})
end
end
# If hours_str is present and valid, parse it. Otherwise return nil (no filter).
defp parse_hours_ago(nil), do: nil
@@ -830,18 +951,6 @@ defmodule WandererAppWeb.MapAPIController do
end
defp character_to_json(ch) do
Map.take(ch, [
:id,
:eve_id,
:name,
:corporation_id,
:corporation_name,
:corporation_ticker,
:alliance_id,
:alliance_name,
:alliance_ticker,
:inserted_at,
:updated_at
])
WandererAppWeb.MapEventHandler.map_ui_character_stat(ch)
end
end

View File

@@ -73,11 +73,11 @@ defmodule WandererAppWeb.CharactersTrackingLive do
@impl true
def handle_event("toggle_track_" <> character_id, _, socket) do
handle_event("toggle_track", %{"character-id" => character_id}, socket)
handle_event("toggle_track", %{"character_id" => character_id}, socket)
end
@impl true
def handle_event("toggle_track", %{"character-id" => character_id}, socket) do
def handle_event("toggle_track", %{"character_id" => character_id}, socket) do
selected_map = socket.assigns.selected_map
character_settings = socket.assigns.character_settings

View File

@@ -45,7 +45,7 @@
type="checkbox"
class="checkbox"
phx-click="toggle_track"
phx-value-character-id={character.id}
phx-value-character_id={character.id}
id={"character-track-#{character.id}"}
checked={character.tracked}
/>

View File

@@ -117,7 +117,7 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
def handle_ui_event(
"toggle_track",
%{"character-id" => character_eve_id},
%{"character_id" => character_eve_id},
%{
assigns: %{
map_id: map_id,
@@ -126,54 +126,22 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
}
} = socket
) do
# First, get all existing settings to preserve states
{:ok, all_settings} = WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id)
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)
# Save the followed character ID before making any changes
followed_character_id =
all_settings
|> Enum.find(& &1.followed)
|> case do
nil -> nil
setting -> setting.character_id
end
# Send the updated tracking data to the client
{:noreply,
socket
|> MapEventHandler.push_map_event(
"tracking_characters_data",
%{characters: tracking_data}
)}
# Find the character we're toggling
with {:ok, character} <-
WandererApp.Character.find_character_by_eve_id(current_user, character_eve_id),
{:ok, _updated_settings} <-
toggle_character_tracking(character, map_id, only_tracked_characters) do
# Get the map with ACLs
{:ok, map} = WandererApp.Api.Map.by_id(map_id)
map = Ash.load!(map, :acls)
# If there was a followed character before, check if it's still followed
# Only check if we're not toggling the followed character itself
if followed_character_id && followed_character_id != character.id do
# Get the current settings for the followed character
case WandererApp.MapCharacterSettingsRepo.get_by_map(map_id, followed_character_id) do
{:ok, current_settings} ->
# If it's not followed anymore, follow it again
if !current_settings.followed do
{:ok, _} = WandererApp.MapCharacterSettingsRepo.follow(current_settings)
end
_ ->
:ok
end
end
{:ok, tracking_data} = build_tracking_data(map_id, current_user)
{:noreply,
socket
|> MapEventHandler.push_map_event(
"tracking_characters_data",
%{characters: tracking_data}
)}
else
_ ->
{:noreply, socket}
{:error, reason} ->
Logger.error("Failed to toggle track: #{inspect(reason)}")
{:noreply, socket |> put_flash(:error, "Failed to toggle character tracking")}
end
end
@@ -183,48 +151,38 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
%{assigns: %{map_id: map_id, current_user: current_user}} = socket
) do
# Create tracking data for characters with access to the map
{:ok, tracking_data} = build_tracking_data(map_id, current_user)
case WandererApp.Character.TrackingUtils.build_tracking_data(map_id, current_user.id) do
{:ok, tracking_data} ->
{:noreply,
socket
|> MapEventHandler.push_map_event(
"tracking_characters_data",
%{characters: tracking_data}
)}
{:noreply,
socket
|> MapEventHandler.push_map_event(
"show_tracking",
%{}
)
|> MapEventHandler.push_map_event(
"tracking_characters_data",
%{characters: tracking_data}
)}
{:error, reason} ->
Logger.error("Failed to load tracking data: #{inspect(reason)}")
{:noreply, socket |> put_flash(:error, "Failed to load tracking data")}
end
end
def handle_ui_event(
"toggle_follow",
%{"character-id" => clicked_char_id},
%{"character_id" => clicked_char_id},
%{assigns: %{current_user: current_user, map_id: map_id}} = socket
) do
# Get all settings before the operation to see the followed state
{:ok, all_settings_before} = WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id)
followed_before = all_settings_before |> Enum.find(& &1.followed)
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)
# Check if the clicked character is already followed
is_already_followed =
followed_before && "#{followed_before.character_id}" == "#{clicked_char_id}"
{:noreply,
socket
|> MapEventHandler.push_map_event("tracking_characters_data", %{characters: tracking_data})}
# Use find_character_by_eve_id from WandererApp.Character
with {:ok, clicked_char} <-
WandererApp.Character.find_character_by_eve_id(current_user, clicked_char_id),
{:ok, _updated_settings} <-
toggle_character_follow(map_id, clicked_char, is_already_followed) do
# Build tracking data
{:ok, tracking_data} = build_tracking_data(map_id, current_user)
{:noreply,
socket
|> MapEventHandler.push_map_event("tracking_characters_data", %{characters: tracking_data})}
else
error ->
Logger.error("Failed to toggle follow: #{inspect(error)}")
{:noreply, socket}
{:error, reason} ->
Logger.error("Failed to toggle follow: #{inspect(reason)}")
{:noreply, socket |> put_flash(:error, "Failed to toggle character following")}
end
end
@@ -251,133 +209,6 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|> Map.put_new(:ship, WandererApp.Character.get_ship(character))
|> Map.put_new(:location, get_location(character))
def add_characters([], _map_id, _track_character), do: :ok
def add_characters([character | characters], map_id, track_character) do
map_id
|> WandererApp.Map.Server.add_character(character, track_character)
add_characters(characters, map_id, track_character)
end
def remove_characters([], _map_id), do: :ok
def remove_characters([character | characters], map_id) do
map_id
|> WandererApp.Map.Server.remove_character(character.id)
remove_characters(characters, map_id)
end
def untrack_characters(characters, map_id) do
characters
|> Enum.each(fn character ->
WandererAppWeb.Presence.untrack(self(), map_id, character.id)
WandererApp.Cache.put(
"#{inspect(self())}_map_#{map_id}:character_#{character.id}:tracked",
false
)
:ok =
Phoenix.PubSub.unsubscribe(
WandererApp.PubSub,
"character:#{character.eve_id}"
)
end)
end
def track_characters(_, _, false), do: :ok
def track_characters([], _map_id, _is_track_character?), do: :ok
def track_characters(
[character | characters],
map_id,
true
) do
track_character(character, map_id)
track_characters(characters, map_id, true)
end
def track_character(
%{
id: character_id,
eve_id: eve_id,
corporation_id: corporation_id,
alliance_id: alliance_id
},
map_id
) do
WandererAppWeb.Presence.track(self(), map_id, character_id, %{})
case WandererApp.Cache.lookup!(
"#{inspect(self())}_map_#{map_id}:character_#{character_id}:tracked",
false
) do
true ->
:ok
_ ->
:ok =
Phoenix.PubSub.subscribe(
WandererApp.PubSub,
"character:#{eve_id}"
)
:ok =
WandererApp.Cache.put(
"#{inspect(self())}_map_#{map_id}:character_#{character_id}:tracked",
true
)
end
case WandererApp.Cache.lookup(
"#{inspect(self())}_map_#{map_id}:corporation_#{corporation_id}:tracked",
false
) do
{:ok, true} ->
:ok
{:ok, false} ->
:ok =
Phoenix.PubSub.subscribe(
WandererApp.PubSub,
"corporation:#{corporation_id}"
)
:ok =
WandererApp.Cache.put(
"#{inspect(self())}_map_#{map_id}:corporation_#{corporation_id}:tracked",
true
)
end
case WandererApp.Cache.lookup(
"#{inspect(self())}_map_#{map_id}:alliance_#{alliance_id}:tracked",
false
) do
{:ok, true} ->
:ok
{:ok, false} ->
:ok =
Phoenix.PubSub.subscribe(
WandererApp.PubSub,
"alliance:#{alliance_id}"
)
:ok =
WandererApp.Cache.put(
"#{inspect(self())}_map_#{map_id}:alliance_#{alliance_id}:tracked",
true
)
end
:ok = WandererApp.Character.TrackerManager.start_tracking(character_id)
end
defp get_location(character),
do: %{solar_system_id: character.solar_system_id, structure_id: character.structure_id}
@@ -461,8 +292,8 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
end
defp handle_tracking_event({:track_characters, map_characters, track_character}, socket, map_id) do
:ok = track_characters(map_characters, map_id, track_character)
:ok = 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
@@ -555,165 +386,4 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
:character_fleet -> handle_fleet_result(socket, result)
end
end
defp toggle_character_follow(map_id, clicked_char, is_already_followed) do
with {:ok, clicked_char_settings} <-
WandererApp.MapCharacterSettingsRepo.get_by_map(map_id, clicked_char.id) do
if is_already_followed do
# If already followed, just unfollow without affecting other characters
WandererApp.MapCharacterSettingsRepo.unfollow(clicked_char_settings)
else
# Normal follow toggle
update_follow_status(map_id, clicked_char, clicked_char_settings)
end
else
{:error, :not_found} ->
# Character not found in settings, create new settings
update_follow_status(map_id, clicked_char, nil)
end
end
defp update_follow_status(map_id, clicked_char, nil) do
# Create new settings with tracked=true and followed=true
# If we're following this character, unfollow all others first
:ok = maybe_unfollow_others(map_id, clicked_char.id, true)
result =
WandererApp.MapCharacterSettingsRepo.create(%{
character_id: clicked_char.id,
map_id: map_id,
tracked: true,
followed: true
})
result
end
defp update_follow_status(map_id, clicked_char, clicked_char_settings) do
# Toggle the followed state
followed = !clicked_char_settings.followed
# Only unfollow other characters if we're explicitly following this character
# This prevents unfollowing other characters when just tracking a character
if followed do
# We're following this character, so unfollow all others
:ok = maybe_unfollow_others(map_id, clicked_char.id, followed)
end
# If we're following, make sure the character is also tracked
:ok = maybe_track_character(clicked_char_settings, followed)
# Update the follow status
{:ok, settings} = update_follow(clicked_char_settings, followed)
{:ok, settings}
end
defp maybe_unfollow_others(_map_id, _char_id, false), do: :ok
defp maybe_unfollow_others(map_id, char_id, true) do
# This function should only be called when explicitly following a character,
# not when tracking a character. It unfollows all other characters when
# setting a character as followed.
{:ok, all_settings} = WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id)
# Unfollow other characters
all_settings
|> Enum.filter(&(&1.character_id != char_id && &1.followed))
|> Enum.each(fn setting ->
WandererApp.MapCharacterSettingsRepo.unfollow(setting)
end)
:ok
end
defp maybe_track_character(_settings, false), do: :ok
defp maybe_track_character(settings, true) do
if not settings.tracked do
{:ok, _} = WandererApp.MapCharacterSettingsRepo.track(settings)
end
:ok
end
defp update_follow(settings, true), do: WandererApp.MapCharacterSettingsRepo.follow(settings)
defp update_follow(settings, false), do: WandererApp.MapCharacterSettingsRepo.unfollow(settings)
defp build_tracking_data(map_id, current_user) do
with {:ok, map} <- WandererApp.Api.Map.by_id(map_id),
map <- Ash.load!(map, :acls),
{:ok, character_settings} <- WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id),
{:ok, %{characters: characters_with_access}} <-
WandererApp.Maps.load_characters(map, character_settings, current_user.id) do
tracking_data =
Enum.map(characters_with_access, fn char ->
setting = Enum.find(character_settings, &(&1.character_id == char.id))
tracked = if setting, do: setting.tracked, else: false
# Important: Preserve the followed state
followed = if setting, do: setting.followed, else: false
%{
character: char |> MapEventHandler.map_ui_character_stat(),
tracked: tracked,
followed: followed
}
end)
{:ok, tracking_data}
end
end
# Helper function to toggle character tracking
defp toggle_character_tracking(character, map_id, only_tracked_characters) do
case WandererApp.MapCharacterSettingsRepo.get_by_map(map_id, character.id) do
{:ok, existing_settings} ->
if existing_settings.tracked do
# Untrack the character
{:ok, updated_settings} =
WandererApp.MapCharacterSettingsRepo.untrack(existing_settings)
:ok = untrack_characters([character], map_id)
:ok = remove_characters([character], map_id)
if only_tracked_characters do
Process.send_after(self(), :not_all_characters_tracked, 10)
end
# If the character was followed, we need to unfollow it too
# But we should NOT unfollow other characters
if existing_settings.followed do
{:ok, final_settings} =
WandererApp.MapCharacterSettingsRepo.unfollow(updated_settings)
{:ok, final_settings}
else
{:ok, updated_settings}
end
else
# Track the character
{:ok, updated_settings} =
WandererApp.MapCharacterSettingsRepo.track(existing_settings)
:ok = track_characters([character], map_id, true)
:ok = add_characters([character], map_id, true)
Process.send_after(self(), %{event: :refresh_user_characters}, 10)
{:ok, updated_settings}
end
{:error, :not_found} ->
# Create new settings
result =
WandererApp.MapCharacterSettingsRepo.create(%{
character_id: character.id,
map_id: map_id,
tracked: true,
followed: false
})
result
end
end
end

View File

@@ -56,14 +56,14 @@ defmodule WandererAppWeb.MapCoreEventHandler do
case track_character do
false ->
:ok = MapCharactersEventHandler.untrack_characters(map_characters, map_id)
:ok = MapCharactersEventHandler.remove_characters(map_characters, map_id)
:ok = WandererApp.Character.TrackingUtils.untrack_characters(map_characters, map_id, self())
:ok = WandererApp.Character.TrackingUtils.remove_characters(map_characters, map_id)
_ ->
:ok = MapCharactersEventHandler.track_characters(map_characters, map_id, true)
:ok = WandererApp.Character.TrackingUtils.track_characters(map_characters, map_id, true, self())
:ok =
MapCharactersEventHandler.add_characters(map_characters, map_id, track_character)
WandererApp.Character.TrackingUtils.add_characters(map_characters, map_id, track_character)
end
socket

View File

@@ -29,7 +29,6 @@ defmodule WandererAppWeb.MapEventHandler do
@map_characters_ui_events [
"toggle_track",
"toggle_follow",
"hide_tracking",
"show_tracking",
"getCharacterInfo"
]
@@ -304,13 +303,13 @@ defmodule WandererAppWeb.MapEventHandler do
} = socket,
type,
body
),
do:
socket
|> Phoenix.LiveView.Utils.push_event("map_event", %{
type: type,
body: body
})
) do
socket
|> Phoenix.LiveView.Utils.push_event("map_event", %{
type: type,
body: body
})
end
def push_map_event(socket, _type, _body), do: socket

View File

@@ -210,6 +210,7 @@ defmodule WandererAppWeb.Router do
get "/system", MapAPIController, :show_system
get "/characters", MapAPIController, :tracked_characters_with_info
get "/structure-timers", MapAPIController, :show_structure_timers
get "/character-activity", MapAPIController, :character_activity
get "/acls", MapAccessListAPIController, :index
post "/acls", MapAccessListAPIController, :create
end

View File

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

View File

@@ -19,6 +19,7 @@ With these APIs, you can:
- Retrieve map data, including systems and their properties
- Access system static information
- Track character locations and activities
- View character activity metrics (passages, connections, signatures)
- Monitor kill activity in systems
- Manage Access Control Lists (ACLs) for permissions
- Add, update, and remove ACL members
@@ -245,17 +246,12 @@ curl -H "Authorization: Bearer <REDACTED_TOKEN>" \
{
"id": "<REDACTED_ID>",
"character": {
"id": "<REDACTED_ID>",
"eve_id": "<REDACTED_EVE_ID>",
"name": "<REDACTED_NAME>",
"inserted_at": "2025-01-01T05:24:18.461721Z",
"updated_at": "2025-01-03T07:45:52.294052Z",
"alliance_id": "<REDACTED>",
"alliance_name": "<REDACTED>",
"alliance_ticker": "<REDACTED>",
"corporation_id": "<REDACTED>",
"corporation_name": "<REDACTED>",
"corporation_ticker": "<REDACTED>",
"eve_id": "<REDACTED>"
"corporation_id": "<REDACTED_CORP_ID>",
"corporation_ticker": "<REDACTED_CORP_TICKER>",
"alliance_id": "<REDACTED_ALLIANCE_ID>",
"alliance_ticker": "<REDACTED_ALLIANCE_TICKER>"
},
"tracked": true,
"map_id": "<REDACTED_ID>"
@@ -337,7 +333,51 @@ curl -H "Authorization: Bearer <REDACTED_TOKEN>" \
}
```
### 6. Structure Timers
### 6. Character Activity
```bash
GET /api/map/character-activity?map_id=<UUID>
GET /api/map/character-activity?slug=<map-slug>
```
- **Description:** Retrieves character activity data for a map, including passages, connections, and signatures.
- **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/character-activity?slug=some-slug"
```
#### Example Response
```json
{
"data": [
{
"character": {
"eve_id": "<REDACTED_EVE_ID>",
"name": "<REDACTED_NAME>",
"corporation_id": "<REDACTED_CORP_ID>",
"corporation_ticker": "<REDACTED_CORP_TICKER>",
"alliance_id": "<REDACTED_ALLIANCE_ID>",
"alliance_ticker": "<REDACTED_ALLIANCE_TICKER>"
},
"passages": 42,
"connections": 15,
"signatures": 23,
"timestamp": "2025-03-01T14:30:22Z"
},
...
]
}
```
### 7. Structure Timers
```bash
GET /api/map/structure-timers?map_id=<UUID>
@@ -377,18 +417,20 @@ curl -H "Authorization: Bearer <REDACTED_TOKEN>" \
{
"data": [
{
"id": "b374d9e6-47a7-4e20-85ad-d608809827b5",
"name": "Some Character",
"eve_id": "2122825111",
"corporation_name": "School of Applied Knowledge",
"alliance_name": null
"name": "Some Character",
"corporation_id": "1000044",
"corporation_ticker": "SAOK",
"alliance_id": null,
"alliance_ticker": null
},
{
"id": "6963bee6-eaa1-40e2-8200-4bc2fcbd7350",
"name": "Other Character",
"eve_id": "2122019111",
"corporation_name": "Some Corporation",
"alliance_name": null
"name": "Other Character",
"corporation_id": "98140648",
"corporation_ticker": "CORP",
"alliance_id": "99013806",
"alliance_ticker": "ALLY"
},
...
]
@@ -828,6 +870,7 @@ This guide provides a comprehensive overview of Wanderer's API capabilities. Wit
3. **Access system information** with or without authentication
4. **Manage Access Control Lists (ACLs)** for permissions
5. **Add, update, and remove ACL members** with different roles
6. **View character activity metrics** including passages, connections, and signatures
For the most up-to-date and interactive documentation, we recommend using the Swagger UI at `/swaggerui` which allows you to explore and test endpoints directly from your browser.

View File

@@ -0,0 +1,346 @@
%{
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](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.
## Prerequisites
Before setting up Wanderer Notifier, ensure you have the following:
- A Discord server where you have administrator permissions
- Docker and Docker Compose installed on your system
- Basic knowledge of terminal/command line operations
- Your Wanderer map URL and API token
- A Discord bot token (see our [guide on creating a Discord bot](https://gist.github.com/guarzo/a4d238b932b6a168ad1c5f0375c4a561))
## How to Get Started
There are two ways to install Wanderer Notifier: a **Quick Install** option using a one-liner, or a **Manual Setup** for those who prefer step-by-step control.
### Quick Install Option
For a streamlined installation that creates the necessary directory and files automatically, run:
```bash
curl -fsSL https://gist.githubusercontent.com/guarzo/3f05f3c57005c3cf3585869212caecfe/raw/wanderer-notifier-setup.sh | bash
```
Once the script finishes, update the `wanderer-notifier/.env` file with your configuration values, then proceed to [Step 4](#4-run-it).
### Manual Setup
If you'd rather set up everything manually, follow these steps:
#### 1. Download the Docker Image
Pull the latest Docker image:
```bash
docker pull guarzo/wanderer-notifier:latest
```
#### 2. Configure Your Environment
Create a `.env` file in your working directory with the following content. Replace the placeholder values with your actual credentials:
```dotenv
# Required Configuration
DISCORD_BOT_TOKEN=your_discord_bot_token
DISCORD_CHANNEL_ID=your_discord_channel_id
MAP_URL_WITH_NAME="https://wanderer.ltd/<yourmap>"
MAP_TOKEN=your_map_api_token
# Map Subscription Configuration (for enhanced features)
# Note: Premium features are enabled with your map subscription
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
```
> **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.
#### 3. Create the Docker Compose Configuration
Create a file named `docker-compose.yml` with the following content:
```yaml
services:
wanderer_notifier:
image: guarzo/wanderer-notifier:latest
container_name: wanderer_notifier
restart: unless-stopped
environment:
- DISCORD_BOT_TOKEN=${DISCORD_BOT_TOKEN}
- DISCORD_CHANNEL_ID=${DISCORD_CHANNEL_ID}
- MAP_URL_WITH_NAME=${MAP_URL_WITH_NAME}
- MAP_TOKEN=${MAP_TOKEN}
- LICENSE_KEY=${LICENSE_KEY}
ports:
- 4000:4000
volumes:
- wanderer_data:/app/data
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:${PORT:-4000}/health"]
interval: 30s
timeout: 3s
retries: 3
start_period: 10s
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
volumes:
wanderer_data:
```
> **Note:** If you used the quick install option, these files have already been created for you.
#### 4. Run It
Start the service with Docker Compose:
```bash
docker-compose up -d
```
Your notifier is now up and running, delivering alerts to your Discord channel automatically!
---
## Notification Types
Wanderer Notifier supports three main notification types, each tailored based on your map subscription status.
### Kill Notifications
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
- Ship type details
- ISK value of the kill
- Corporation logos
- A clickable link on the final blow character to zKillboard
![Premium Kill Notification Example](/images/news/03-18-bots/paid-kill.png)
- **With Free Map:**
Displays a basic text notification containing:
- Victim name
- Ship type lost
- System name
![Free Kill Notification Example](/images/news/03-18-bots/free-kill.png)
### Character Tracking Notifications
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
- Formatted timestamp
![Premium Character Notification Example](/images/news/03-18-bots/paid-character.png)
- **With Free Map:**
Receives a simple text notification that includes:
- Character name
- Corporation name (if available)
![Free Character Notification Example](/images/news/03-18-bots/free-character.png)
### System Notifications
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
- Security status
- Recent kills in the system
- Links to zKillboard and Dotlan
![Premium System Notification Example](/images/news/03-18-bots/paid-system.png)
- **With Free Map:**
Provides a basic text notification including:
- Original system name (for wormholes)
- System name (for k-space)
![Free System Notification Example](/images/news/03-18-bots/free-system.png)
---
## Map Subscription Features & Limitations
Wanderer Notifier offers enhanced functionality with a premium map subscription while still providing robust features for free maps.
### Free Version Features
- **Core Notifications:** Basic text notifications for systems and characters.
- **Web Dashboard:** View system status and subscription information.
- **Unlimited Tracking:** Track an unlimited number of systems and characters.
- **Notification History:** 24-hour retention of notification history.
### Premium Map Subscription Enhancements
- **Rich Notifications:** Enhanced embeds with images, links, and detailed data.
- **Interactive Elements:** Clickable links to zKillboard profiles and additional resources.
- **Enhanced System Information:** Comprehensive data including region details, security status, and wormhole statics.
- **Recent Activity:** Access to recent kill data in newly mapped systems.
- **Upcoming Features:** Daily reporting on tracked character activity, structure notifications, ACL notifications, and Slack notifications.
### How to Subscribe
To unlock the enhanced features of Wanderer Notifier:
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](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 |
---
## Web Dashboard
Wanderer Notifier includes a web dashboard that provides real-time insights into your notification system:
- **Access:** Visit `http://localhost:4000` to view the dashboard.
- **System Status:** Monitor system details, subscription information, and notification statistics.
- **Resource Monitoring:** Keep an eye on resource usage and feature availability.
- **Notification Testing:** Test notifications directly from the dashboard.
Premium map subscribers also gain access to detailed statistics and advanced visualization tools.
![Dashboard](/images/news/03-18-bots/dashboard.png)
---
## Configuration Options
Customize your notification experience with several configuration options available through environment variables.
### 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.
To disable a notification type, set the corresponding variable to `false` or `0` in your `.env` file:
```dotenv
# Example: Disable kill notifications while keeping other notifications enabled
ENABLE_KILL_NOTIFICATIONS=false
```
---
## Troubleshooting
If you encounter issues with Wanderer Notifier, here are solutions to common problems:
### No Notifications Appearing
1. **Check Bot Permissions:** Ensure your bot has the "Send Messages" and "Embed Links" permissions in the Discord channel.
2. **Verify Channel ID:** Double-check your DISCORD_CHANNEL_ID in the .env file.
3. **Check Container Logs:** Run `docker logs wanderer_notifier` to see if there are any error messages.
4. **Test API Connection:** Visit `http://localhost:4000/health` to verify the service is running.
### Connection Issues
1. **Network Configuration:** Ensure port 4000 is not blocked by your firewall.
2. **Docker Status:** Run `docker ps` to verify the container is running.
3. **Restart Service:** Try `docker-compose restart` to refresh the connection.
### Subscription Not Recognized
1. **Check Map Token:** Ensure your MAP_TOKEN is correct and associated with your map.
2. **Verify LICENSE_KEY:** Make sure you've entered the correct map subscription key in your .env file.
3. **Verify Status:** Check the dashboard at `http://localhost:4000` to see subscription status.
4. **Restart After Subscribing:** If you've recently subscribed, restart the notifier with `docker-compose restart`.
For additional support, join our [Discord community](https://discord.gg/wanderer) or email support@wanderer.ltd.
## Updating Wanderer Notifier
To ensure you have the latest features and security updates, periodically update your Wanderer Notifier installation:
### Automatic Updates
The Docker image is configured to check for updates daily. To manually trigger an update:
```bash
# Navigate to your wanderer-notifier directory
cd wanderer-notifier
# Pull the latest image
docker-compose pull
# Restart the container with the new image
docker-compose up -d
```
### Update Notifications
When significant updates are available, you'll receive a notification in your Discord channel. These updates may include:
- New notification types
- Enhanced visualization features
- Security improvements
- Bug fixes
### Preserving Your Configuration
Updates preserve your existing configuration and data. Your `.env` file and tracked entities will remain intact through the update process.
## Conclusion
Wanderer Notifier is engineered to keep you informed of crucial in-game events effortlessly. The free version provides unlimited tracking with basic notifications, while premium map subscribers receive rich, detailed alerts with enhanced features.
By following either the quick install or manual setup process, you'll have the notifier running in no time—delivering real-time alerts directly to your Discord channel so you can focus on what matters most in your gameplay.
For further support or questions, please contact the Wanderer Team.
Stay vigilant and enjoy your real-time alerts!

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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