Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
999a702291 | ||
|
|
020b9bb2c2 | ||
|
|
7713caab51 | ||
|
|
97a777d729 | ||
|
|
8241d1f08c | ||
|
|
2ac85bbfff | ||
|
|
3f68ae2235 | ||
|
|
0f7b6f75df | ||
|
|
b048e8f5ca | ||
|
|
9783dc45ff | ||
|
|
badbefbade | ||
|
|
b6a265cfad | ||
|
|
9b5ea2f84b | ||
|
|
d8acfa5c05 | ||
|
|
2a5b6924eb | ||
|
|
3b9aee1eb9 | ||
|
|
83801c9063 | ||
|
|
0f34350c58 | ||
|
|
1c4c0f0715 | ||
|
|
3825fc831a | ||
|
|
654670cbc8 | ||
|
|
947570072c | ||
|
|
01b6b45380 | ||
|
|
b9dc1f8357 | ||
|
|
b4bd810c9d | ||
|
|
490b037920 | ||
|
|
cdff5458bc | ||
|
|
09314a09e9 | ||
|
|
49ea8edb27 |
@@ -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
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
79
CHANGELOG.md
@@ -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)
|
||||
|
||||
|
||||
|
||||
7
Makefile
@@ -1,4 +1,4 @@
|
||||
.PHONY: deploy install cleanup start yarn migrate format test coverage versions
|
||||
.PHONY: deploy install cleanup start yarn migrate format test coverage versions standalone-tests
|
||||
|
||||
ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
|
||||
SHELL := /bin/bash
|
||||
@@ -35,6 +35,11 @@ test t:
|
||||
coverage cover co:
|
||||
mix test --cover
|
||||
|
||||
unit-tests ut:
|
||||
@echo "Running unit tests..."
|
||||
@find test/unit -name "*.exs" -exec elixir {} \;
|
||||
@echo "All unit tests completed."
|
||||
|
||||
versions v:
|
||||
@echo "Tool Versions"
|
||||
@cat .tool-versions
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { Column } from 'primereact/column';
|
||||
import {
|
||||
DataTable,
|
||||
DataTableRowClickEvent,
|
||||
@@ -6,13 +7,9 @@ import {
|
||||
DataTableStateEvent,
|
||||
SortOrder,
|
||||
} from 'primereact/datatable';
|
||||
import { Column } from 'primereact/column';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import useLocalStorageState from 'use-local-storage-state';
|
||||
|
||||
import { ExtendedSystemSignature, SignatureGroup, SignatureKind, SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { SignatureSettings } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings';
|
||||
import { WdTooltip, WdTooltipHandlers, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { SignatureView } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SignatureView';
|
||||
import {
|
||||
COMPACT_MAX_WIDTH,
|
||||
@@ -24,6 +21,9 @@ import {
|
||||
SIGNATURE_WINDOW_ID,
|
||||
SignatureSettingsType,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants';
|
||||
import { SignatureSettings } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings';
|
||||
import { TooltipPosition, WdTooltip, WdTooltipHandlers, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { ExtendedSystemSignature, SignatureGroup, SignatureKind, SystemSignature } from '@/hooks/Mapper/types';
|
||||
|
||||
import {
|
||||
renderAddedTimeLeft,
|
||||
@@ -32,10 +32,10 @@ import {
|
||||
renderInfoColumn,
|
||||
renderUpdatedTimeLeft,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders';
|
||||
import { useSystemSignaturesData } from '../hooks/useSystemSignaturesData';
|
||||
import { getSignatureRowClass } from '../helpers/rowStyles';
|
||||
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
|
||||
import { useClipboard, useHotkey } from '@/hooks/Mapper/hooks';
|
||||
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
|
||||
import { getSignatureRowClass } from '../helpers/rowStyles';
|
||||
import { useSystemSignaturesData } from '../hooks/useSystemSignaturesData';
|
||||
|
||||
const renderColIcon = (sig: SystemSignature) => renderIcon(sig);
|
||||
|
||||
@@ -348,6 +348,7 @@ export const SystemSignaturesContent = ({
|
||||
<WdTooltip
|
||||
className="bg-stone-900/95 text-slate-50"
|
||||
ref={tooltipRef}
|
||||
position={TooltipPosition.top}
|
||||
content={
|
||||
hoveredSignature ? (
|
||||
<SignatureView signature={hoveredSignature} showCharacterPortrait={showCharacterPortrait} />
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
},
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
18175
assets/static/images/news/03-18-bots/bot.svg
Executable file
|
After Width: | Height: | Size: 524 KiB |
BIN
assets/static/images/news/03-18-bots/dashboard.png
Executable file
|
After Width: | Height: | Size: 119 KiB |
BIN
assets/static/images/news/03-18-bots/free-character.png
Executable file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
assets/static/images/news/03-18-bots/free-kill.png
Executable file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
assets/static/images/news/03-18-bots/free-system.png
Executable file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
assets/static/images/news/03-18-bots/paid-character.png
Executable file
|
After Width: | Height: | Size: 23 KiB |
BIN
assets/static/images/news/03-18-bots/paid-kill.png
Executable file
|
After Width: | Height: | Size: 33 KiB |
BIN
assets/static/images/news/03-18-bots/paid-system.png
Executable file
|
After Width: | Height: | Size: 18 KiB |
@@ -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} ->
|
||||
|
||||
374
lib/wanderer_app/character/tracking_utils.ex
Normal 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
|
||||
@@ -4,7 +4,12 @@ defmodule WandererApp.Env do
|
||||
|
||||
@app :wanderer_app
|
||||
|
||||
@decorate cacheable(
|
||||
cache: WandererApp.Cache,
|
||||
key: "vsn_version"
|
||||
)
|
||||
def vsn(), do: Application.spec(@app)[:vsn]
|
||||
|
||||
def git_sha(), do: get_key(:git_sha, "<GIT_SHA>")
|
||||
def base_url, do: get_key(:web_app_url, "<BASE_URL>")
|
||||
def custom_route_base_url, do: get_key(:custom_route_base_url, "<CUSTOM_ROUTE_BASE_URL>")
|
||||
|
||||
@@ -9,6 +9,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
@routes_ttl :timer.minutes(15)
|
||||
|
||||
@base_url "https://esi.evetech.net/latest"
|
||||
@wanderrer_user_agent "(wanderer-industries@proton.me; +https://github.com/wanderer-industries/wanderer)"
|
||||
|
||||
@get_link_pairs_advanced_params [
|
||||
:include_mass_crit,
|
||||
@@ -289,14 +290,13 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@decorate cacheable(
|
||||
cache: Cache,
|
||||
key: "killmail-#{killmail_id}-#{killmail_hash}",
|
||||
opts: [ttl: @ttl]
|
||||
)
|
||||
cache: Cache,
|
||||
key: "killmail-#{killmail_id}-#{killmail_hash}",
|
||||
opts: [ttl: @ttl]
|
||||
)
|
||||
def get_killmail(killmail_id, killmail_hash, opts \\ []) do
|
||||
get("/killmails/#{killmail_id}/#{killmail_hash}/", _with_cache_opts(opts))
|
||||
get("/killmails/#{killmail_id}/#{killmail_hash}/", opts)
|
||||
end
|
||||
|
||||
@decorate cacheable(
|
||||
@@ -319,7 +319,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
def get_character_info(eve_id, opts \\ []) do
|
||||
case get(
|
||||
"/characters/#{eve_id}/",
|
||||
opts |> _with_cache_opts()
|
||||
opts
|
||||
) do
|
||||
{:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
|
||||
{:error, error} -> {:error, error}
|
||||
@@ -370,10 +370,10 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
end
|
||||
|
||||
@decorate cacheable(
|
||||
cache: Cache,
|
||||
key: "search-#{character_eve_id}-#{categories_val}-#{search_val |> Slug.slugify()}",
|
||||
opts: [ttl: @ttl]
|
||||
)
|
||||
cache: Cache,
|
||||
key: "search-#{character_eve_id}-#{categories_val}-#{search_val |> Slug.slugify()}",
|
||||
opts: [ttl: @ttl]
|
||||
)
|
||||
defp _search(character_eve_id, search_val, categories_val, merged_opts) do
|
||||
_get_character_auth_data(character_eve_id, "search", merged_opts)
|
||||
end
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
2
mix.exs
@@ -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
|
||||
[
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
346
priv/posts/2025/03-18-bots.md
Normal 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
|
||||
|
||||

|
||||
|
||||
- **With Free Map:**
|
||||
Displays a basic text notification containing:
|
||||
- Victim name
|
||||
- Ship type lost
|
||||
- System name
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
|
||||

|
||||
|
||||
- **With Free Map:**
|
||||
Receives a simple text notification that includes:
|
||||
- Character name
|
||||
- Corporation name (if available)
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
|
||||

|
||||
|
||||
- **With Free Map:**
|
||||
Provides a basic text notification including:
|
||||
- Original system name (for wormholes)
|
||||
- System name (for k-space)
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 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!
|
||||
@@ -1,13 +0,0 @@
|
||||
# Wanderer API Testing Tool Configuration
|
||||
# Generated on Thu Mar 6 14:52:00 UTC 2025
|
||||
|
||||
# Base configuration
|
||||
HOST="http://localhost:4444"
|
||||
MAP_SLUG="flygd"
|
||||
MAP_API_KEY="589016d9-c9ac-48ef-ae74-7a55483b3cc2"
|
||||
ACL_API_KEY=""
|
||||
|
||||
# Selected IDs
|
||||
SELECTED_ACL_ID=""
|
||||
SELECTED_SYSTEM_ID=""
|
||||
CHARACTER_EVE_ID=""
|
||||
@@ -1,13 +0,0 @@
|
||||
# Wanderer API Testing Tool Configuration
|
||||
# Example configuration file - Copy to .api_test_config and modify as needed
|
||||
|
||||
# Base configuration
|
||||
HOST="http://localhost:4000"
|
||||
MAP_SLUG="flygd"
|
||||
MAP_API_KEY="589016d9-c9ac-48ef-ae74-7a55483b3cc2"
|
||||
ACL_API_KEY="acl-api-key-here"
|
||||
|
||||
# Selected IDs
|
||||
SELECTED_ACL_ID="123"
|
||||
SELECTED_SYSTEM_ID="31002019"
|
||||
CHARACTER_EVE_ID="456"
|
||||
@@ -1,13 +0,0 @@
|
||||
# Wanderer API Testing Tool Configuration
|
||||
# Generated on Thu Mar 6 18:44:20 UTC 2025
|
||||
|
||||
# Base configuration
|
||||
HOST="http://localhost:4444"
|
||||
MAP_SLUG="flygd"
|
||||
MAP_API_KEY="589016d9-c9ac-48ef-ae74-7a55483b3cc2"
|
||||
ACL_API_KEY="116bd70e-2bbf-4a99-97ed-1869c09ab5bf"
|
||||
|
||||
# Selected IDs
|
||||
SELECTED_ACL_ID="9c91d283-f49f-4f45-a21d-9bf53ce9d1fd"
|
||||
SELECTED_SYSTEM_ID="30002768"
|
||||
CHARACTER_EVE_ID="2115754172"
|
||||
@@ -1,894 +0,0 @@
|
||||
#!/bin/bash
|
||||
#==============================================================================
|
||||
# Wanderer API Automated Testing Tool
|
||||
#
|
||||
# This script tests various endpoints of the Wanderer API.
|
||||
#
|
||||
# Features:
|
||||
# - Uses strict mode (set -euo pipefail) for robust error handling.
|
||||
# - Contains a DEBUG mode for extra logging (set DEBUG=1 to enable).
|
||||
# - Validates configuration including a reachability test for the HOST.
|
||||
# - Outputs a summary in plain text and optionally as JSON.
|
||||
# - Exits with a nonzero code if any test fails.
|
||||
#
|
||||
# Usage:
|
||||
# ./auto_test_api.sh
|
||||
#
|
||||
#==============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
|
||||
# Set DEBUG=1 to enable extra logging
|
||||
DEBUG=0
|
||||
# Set VERBOSE=1 to print raw JSON responses for every test (default 0)
|
||||
VERBOSE=0
|
||||
# Set VERBOSE_SUMMARY=1 to output a JSON summary at the end (default 0)
|
||||
VERBOSE_SUMMARY=0
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration file and default configuration
|
||||
CONFIG_FILE=".auto_api_test_config"
|
||||
HOST="http://localhost:4444" # Default host
|
||||
MAP_SLUG=""
|
||||
MAP_API_KEY=""
|
||||
ACL_API_KEY=""
|
||||
SELECTED_ACL_ID=""
|
||||
SELECTED_SYSTEM_ID=""
|
||||
CHARACTER_EVE_ID=""
|
||||
TEST_RESULTS=()
|
||||
FAILED_TESTS=()
|
||||
|
||||
# Global variables for last API response
|
||||
LAST_JSON_RESPONSE=""
|
||||
LAST_HTTP_CODE=""
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# Helper Functions
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
debug() {
|
||||
if [ "$DEBUG" -eq 1 ]; then
|
||||
echo -e "${YELLOW}[DEBUG] $*${NC}" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
print_header() {
|
||||
echo -e "\n${BLUE}=== $1 ===${NC}\n"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}$1${NC}"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}$1${NC}" >&2
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}$1${NC}"
|
||||
}
|
||||
|
||||
# Check if the host is reachable; accept any HTTP status code 200-399.
|
||||
check_host_reachable() {
|
||||
debug "Checking if host $HOST is reachable..."
|
||||
local status
|
||||
status=$(curl -s -o /dev/null -w "%{http_code}" "$HOST")
|
||||
debug "HTTP status code for host: $status"
|
||||
if [[ "$status" -ge 200 && "$status" -lt 400 ]]; then
|
||||
print_success "Host $HOST is reachable."
|
||||
else
|
||||
print_error "Host $HOST is not reachable (HTTP code: $status). Please check the host URL."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Load configuration from file
|
||||
load_config() {
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
print_success "Loading configuration from $CONFIG_FILE"
|
||||
source "$CONFIG_FILE"
|
||||
return 0
|
||||
else
|
||||
print_warning "No configuration file found. Using default values."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Save configuration to file
|
||||
save_config() {
|
||||
print_success "Saving configuration to $CONFIG_FILE"
|
||||
cat > "$CONFIG_FILE" << EOF
|
||||
# Wanderer API Testing Tool Configuration
|
||||
# Generated on $(date)
|
||||
|
||||
# Base configuration
|
||||
HOST="$HOST"
|
||||
MAP_SLUG="$MAP_SLUG"
|
||||
MAP_API_KEY="$MAP_API_KEY"
|
||||
ACL_API_KEY="$ACL_API_KEY"
|
||||
|
||||
# Selected IDs
|
||||
SELECTED_ACL_ID="$SELECTED_ACL_ID"
|
||||
SELECTED_SYSTEM_ID="$SELECTED_SYSTEM_ID"
|
||||
CHARACTER_EVE_ID="$CHARACTER_EVE_ID"
|
||||
EOF
|
||||
chmod 600 "$CONFIG_FILE"
|
||||
print_success "Configuration saved successfully."
|
||||
}
|
||||
|
||||
# Make an API call using curl and capture response and HTTP code
|
||||
call_api() {
|
||||
local method=$1
|
||||
local endpoint=$2
|
||||
local api_key=$3
|
||||
local data=${4:-""}
|
||||
|
||||
local curl_cmd=(curl -s -w "\n%{http_code}" -X "$method" -H "Content-Type: application/json")
|
||||
if [ -n "$api_key" ]; then
|
||||
curl_cmd+=(-H "Authorization: Bearer $api_key")
|
||||
fi
|
||||
if [ -n "$data" ]; then
|
||||
curl_cmd+=(-d "$data")
|
||||
fi
|
||||
curl_cmd+=("$HOST$endpoint")
|
||||
|
||||
# Print debug command (mask API key)
|
||||
local debug_cmd
|
||||
debug_cmd=$(printf "%q " "${curl_cmd[@]}")
|
||||
debug_cmd=$(echo "$debug_cmd" | sed "s/$api_key/API_KEY_HIDDEN/g")
|
||||
print_warning "Executing: $debug_cmd"
|
||||
|
||||
local output
|
||||
output=$("${curl_cmd[@]}")
|
||||
LAST_HTTP_CODE=$(echo "$output" | tail -n1)
|
||||
local response
|
||||
response=$(echo "$output" | sed '$d')
|
||||
echo "$response"
|
||||
}
|
||||
|
||||
# Check that required variables are set
|
||||
check_required_vars() {
|
||||
local missing=false
|
||||
if [ $# -eq 0 ]; then
|
||||
if [ -z "$HOST" ]; then
|
||||
print_error "HOST is not set. Please set it first."
|
||||
missing=true
|
||||
fi
|
||||
if [ -z "$MAP_SLUG" ]; then
|
||||
print_error "MAP_SLUG is not set. Please set it first."
|
||||
missing=true
|
||||
fi
|
||||
if [ -z "$MAP_API_KEY" ]; then
|
||||
print_error "MAP_API_KEY is not set. Please set it first."
|
||||
missing=true
|
||||
fi
|
||||
else
|
||||
for var in "$@"; do
|
||||
if [ -z "${!var}" ]; then
|
||||
print_error "$var is not set. Please set it first."
|
||||
missing=true
|
||||
fi
|
||||
done
|
||||
fi
|
||||
$missing && return 1 || return 0
|
||||
}
|
||||
|
||||
# Record a test result
|
||||
record_test_result() {
|
||||
local endpoint=$1
|
||||
local status=$2
|
||||
local message=$3
|
||||
if [ "$status" = "success" ]; then
|
||||
TEST_RESULTS+=("${GREEN}✓${NC} $endpoint - $message")
|
||||
else
|
||||
TEST_RESULTS+=("${RED}✗${NC} $endpoint - $message")
|
||||
FAILED_TESTS+=("$endpoint - $message")
|
||||
fi
|
||||
}
|
||||
|
||||
# Process and validate the JSON response
|
||||
check_response() {
|
||||
local response=$1
|
||||
local endpoint=$2
|
||||
|
||||
if [ -z "$(echo "$response" | xargs)" ]; then
|
||||
if [ "$LAST_HTTP_CODE" = "200" ] || [ "$LAST_HTTP_CODE" = "204" ]; then
|
||||
print_success "Received empty response, which is valid"
|
||||
LAST_JSON_RESPONSE="{}"
|
||||
return 0
|
||||
else
|
||||
record_test_result "$endpoint" "failure" "Empty response with HTTP code $LAST_HTTP_CODE"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$VERBOSE" -eq 1 ]; then
|
||||
echo "Raw response from $endpoint:"
|
||||
echo "$response" | head -n 20
|
||||
fi
|
||||
|
||||
if echo "$response" | jq . > /dev/null 2>&1; then
|
||||
LAST_JSON_RESPONSE="$response"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local json_part
|
||||
json_part=$(echo "$response" | grep -o '{.*}' || echo "")
|
||||
if [ -z "$json_part" ] || ! echo "$json_part" | jq . > /dev/null 2>&1; then
|
||||
json_part=$(echo "$response" | sed -n '/^{/,$p' | tr -d '\n')
|
||||
fi
|
||||
if [ -z "$json_part" ] || ! echo "$json_part" | jq . > /dev/null 2>&1; then
|
||||
json_part=$(echo "$response" | sed -n '/{/,/}/p' | tr -d '\n')
|
||||
fi
|
||||
if [ -z "$json_part" ] || ! echo "$json_part" | jq . > /dev/null 2>&1; then
|
||||
json_part=$(echo "$response" | awk '!(/^[<>*]/) {print}' | tr -d '\n')
|
||||
fi
|
||||
if [ -z "$json_part" ] || ! echo "$json_part" | jq . > /dev/null 2>&1; then
|
||||
echo "Raw response from $endpoint:"
|
||||
echo "$response"
|
||||
record_test_result "$endpoint" "failure" "Invalid JSON response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local error
|
||||
error=$(echo "$json_part" | jq -r '.error // empty')
|
||||
if [ -n "$error" ]; then
|
||||
echo "Raw response from $endpoint:"
|
||||
echo "$response"
|
||||
echo "Parsed JSON response from $endpoint:"
|
||||
echo "$json_part" | jq '.'
|
||||
record_test_result "$endpoint" "failure" "Error: $error"
|
||||
return 1
|
||||
fi
|
||||
|
||||
LAST_JSON_RESPONSE="$json_part"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Get a random item from a JSON array using a jq path
|
||||
get_random_item() {
|
||||
local json=$1
|
||||
local jq_path=$2
|
||||
local count
|
||||
count=$(echo "$json" | jq "$jq_path | length")
|
||||
if [ "$count" -eq 0 ]; then
|
||||
echo ""
|
||||
return 1
|
||||
fi
|
||||
local random_index=$((RANDOM % count))
|
||||
echo "$json" | jq -r "$jq_path[$random_index]"
|
||||
}
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# API Test Functions
|
||||
#------------------------------------------------------------------------------
|
||||
test_list_characters() {
|
||||
print_header "Testing GET /api/characters"
|
||||
print_success "Calling API: GET /api/characters"
|
||||
local response
|
||||
response=$(call_api "GET" "/api/characters" "$MAP_API_KEY")
|
||||
if ! check_response "$response" "GET /api/characters"; then
|
||||
return 1
|
||||
fi
|
||||
local character_count
|
||||
character_count=$(echo "$LAST_JSON_RESPONSE" | jq '.data | length')
|
||||
if [ "$character_count" -gt 0 ]; then
|
||||
record_test_result "GET /api/characters" "success" "Found $character_count characters"
|
||||
if [ -z "$CHARACTER_EVE_ID" ]; then
|
||||
local random_index=$((RANDOM % character_count))
|
||||
print_success "Selecting character at index $random_index"
|
||||
local random_character
|
||||
random_character=$(echo "$LAST_JSON_RESPONSE" | jq ".data[$random_index]")
|
||||
CHARACTER_EVE_ID=$(echo "$random_character" | jq -r '.eve_id')
|
||||
local character_name
|
||||
character_name=$(echo "$random_character" | jq -r '.name')
|
||||
print_success "Selected random character: $character_name (EVE ID: $CHARACTER_EVE_ID)"
|
||||
fi
|
||||
return 0
|
||||
else
|
||||
record_test_result "GET /api/characters" "success" "No characters found"
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
test_map_systems() {
|
||||
print_header "Testing GET /api/map/systems"
|
||||
if ! check_required_vars "MAP_SLUG" "MAP_API_KEY"; then
|
||||
record_test_result "GET /api/map/systems" "failure" "Missing required variables"
|
||||
return 1
|
||||
fi
|
||||
print_success "Calling API: GET /api/map/systems?slug=$MAP_SLUG"
|
||||
local response
|
||||
response=$(call_api "GET" "/api/map/systems?slug=$MAP_SLUG" "$MAP_API_KEY")
|
||||
if ! check_response "$response" "GET /api/map/systems"; then
|
||||
return 1
|
||||
fi
|
||||
local system_count
|
||||
system_count=$(echo "$LAST_JSON_RESPONSE" | jq '.data | length')
|
||||
print_success "System count: $system_count"
|
||||
if [ "$system_count" -gt 0 ]; then
|
||||
record_test_result "GET /api/map/systems" "success" "Found $system_count systems"
|
||||
local random_index=$((RANDOM % system_count))
|
||||
print_success "Selecting system at index $random_index"
|
||||
echo "Data structure:"
|
||||
echo "$LAST_JSON_RESPONSE" | jq '.data[0]'
|
||||
local random_system
|
||||
random_system=$(echo "$LAST_JSON_RESPONSE" | jq ".data[$random_index]")
|
||||
echo "Selected system JSON:"
|
||||
echo "$random_system"
|
||||
SELECTED_SYSTEM_ID=$(echo "$random_system" | jq -r '.solar_system_id')
|
||||
if [ -z "$SELECTED_SYSTEM_ID" ] || [ "$SELECTED_SYSTEM_ID" = "null" ]; then
|
||||
SELECTED_SYSTEM_ID=$(echo "$random_system" | jq -r '.id // .system_id // empty')
|
||||
if [ -z "$SELECTED_SYSTEM_ID" ] || [ "$SELECTED_SYSTEM_ID" = "null" ]; then
|
||||
print_error "Could not find system ID in the response"
|
||||
echo "Available fields:"
|
||||
echo "$random_system" | jq 'keys'
|
||||
record_test_result "GET /api/map/systems" "failure" "Could not extract system ID"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
local system_name
|
||||
system_name=$(echo "$random_system" | jq -r '.name // "Unknown"')
|
||||
print_success "Selected random system: $system_name (ID: $SELECTED_SYSTEM_ID)"
|
||||
return 0
|
||||
else
|
||||
record_test_result "GET /api/map/systems" "failure" "No systems found"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
test_map_system() {
|
||||
print_header "Testing GET /api/map/system"
|
||||
if [[ -z "$MAP_SLUG" || -z "$SELECTED_SYSTEM_ID" || -z "$MAP_API_KEY" ]]; then
|
||||
record_test_result "GET /api/map/system" "failure" "Missing required variables"
|
||||
return
|
||||
fi
|
||||
local response
|
||||
response=$(call_api "GET" "/api/map/system?slug=$MAP_SLUG&id=$SELECTED_SYSTEM_ID" "$MAP_API_KEY")
|
||||
print_warning "Response: $response"
|
||||
local trimmed_response
|
||||
trimmed_response=$(echo "$response" | xargs)
|
||||
if [[ "$trimmed_response" == "{}" || "$trimmed_response" == '{"data":{}}' ]]; then
|
||||
print_success "Received empty JSON response, which is valid"
|
||||
record_test_result "GET /api/map/system" "success" "Received valid empty response"
|
||||
return
|
||||
fi
|
||||
if ! check_response "$response" "GET /api/map/system"; then
|
||||
return
|
||||
fi
|
||||
local json_data="$LAST_JSON_RESPONSE"
|
||||
local has_data
|
||||
has_data=$(echo "$json_data" | jq 'has("data")')
|
||||
if [ "$has_data" != "true" ]; then
|
||||
print_error "Response does not contain 'data' field"
|
||||
echo "JSON Response:"
|
||||
echo "$json_data" | jq .
|
||||
record_test_result "GET /api/map/system" "failure" "Response does not contain 'data' field"
|
||||
return
|
||||
fi
|
||||
local system_data
|
||||
system_data=$(echo "$json_data" | jq -r '.data // empty')
|
||||
if [ -z "$system_data" ] || [ "$system_data" = "null" ]; then
|
||||
print_error "Could not find system data in response"
|
||||
echo "JSON Response:"
|
||||
echo "$json_data" | jq .
|
||||
record_test_result "GET /api/map/system" "failure" "Could not find system data in response"
|
||||
return
|
||||
fi
|
||||
local system_id
|
||||
system_id=$(echo "$json_data" | jq -r '.data.solar_system_id // empty')
|
||||
if [ -z "$system_id" ] || [ "$system_id" = "null" ]; then
|
||||
print_error "Could not find solar_system_id in the system data"
|
||||
echo "System Data:"
|
||||
echo "$system_data" | jq .
|
||||
record_test_result "GET /api/map/system" "failure" "Could not find solar_system_id in system data"
|
||||
return
|
||||
fi
|
||||
print_success "Found system data with ID: $system_id"
|
||||
record_test_result "GET /api/map/system" "success" "Found system data with ID: $system_id"
|
||||
}
|
||||
|
||||
test_map_characters() {
|
||||
print_header "Testing GET /api/map/characters"
|
||||
if ! check_required_vars "MAP_SLUG" "MAP_API_KEY"; then
|
||||
record_test_result "GET /api/map/characters" "failure" "Missing required variables"
|
||||
return 1
|
||||
fi
|
||||
print_success "Calling API: GET /api/map/characters?slug=$MAP_SLUG"
|
||||
local response
|
||||
response=$(call_api "GET" "/api/map/characters?slug=$MAP_SLUG" "$MAP_API_KEY")
|
||||
if ! check_response "$response" "GET /api/map/characters"; then
|
||||
return 1
|
||||
fi
|
||||
local character_count
|
||||
character_count=$(echo "$LAST_JSON_RESPONSE" | jq '.data | length')
|
||||
record_test_result "GET /api/map/characters" "success" "Found $character_count tracked characters"
|
||||
return 0
|
||||
}
|
||||
|
||||
test_map_structure_timers() {
|
||||
print_header "Testing GET /api/map/structure-timers"
|
||||
if [[ -z "$MAP_SLUG" || -z "$MAP_API_KEY" ]]; then
|
||||
record_test_result "GET /api/map/structure-timers" "failure" "Missing required variables"
|
||||
return
|
||||
fi
|
||||
local response
|
||||
response=$(call_api "GET" "/api/map/structure-timers?slug=$MAP_SLUG" "$MAP_API_KEY")
|
||||
local trimmed_response
|
||||
trimmed_response=$(echo "$response" | xargs)
|
||||
if [[ "$trimmed_response" == '{"data":[]}' ]]; then
|
||||
print_success "Found 0 structure timers"
|
||||
record_test_result "GET /api/map/structure-timers" "success" "Found 0 structure timers"
|
||||
fi
|
||||
if ! check_response "$response" "GET /api/map/structure-timers"; then
|
||||
return
|
||||
fi
|
||||
local timer_count
|
||||
timer_count=$(echo "$LAST_JSON_RESPONSE" | jq '.data | length')
|
||||
print_success "Found $timer_count structure timers"
|
||||
record_test_result "GET /api/map/structure-timers" "success" "Found $timer_count structure timers"
|
||||
if [ -n "$SELECTED_SYSTEM_ID" ]; then
|
||||
print_header "Testing GET /api/map/structure-timers (filtered)"
|
||||
local filtered_response
|
||||
filtered_response=$(call_api "GET" "/api/map/structure-timers?slug=$MAP_SLUG&system_id=$SELECTED_SYSTEM_ID" "$MAP_API_KEY")
|
||||
print_warning "(Structure Timers) - Filtered response: $filtered_response"
|
||||
local trimmed_filtered
|
||||
trimmed_filtered=$(echo "$filtered_response" | xargs)
|
||||
if [[ "$trimmed_filtered" == '{"data":[]}' ]]; then
|
||||
print_success "Found 0 filtered structure timers"
|
||||
record_test_result "GET /api/map/structure-timers (filtered)" "success" "Found 0 filtered structure timers"
|
||||
return
|
||||
fi
|
||||
if ! check_response "$filtered_response" "GET /api/map/structure-timers (filtered)"; then
|
||||
return
|
||||
fi
|
||||
local filtered_count
|
||||
filtered_count=$(echo "$LAST_JSON_RESPONSE" | jq '.data | length')
|
||||
print_success "Found $filtered_count filtered structure timers"
|
||||
record_test_result "GET /api/map/structure-timers (filtered)" "success" "Found $filtered_count filtered structure timers"
|
||||
fi
|
||||
}
|
||||
|
||||
test_map_systems_kills() {
|
||||
print_header "Testing GET /api/map/systems-kills"
|
||||
if [[ -z "$MAP_SLUG" || -z "$MAP_API_KEY" ]]; then
|
||||
record_test_result "GET /api/map/systems-kills" "failure" "Missing required variables"
|
||||
return
|
||||
fi
|
||||
# Use the correct parameter name: hours
|
||||
local response
|
||||
response=$(call_api "GET" "/api/map/systems-kills?slug=$MAP_SLUG&hours=1" "$MAP_API_KEY")
|
||||
print_warning "(Systems Kills) - Response: $response"
|
||||
if ! check_response "$response" "GET /api/map/systems-kills"; then
|
||||
return
|
||||
fi
|
||||
local json_data="$LAST_JSON_RESPONSE"
|
||||
if [ "$VERBOSE" -eq 1 ]; then
|
||||
echo "JSON Response:"; echo "$json_data" | jq .
|
||||
fi
|
||||
local has_data
|
||||
has_data=$(echo "$json_data" | jq 'has("data")')
|
||||
if [ "$has_data" != "true" ]; then
|
||||
print_error "Response does not contain 'data' field"
|
||||
if [ "$VERBOSE" -eq 1 ]; then
|
||||
echo "JSON Response:"; echo "$json_data" | jq .
|
||||
fi
|
||||
record_test_result "GET /api/map/systems-kills" "failure" "Response does not contain 'data' field"
|
||||
return
|
||||
fi
|
||||
local systems_count
|
||||
systems_count=$(echo "$json_data" | jq '.data | length')
|
||||
print_success "Found kill data for $systems_count systems"
|
||||
record_test_result "GET /api/map/systems-kills" "success" "Found kill data for $systems_count systems"
|
||||
print_header "Testing GET /api/map/systems-kills (filtered)"
|
||||
local filter_url="/api/map/systems-kills?slug=$MAP_SLUG&hours=1"
|
||||
if [ -n "$SELECTED_SYSTEM_ID" ]; then
|
||||
filter_url="$filter_url&system_id=$SELECTED_SYSTEM_ID"
|
||||
print_success "Using system_id filter to reduce response size"
|
||||
fi
|
||||
local filtered_response
|
||||
filtered_response=$(call_api "GET" "$filter_url" "$MAP_API_KEY")
|
||||
local trimmed_filtered
|
||||
trimmed_filtered=$(echo "$filtered_response" | xargs)
|
||||
if [[ "$trimmed_filtered" == '{"data":[]}' ]]; then
|
||||
print_success "Found 0 filtered systems with kill data"
|
||||
record_test_result "GET /api/map/systems-kills (filtered)" "success" "Found 0 filtered systems with kill data"
|
||||
return
|
||||
fi
|
||||
if [[ "$trimmed_filtered" == '{"data":'* ]]; then
|
||||
print_success "Received valid JSON response (large data)"
|
||||
record_test_result "GET /api/map/systems-kills (filtered)" "success" "Received valid JSON response with kill data"
|
||||
return
|
||||
fi
|
||||
if ! check_response "$filtered_response" "GET /api/map/systems-kills (filtered)"; then
|
||||
return
|
||||
fi
|
||||
local filtered_count
|
||||
filtered_count=$(echo "$LAST_JSON_RESPONSE" | jq '.data | length')
|
||||
print_success "Found filtered kill data for $filtered_count systems"
|
||||
record_test_result "GET /api/map/systems-kills (filtered)" "success" "Found filtered kill data for $filtered_count systems"
|
||||
}
|
||||
|
||||
test_map_acls() {
|
||||
print_header "Testing GET /api/map/acls"
|
||||
if ! check_required_vars "MAP_SLUG" "MAP_API_KEY"; then
|
||||
record_test_result "GET /api/map/acls" "failure" "Missing required variables"
|
||||
return 1
|
||||
fi
|
||||
print_success "Calling API: GET /api/map/acls?slug=$MAP_SLUG"
|
||||
local response
|
||||
response=$(call_api "GET" "/api/map/acls?slug=$MAP_SLUG" "$MAP_API_KEY")
|
||||
if ! check_response "$response" "GET /api/map/acls"; then
|
||||
return 1
|
||||
fi
|
||||
local acl_count
|
||||
acl_count=$(echo "$LAST_JSON_RESPONSE" | jq '.data | length')
|
||||
record_test_result "GET /api/map/acls" "success" "Found $acl_count ACLs"
|
||||
if [ "$acl_count" -gt 0 ]; then
|
||||
local random_acl
|
||||
random_acl=$(get_random_item "$LAST_JSON_RESPONSE" ".data")
|
||||
SELECTED_ACL_ID=$(echo "$random_acl" | jq -r '.id')
|
||||
local acl_name
|
||||
acl_name=$(echo "$random_acl" | jq -r '.name')
|
||||
print_success "Selected random ACL: $acl_name (ID: $SELECTED_ACL_ID)"
|
||||
else
|
||||
print_warning "No ACLs found to select for future tests"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
test_create_acl() {
|
||||
print_header "Testing POST /api/map/acls"
|
||||
if ! check_required_vars "MAP_SLUG" "MAP_API_KEY"; then
|
||||
record_test_result "POST /api/map/acls" "failure" "Missing required variables"
|
||||
return 1
|
||||
fi
|
||||
if [ -z "$CHARACTER_EVE_ID" ]; then
|
||||
print_warning "No character EVE ID selected. Fetching characters..."
|
||||
print_success "Calling API: GET /api/characters"
|
||||
local characters_response
|
||||
characters_response=$(call_api "GET" "/api/characters" "$MAP_API_KEY")
|
||||
if ! check_response "$characters_response" "GET /api/characters"; then
|
||||
record_test_result "POST /api/map/acls" "failure" "Failed to get characters"
|
||||
return 1
|
||||
fi
|
||||
local character_count
|
||||
character_count=$(echo "$LAST_JSON_RESPONSE" | jq '.data | length')
|
||||
if [ "$character_count" -eq 0 ]; then
|
||||
record_test_result "POST /api/map/acls" "failure" "No characters found"
|
||||
return 1
|
||||
fi
|
||||
local random_index=$((RANDOM % character_count))
|
||||
print_success "Selecting character at index $random_index"
|
||||
local random_character
|
||||
random_character=$(echo "$LAST_JSON_RESPONSE" | jq ".data[$random_index]")
|
||||
CHARACTER_EVE_ID=$(echo "$random_character" | jq -r '.eve_id')
|
||||
local character_name
|
||||
character_name=$(echo "$random_character" | jq -r '.name')
|
||||
print_success "Selected random character: $character_name (EVE ID: $CHARACTER_EVE_ID)"
|
||||
fi
|
||||
local acl_name="Auto Test ACL $(date +%s)"
|
||||
local acl_description="Created by auto_test_api.sh on $(date)"
|
||||
local data="{\"acl\": {\"name\": \"$acl_name\", \"owner_eve_id\": $CHARACTER_EVE_ID, \"description\": \"$acl_description\"}}"
|
||||
print_success "Calling API: POST /api/map/acls?slug=$MAP_SLUG"
|
||||
print_success "Data: $data"
|
||||
local response
|
||||
response=$(call_api "POST" "/api/map/acls?slug=$MAP_SLUG" "$MAP_API_KEY" "$data")
|
||||
if ! check_response "$response" "POST /api/map/acls"; then
|
||||
return 1
|
||||
fi
|
||||
local new_acl_id
|
||||
new_acl_id=$(echo "$LAST_JSON_RESPONSE" | jq -r '.data.id // empty')
|
||||
local new_api_key
|
||||
new_api_key=$(echo "$LAST_JSON_RESPONSE" | jq -r '.data.api_key // empty')
|
||||
if [ -n "$new_acl_id" ] && [ -n "$new_api_key" ]; then
|
||||
record_test_result "POST /api/map/acls" "success" "Created new ACL with ID: $new_acl_id"
|
||||
SELECTED_ACL_ID=$new_acl_id
|
||||
ACL_API_KEY=$new_api_key
|
||||
print_success "Using the new ACL (ID: $SELECTED_ACL_ID) and its API key for further operations"
|
||||
save_config
|
||||
return 0
|
||||
else
|
||||
record_test_result "POST /api/map/acls" "failure" "Failed to extract ACL ID or API key from response"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
test_show_acl() {
|
||||
print_header "Testing GET /api/acls/:id"
|
||||
if [ -z "$SELECTED_ACL_ID" ] || [ -z "$ACL_API_KEY" ]; then
|
||||
record_test_result "GET /api/acls/:id" "failure" "Missing ACL ID or API key"
|
||||
return 1
|
||||
fi
|
||||
print_success "Calling API: GET /api/acls/$SELECTED_ACL_ID"
|
||||
local response
|
||||
response=$(call_api "GET" "/api/acls/$SELECTED_ACL_ID" "$ACL_API_KEY")
|
||||
if ! check_response "$response" "GET /api/acls/:id"; then
|
||||
return 1
|
||||
fi
|
||||
local acl_name
|
||||
acl_name=$(echo "$LAST_JSON_RESPONSE" | jq -r '.data.name // empty')
|
||||
if [ -n "$acl_name" ]; then
|
||||
record_test_result "GET /api/acls/:id" "success" "Found ACL: $acl_name"
|
||||
return 0
|
||||
else
|
||||
record_test_result "GET /api/acls/:id" "failure" "ACL data not found"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
test_update_acl() {
|
||||
print_header "Testing PUT /api/acls/:id"
|
||||
if [ -z "$SELECTED_ACL_ID" ] || [ -z "$ACL_API_KEY" ]; then
|
||||
record_test_result "PUT /api/acls/:id" "failure" "Missing ACL ID or API key"
|
||||
return 1
|
||||
fi
|
||||
local new_name="Updated Auto Test ACL $(date +%s)"
|
||||
local new_description="Updated by auto_test_api.sh on $(date)"
|
||||
local data="{\"acl\": {\"name\": \"$new_name\", \"description\": \"$new_description\"}}"
|
||||
print_success "Calling API: PUT /api/acls/$SELECTED_ACL_ID"
|
||||
print_success "Data: $data"
|
||||
local response
|
||||
response=$(call_api "PUT" "/api/acls/$SELECTED_ACL_ID" "$ACL_API_KEY" "$data")
|
||||
if ! check_response "$response" "PUT /api/acls/:id"; then
|
||||
return 1
|
||||
fi
|
||||
local updated_name
|
||||
updated_name=$(echo "$LAST_JSON_RESPONSE" | jq -r '.data.name // empty')
|
||||
if [ "$updated_name" = "$new_name" ]; then
|
||||
record_test_result "PUT /api/acls/:id" "success" "Updated ACL name to: $updated_name"
|
||||
return 0
|
||||
else
|
||||
record_test_result "PUT /api/acls/:id" "failure" "Failed to update ACL name"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
test_create_acl_member() {
|
||||
print_header "Testing POST /api/acls/:acl_id/members"
|
||||
if [ -z "$SELECTED_ACL_ID" ] || [ -z "$ACL_API_KEY" ]; then
|
||||
record_test_result "POST /api/acls/:acl_id/members" "failure" "Missing ACL ID or API key"
|
||||
return 1
|
||||
fi
|
||||
if [ -z "$CHARACTER_EVE_ID" ]; then
|
||||
print_warning "No character EVE ID selected. Fetching characters..."
|
||||
print_success "Calling API: GET /api/characters"
|
||||
local characters_response
|
||||
characters_response=$(call_api "GET" "/api/characters" "$MAP_API_KEY")
|
||||
if ! check_response "$characters_response" "GET /api/characters"; then
|
||||
record_test_result "POST /api/acls/:acl_id/members" "failure" "Failed to get characters"
|
||||
return 1
|
||||
fi
|
||||
local character_count
|
||||
character_count=$(echo "$LAST_JSON_RESPONSE" | jq '.data | length')
|
||||
if [ "$character_count" -eq 0 ]; then
|
||||
record_test_result "POST /api/acls/:acl_id/members" "failure" "No characters found"
|
||||
return 1
|
||||
fi
|
||||
local random_index=$((RANDOM % character_count))
|
||||
print_success "Selecting character at index $random_index"
|
||||
local random_character
|
||||
random_character=$(echo "$LAST_JSON_RESPONSE" | jq ".data[$random_index]")
|
||||
CHARACTER_EVE_ID=$(echo "$random_character" | jq -r '.eve_id')
|
||||
local character_name
|
||||
character_name=$(echo "$random_character" | jq -r '.name')
|
||||
print_success "Selected random character: $character_name (EVE ID: $CHARACTER_EVE_ID)"
|
||||
fi
|
||||
local data="{\"member\": {\"eve_character_id\": $CHARACTER_EVE_ID, \"role\": \"member\"}}"
|
||||
print_success "Calling API: POST /api/acls/$SELECTED_ACL_ID/members"
|
||||
print_success "Data: $data"
|
||||
local response
|
||||
response=$(call_api "POST" "/api/acls/$SELECTED_ACL_ID/members" "$ACL_API_KEY" "$data")
|
||||
if ! check_response "$response" "POST /api/acls/:acl_id/members"; then
|
||||
return 1
|
||||
fi
|
||||
local member_id
|
||||
member_id=$(echo "$LAST_JSON_RESPONSE" | jq -r '.data.id // empty')
|
||||
if [ -n "$member_id" ]; then
|
||||
record_test_result "POST /api/acls/:acl_id/members" "success" "Created new member with ID: $member_id"
|
||||
MEMBER_ID=$CHARACTER_EVE_ID
|
||||
return 0
|
||||
else
|
||||
record_test_result "POST /api/acls/:acl_id/members" "failure" "Failed to create member"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
test_update_acl_member() {
|
||||
print_header "Testing PUT /api/acls/:acl_id/members/:member_id"
|
||||
if [ -z "$SELECTED_ACL_ID" ] || [ -z "$ACL_API_KEY" ] || [ -z "$MEMBER_ID" ]; then
|
||||
record_test_result "PUT /api/acls/:acl_id/members/:member_id" "failure" "Missing ACL ID, API key, or member ID"
|
||||
return 1
|
||||
fi
|
||||
local data="{\"member\": {\"role\": \"member\"}}"
|
||||
print_success "Calling API: PUT /api/acls/$SELECTED_ACL_ID/members/$MEMBER_ID"
|
||||
print_success "Data: $data"
|
||||
local response
|
||||
response=$(call_api "PUT" "/api/acls/$SELECTED_ACL_ID/members/$MEMBER_ID" "$ACL_API_KEY" "$data")
|
||||
if ! check_response "$response" "PUT /api/acls/:acl_id/members/:member_id"; then
|
||||
return 1
|
||||
fi
|
||||
local updated_role
|
||||
updated_role=$(echo "$LAST_JSON_RESPONSE" | jq -r '.data.role // empty')
|
||||
if [ "$updated_role" = "member" ]; then
|
||||
record_test_result "PUT /api/acls/:acl_id/members/:member_id" "success" "Updated member role to: $updated_role"
|
||||
return 0
|
||||
else
|
||||
record_test_result "PUT /api/acls/:acl_id/members/:member_id" "failure" "Failed to update member role"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
test_delete_acl_member() {
|
||||
print_header "Testing DELETE /api/acls/:acl_id/members/:member_id"
|
||||
if [ -z "$SELECTED_ACL_ID" ] || [ -z "$ACL_API_KEY" ] || [ -z "$MEMBER_ID" ]; then
|
||||
record_test_result "DELETE /api/acls/:acl_id/members/:member_id" "failure" "Missing ACL ID, API key, or member ID"
|
||||
return 1
|
||||
fi
|
||||
print_success "Calling API: DELETE /api/acls/$SELECTED_ACL_ID/members/$MEMBER_ID"
|
||||
local response
|
||||
response=$(call_api "DELETE" "/api/acls/$SELECTED_ACL_ID/members/$MEMBER_ID" "$ACL_API_KEY")
|
||||
if ! check_response "$response" "DELETE /api/acls/:acl_id/members/:member_id"; then
|
||||
return 1
|
||||
fi
|
||||
record_test_result "DELETE /api/acls/:acl_id/members/:member_id" "success" "Deleted member with ID: $MEMBER_ID"
|
||||
MEMBER_ID=""
|
||||
return 0
|
||||
}
|
||||
|
||||
test_system_static_info() {
|
||||
print_header "Testing GET /api/common/system-static-info"
|
||||
if [ -z "$SELECTED_SYSTEM_ID" ]; then
|
||||
record_test_result "GET /api/common/system-static-info" "failure" "No system ID selected"
|
||||
return 1
|
||||
fi
|
||||
print_success "Calling API: GET /api/common/system-static-info?id=$SELECTED_SYSTEM_ID"
|
||||
local response
|
||||
response=$(call_api "GET" "/api/common/system-static-info?id=$SELECTED_SYSTEM_ID" "$MAP_API_KEY")
|
||||
if ! check_response "$response" "GET /api/common/system-static-info"; then
|
||||
return 1
|
||||
fi
|
||||
local system_count
|
||||
system_count=$(echo "$LAST_JSON_RESPONSE" | jq 'length')
|
||||
record_test_result "GET /api/common/system-static-info" "success" "Found static info for $system_count systems"
|
||||
return 0
|
||||
}
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# Configuration and Main Menu Functions
|
||||
#------------------------------------------------------------------------------
|
||||
set_config() {
|
||||
print_header "Configuration"
|
||||
echo -e "Current configuration:"
|
||||
[ -n "$HOST" ] && echo -e " Host: ${BLUE}$HOST${NC}"
|
||||
[ -n "$MAP_SLUG" ] && echo -e " Map Slug: ${BLUE}$MAP_SLUG${NC}"
|
||||
[ -n "$MAP_API_KEY" ] && echo -e " Map API Key: ${BLUE}${MAP_API_KEY:0:8}...${NC}"
|
||||
read -p "Enter host (default: $HOST): " input_host
|
||||
[ -n "$input_host" ] && HOST="$input_host"
|
||||
read -p "Enter map slug: " input_map_slug
|
||||
[ -n "$input_map_slug" ] && MAP_SLUG="$input_map_slug"
|
||||
read -p "Enter map API key: " input_map_api_key
|
||||
[ -n "$input_map_api_key" ] && MAP_API_KEY="$input_map_api_key"
|
||||
# Reset IDs to force fresh data
|
||||
SELECTED_SYSTEM_ID=""
|
||||
SELECTED_ACL_ID=""
|
||||
ACL_API_KEY=""
|
||||
CHARACTER_EVE_ID=""
|
||||
save_config
|
||||
}
|
||||
|
||||
run_all_tests() {
|
||||
print_header "Running all API tests"
|
||||
TEST_RESULTS=()
|
||||
FAILED_TESTS=()
|
||||
|
||||
if ! command -v jq &> /dev/null; then
|
||||
print_error "jq is required for this script to work. Please install it first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! check_required_vars "MAP_SLUG" "MAP_API_KEY"; then
|
||||
print_error "Please set MAP_SLUG and MAP_API_KEY before running tests."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
check_host_reachable
|
||||
|
||||
test_list_characters
|
||||
if test_map_systems; then
|
||||
test_map_system
|
||||
else
|
||||
print_error "Skipping test_map_system because test_map_systems failed"
|
||||
record_test_result "GET /api/map/system" "failure" "Skipped because test_map_systems failed"
|
||||
fi
|
||||
test_map_characters
|
||||
test_map_structure_timers
|
||||
test_map_systems_kills
|
||||
test_map_acls
|
||||
if test_create_acl; then
|
||||
test_show_acl
|
||||
test_update_acl
|
||||
if test_create_acl_member; then
|
||||
test_update_acl_member
|
||||
test_delete_acl_member
|
||||
else
|
||||
print_error "Skipping ACL member tests because test_create_acl_member failed"
|
||||
record_test_result "PUT /api/acls/:acl_id/members/:member_id" "failure" "Skipped because test_create_acl_member failed"
|
||||
record_test_result "DELETE /api/acls/:acl_id/members/:member_id" "failure" "Skipped because test_create_acl_member failed"
|
||||
fi
|
||||
else
|
||||
print_error "Skipping ACL tests because test_create_acl failed"
|
||||
record_test_result "GET /api/acls/:id" "failure" "Skipped because test_create_acl failed"
|
||||
record_test_result "PUT /api/acls/:id" "failure" "Skipped because test_create_acl failed"
|
||||
record_test_result "POST /api/acls/:acl_id/members" "failure" "Skipped because test_create_acl failed"
|
||||
record_test_result "PUT /api/acls/:acl_id/members/:member_id" "failure" "Skipped because test_create_acl failed"
|
||||
record_test_result "DELETE /api/acls/:acl_id/members/:member_id" "failure" "Skipped because test_create_acl failed"
|
||||
fi
|
||||
test_system_static_info
|
||||
|
||||
print_header "Test Results"
|
||||
for result in "${TEST_RESULTS[@]}"; do
|
||||
echo -e "$result"
|
||||
done
|
||||
|
||||
local total_tests=${#TEST_RESULTS[@]}
|
||||
local failed_tests=${#FAILED_TESTS[@]}
|
||||
local passed_tests=$((total_tests - failed_tests))
|
||||
print_header "Summary"
|
||||
echo -e "Total tests: $total_tests"
|
||||
echo -e "Passed: ${GREEN}$passed_tests${NC}"
|
||||
echo -e "Failed: ${RED}$failed_tests${NC}"
|
||||
if [ $failed_tests -gt 0 ]; then
|
||||
print_header "Failed Tests"
|
||||
for failed in "${FAILED_TESTS[@]}"; do
|
||||
echo -e "${RED}✗${NC} $failed"
|
||||
done
|
||||
fi
|
||||
|
||||
if [ "$VERBOSE_SUMMARY" -eq 1 ]; then
|
||||
summary_json=$(jq -n --arg total "$total_tests" --arg passed "$passed_tests" --arg failed "$failed_tests" \
|
||||
'{total_tests: $total_tests|tonumber, passed: $passed|tonumber, failed: $failed|tonumber}')
|
||||
echo "JSON Summary:"; echo "$summary_json" | jq .
|
||||
fi
|
||||
|
||||
save_config
|
||||
|
||||
if [ $failed_tests -gt 0 ]; then
|
||||
exit 1
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# Main Menu and Entry Point
|
||||
#------------------------------------------------------------------------------
|
||||
main() {
|
||||
print_header "Wanderer API Automated Testing Tool"
|
||||
load_config
|
||||
if [ -z "$MAP_SLUG" ] || [ -z "$MAP_API_KEY" ]; then
|
||||
print_warning "MAP_SLUG or MAP_API_KEY not set. Let's configure them now."
|
||||
set_config
|
||||
fi
|
||||
echo -e "What would you like to do?"
|
||||
echo "1) Run all tests"
|
||||
echo "2) Set configuration"
|
||||
echo "3) Exit"
|
||||
read -p "Enter your choice: " choice
|
||||
case $choice in
|
||||
1) run_all_tests ;;
|
||||
2) set_config ;;
|
||||
3) exit 0 ;;
|
||||
*) print_error "Invalid choice"; main ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Start the script
|
||||
main
|
||||
334
test/unit/character_api_controller_test.exs
Normal file
@@ -0,0 +1,334 @@
|
||||
# Standalone test for the CharacterAPIController
|
||||
#
|
||||
# This file can be run directly with:
|
||||
# elixir test/standalone/character_api_controller_test.exs
|
||||
#
|
||||
# It doesn't require any database connections or external dependencies.
|
||||
|
||||
# Start ExUnit
|
||||
ExUnit.start()
|
||||
|
||||
defmodule CharacterAPIControllerTest do
|
||||
use ExUnit.Case
|
||||
|
||||
# Mock modules to simulate the behavior of the controller's dependencies
|
||||
defmodule MockUtil do
|
||||
def require_param(params, key) do
|
||||
case params[key] do
|
||||
nil -> {:error, "Missing required param: #{key}"}
|
||||
"" -> {:error, "Param #{key} cannot be empty"}
|
||||
val -> {:ok, val}
|
||||
end
|
||||
end
|
||||
|
||||
def parse_int(str) do
|
||||
case Integer.parse(str) do
|
||||
{num, ""} -> {:ok, num}
|
||||
_ -> {:error, "Invalid integer for param id=#{str}"}
|
||||
end
|
||||
end
|
||||
|
||||
def parse_bool(str) do
|
||||
case str do
|
||||
"true" -> {:ok, true}
|
||||
"false" -> {:ok, false}
|
||||
_ -> {:error, "Invalid boolean value: #{str}"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule MockCharacterRepo do
|
||||
# In-memory storage for character tracking data
|
||||
def init_storage do
|
||||
:ets.new(:character_tracking, [:set, :public, :named_table])
|
||||
|
||||
# Initialize with some test data
|
||||
:ets.insert(:character_tracking, {"user1", [
|
||||
%{eve_id: "123456", name: "Character One", tracked: true, followed: true},
|
||||
%{eve_id: "234567", name: "Character Two", tracked: true, followed: false},
|
||||
%{eve_id: "345678", name: "Character Three", tracked: false, followed: false}
|
||||
]})
|
||||
|
||||
:ets.insert(:character_tracking, {"user2", [
|
||||
%{eve_id: "456789", name: "Character Four", tracked: true, followed: true}
|
||||
]})
|
||||
end
|
||||
|
||||
def get_tracking_data(user_id) do
|
||||
case :ets.lookup(:character_tracking, user_id) do
|
||||
[{^user_id, data}] -> {:ok, data}
|
||||
[] -> {:ok, []}
|
||||
end
|
||||
end
|
||||
|
||||
def update_tracking_data(user_id, new_data) do
|
||||
:ets.insert(:character_tracking, {user_id, new_data})
|
||||
{:ok, new_data}
|
||||
end
|
||||
|
||||
def toggle_character_follow(user_id, character_id, follow_state) do
|
||||
case get_tracking_data(user_id) do
|
||||
{:ok, data} ->
|
||||
# Find the character and update its followed state
|
||||
updated_data = Enum.map(data, fn char ->
|
||||
if char.eve_id == character_id do
|
||||
%{char | followed: follow_state}
|
||||
else
|
||||
char
|
||||
end
|
||||
end)
|
||||
|
||||
# Update the storage
|
||||
update_tracking_data(user_id, updated_data)
|
||||
|
||||
# Return the updated character
|
||||
updated_char = Enum.find(updated_data, fn char -> char.eve_id == character_id end)
|
||||
{:ok, updated_char}
|
||||
|
||||
error -> error
|
||||
end
|
||||
end
|
||||
|
||||
def toggle_character_track(user_id, character_id, track_state) do
|
||||
case get_tracking_data(user_id) do
|
||||
{:ok, data} ->
|
||||
# Find the character and update its tracked state
|
||||
updated_data = Enum.map(data, fn char ->
|
||||
if char.eve_id == character_id do
|
||||
%{char | tracked: track_state}
|
||||
else
|
||||
char
|
||||
end
|
||||
end)
|
||||
|
||||
# Update the storage
|
||||
update_tracking_data(user_id, updated_data)
|
||||
|
||||
# Return the updated character
|
||||
updated_char = Enum.find(updated_data, fn char -> char.eve_id == character_id end)
|
||||
{:ok, updated_char}
|
||||
|
||||
error -> error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule MockTrackingUtils do
|
||||
def check_tracking_consistency(tracking_data) do
|
||||
# Log warnings for characters that are followed but not tracked
|
||||
inconsistent_chars = Enum.filter(tracking_data, fn char ->
|
||||
char[:followed] == true && char[:tracked] == false
|
||||
end)
|
||||
|
||||
if length(inconsistent_chars) > 0 do
|
||||
Enum.each(inconsistent_chars, fn char ->
|
||||
eve_id = Map.get(char, :eve_id, "unknown")
|
||||
name = Map.get(char, :name, "Unknown Character")
|
||||
IO.puts("WARNING: Inconsistent state detected - Character (ID: #{eve_id}, Name: #{name}) is followed but not tracked")
|
||||
end)
|
||||
end
|
||||
|
||||
# Return the original data unchanged
|
||||
tracking_data
|
||||
end
|
||||
end
|
||||
|
||||
# Mock controller that uses our mock dependencies
|
||||
defmodule MockCharacterAPIController do
|
||||
# Simplified version of toggle_follow from CharacterAPIController
|
||||
def toggle_follow(params, user_id) do
|
||||
with {:ok, character_id} <- MockUtil.require_param(params, "character_id"),
|
||||
{:ok, follow_str} <- MockUtil.require_param(params, "follow"),
|
||||
{:ok, follow} <- MockUtil.parse_bool(follow_str) do
|
||||
|
||||
case MockCharacterRepo.toggle_character_follow(user_id, character_id, follow) do
|
||||
{:ok, updated_char} ->
|
||||
# Get all tracking data to check consistency
|
||||
{:ok, all_tracking} = MockCharacterRepo.get_tracking_data(user_id)
|
||||
|
||||
# Check for inconsistencies (characters followed but not tracked)
|
||||
MockTrackingUtils.check_tracking_consistency(all_tracking)
|
||||
|
||||
# Return the updated character
|
||||
{:ok, %{data: updated_char}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, :internal_server_error, "Failed to update character: #{reason}"}
|
||||
end
|
||||
else
|
||||
{:error, msg} ->
|
||||
{:error, :bad_request, msg}
|
||||
end
|
||||
end
|
||||
|
||||
# Simplified version of toggle_track from CharacterAPIController
|
||||
def toggle_track(params, user_id) do
|
||||
with {:ok, character_id} <- MockUtil.require_param(params, "character_id"),
|
||||
{:ok, track_str} <- MockUtil.require_param(params, "track"),
|
||||
{:ok, track} <- MockUtil.parse_bool(track_str) do
|
||||
|
||||
# If we're untracking a character, we should also unfollow it
|
||||
result = if track == false do
|
||||
# First unfollow if needed
|
||||
MockCharacterRepo.toggle_character_follow(user_id, character_id, false)
|
||||
# Then untrack
|
||||
MockCharacterRepo.toggle_character_track(user_id, character_id, false)
|
||||
else
|
||||
# Just track
|
||||
MockCharacterRepo.toggle_character_track(user_id, character_id, true)
|
||||
end
|
||||
|
||||
case result do
|
||||
{:ok, updated_char} ->
|
||||
# Get all tracking data to check consistency
|
||||
{:ok, all_tracking} = MockCharacterRepo.get_tracking_data(user_id)
|
||||
|
||||
# Check for inconsistencies (characters followed but not tracked)
|
||||
MockTrackingUtils.check_tracking_consistency(all_tracking)
|
||||
|
||||
# Return the updated character
|
||||
{:ok, %{data: updated_char}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, :internal_server_error, "Failed to update character: #{reason}"}
|
||||
end
|
||||
else
|
||||
{:error, msg} ->
|
||||
{:error, :bad_request, msg}
|
||||
end
|
||||
end
|
||||
|
||||
# Simplified version of list_tracking from CharacterAPIController
|
||||
def list_tracking(user_id) do
|
||||
case MockCharacterRepo.get_tracking_data(user_id) do
|
||||
{:ok, tracking_data} ->
|
||||
# Check for inconsistencies
|
||||
checked_data = MockTrackingUtils.check_tracking_consistency(tracking_data)
|
||||
|
||||
# Return the data
|
||||
{:ok, %{data: checked_data}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, :internal_server_error, "Failed to get tracking data: #{reason}"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Setup for tests
|
||||
setup do
|
||||
# Initialize the mock storage
|
||||
MockCharacterRepo.init_storage()
|
||||
:ok
|
||||
end
|
||||
|
||||
describe "toggle_follow/2" do
|
||||
test "follows a character successfully" do
|
||||
params = %{"character_id" => "345678", "follow" => "true"}
|
||||
result = MockCharacterAPIController.toggle_follow(params, "user1")
|
||||
|
||||
assert {:ok, %{data: data}} = result
|
||||
assert data.eve_id == "345678"
|
||||
assert data.name == "Character Three"
|
||||
assert data.followed == true
|
||||
assert data.tracked == false
|
||||
|
||||
# This should have created an inconsistency (followed but not tracked)
|
||||
# The check_tracking_consistency function should have logged a warning
|
||||
end
|
||||
|
||||
test "unfollows a character successfully" do
|
||||
params = %{"character_id" => "123456", "follow" => "false"}
|
||||
result = MockCharacterAPIController.toggle_follow(params, "user1")
|
||||
|
||||
assert {:ok, %{data: data}} = result
|
||||
assert data.eve_id == "123456"
|
||||
assert data.followed == false
|
||||
assert data.tracked == true
|
||||
end
|
||||
|
||||
test "returns error when character_id is missing" do
|
||||
params = %{"follow" => "true"}
|
||||
result = MockCharacterAPIController.toggle_follow(params, "user1")
|
||||
|
||||
assert {:error, :bad_request, message} = result
|
||||
assert message == "Missing required param: character_id"
|
||||
end
|
||||
|
||||
test "returns error when follow is not a valid boolean" do
|
||||
params = %{"character_id" => "123456", "follow" => "not-a-boolean"}
|
||||
result = MockCharacterAPIController.toggle_follow(params, "user1")
|
||||
|
||||
assert {:error, :bad_request, message} = result
|
||||
assert message =~ "Invalid boolean value"
|
||||
end
|
||||
end
|
||||
|
||||
describe "toggle_track/2" do
|
||||
test "tracks a character successfully" do
|
||||
params = %{"character_id" => "345678", "track" => "true"}
|
||||
result = MockCharacterAPIController.toggle_track(params, "user1")
|
||||
|
||||
assert {:ok, %{data: data}} = result
|
||||
assert data.eve_id == "345678"
|
||||
assert data.tracked == true
|
||||
end
|
||||
|
||||
test "untracks and unfollows a character" do
|
||||
# First, make sure the character is followed
|
||||
follow_params = %{"character_id" => "123456", "follow" => "true"}
|
||||
MockCharacterAPIController.toggle_follow(follow_params, "user1")
|
||||
|
||||
# Now untrack the character
|
||||
params = %{"character_id" => "123456", "track" => "false"}
|
||||
result = MockCharacterAPIController.toggle_track(params, "user1")
|
||||
|
||||
assert {:ok, %{data: data}} = result
|
||||
assert data.eve_id == "123456"
|
||||
assert data.tracked == false
|
||||
assert data.followed == false # Should also be unfollowed
|
||||
end
|
||||
|
||||
test "returns error when character_id is missing" do
|
||||
params = %{"track" => "true"}
|
||||
result = MockCharacterAPIController.toggle_track(params, "user1")
|
||||
|
||||
assert {:error, :bad_request, message} = result
|
||||
assert message == "Missing required param: character_id"
|
||||
end
|
||||
|
||||
test "returns error when track is not a valid boolean" do
|
||||
params = %{"character_id" => "123456", "track" => "not-a-boolean"}
|
||||
result = MockCharacterAPIController.toggle_track(params, "user1")
|
||||
|
||||
assert {:error, :bad_request, message} = result
|
||||
assert message =~ "Invalid boolean value"
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_tracking/1" do
|
||||
test "returns tracking data for a user" do
|
||||
result = MockCharacterAPIController.list_tracking("user1")
|
||||
|
||||
assert {:ok, %{data: data}} = result
|
||||
assert length(data) == 3
|
||||
|
||||
# Check that the data contains the expected characters
|
||||
char_one = Enum.find(data, fn char -> char.eve_id == "123456" end)
|
||||
assert char_one.name == "Character One"
|
||||
assert char_one.tracked == true
|
||||
assert char_one.followed == true
|
||||
|
||||
char_two = Enum.find(data, fn char -> char.eve_id == "234567" end)
|
||||
assert char_two.name == "Character Two"
|
||||
assert char_two.tracked == true
|
||||
assert char_two.followed == false
|
||||
end
|
||||
|
||||
test "returns empty list for user with no tracking data" do
|
||||
result = MockCharacterAPIController.list_tracking("non-existent-user")
|
||||
|
||||
assert {:ok, %{data: data}} = result
|
||||
assert data == []
|
||||
end
|
||||
end
|
||||
end
|
||||
297
test/unit/common_api_controller_test.exs
Normal file
@@ -0,0 +1,297 @@
|
||||
# Standalone test for the CommonAPIController
|
||||
#
|
||||
# This file can be run directly with:
|
||||
# elixir test/standalone/common_api_controller_test.exs
|
||||
#
|
||||
# It doesn't require any database connections or external dependencies.
|
||||
|
||||
# Start ExUnit
|
||||
ExUnit.start()
|
||||
|
||||
defmodule CommonAPIControllerTest do
|
||||
use ExUnit.Case
|
||||
|
||||
# Mock modules to simulate the behavior of the controller's dependencies
|
||||
defmodule MockUtil do
|
||||
def require_param(params, key) do
|
||||
case params[key] do
|
||||
nil -> {:error, "Missing required param: #{key}"}
|
||||
"" -> {:error, "Param #{key} cannot be empty"}
|
||||
val -> {:ok, val}
|
||||
end
|
||||
end
|
||||
|
||||
def parse_int(str) do
|
||||
case Integer.parse(str) do
|
||||
{num, ""} -> {:ok, num}
|
||||
_ -> {:error, "Invalid integer for param id=#{str}"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule MockCachedInfo do
|
||||
def get_system_static_info(30000142) do
|
||||
{:ok, %{
|
||||
solar_system_id: 30000142,
|
||||
region_id: 10000002,
|
||||
constellation_id: 20000020,
|
||||
solar_system_name: "Jita",
|
||||
solar_system_name_lc: "jita",
|
||||
constellation_name: "Kimotoro",
|
||||
region_name: "The Forge",
|
||||
system_class: 0,
|
||||
security: "0.9",
|
||||
type_description: "High Security",
|
||||
class_title: "High Sec",
|
||||
is_shattered: false,
|
||||
effect_name: nil,
|
||||
effect_power: nil,
|
||||
statics: [],
|
||||
wandering: [],
|
||||
triglavian_invasion_status: nil,
|
||||
sun_type_id: 45041
|
||||
}}
|
||||
end
|
||||
|
||||
def get_system_static_info(31000005) do
|
||||
{:ok, %{
|
||||
solar_system_id: 31000005,
|
||||
region_id: 11000000,
|
||||
constellation_id: 21000000,
|
||||
solar_system_name: "J123456",
|
||||
solar_system_name_lc: "j123456",
|
||||
constellation_name: "Unknown",
|
||||
region_name: "Wormhole Space",
|
||||
system_class: 1,
|
||||
security: "-0.9",
|
||||
type_description: "Wormhole",
|
||||
class_title: "Class 1",
|
||||
is_shattered: false,
|
||||
effect_name: "Wolf-Rayet Star",
|
||||
effect_power: 1,
|
||||
statics: ["N110"],
|
||||
wandering: ["K162"],
|
||||
triglavian_invasion_status: nil,
|
||||
sun_type_id: 45042
|
||||
}}
|
||||
end
|
||||
|
||||
def get_system_static_info(_) do
|
||||
{:error, :not_found}
|
||||
end
|
||||
|
||||
def get_wormhole_types do
|
||||
{:ok, [
|
||||
%{
|
||||
name: "N110",
|
||||
dest: 1,
|
||||
lifetime: "16h",
|
||||
total_mass: 500000000,
|
||||
max_mass_per_jump: 20000000,
|
||||
mass_regen: 0
|
||||
}
|
||||
]}
|
||||
end
|
||||
|
||||
def get_wormhole_classes! do
|
||||
[
|
||||
%{
|
||||
id: 1,
|
||||
title: "Class 1 Wormhole",
|
||||
short_name: "C1"
|
||||
}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
# Mock controller that uses our mock dependencies
|
||||
defmodule MockCommonAPIController do
|
||||
# Simplified version of show_system_static from CommonAPIController
|
||||
def show_system_static(params) do
|
||||
with {:ok, solar_system_str} <- MockUtil.require_param(params, "id"),
|
||||
{:ok, solar_system_id} <- MockUtil.parse_int(solar_system_str) do
|
||||
case MockCachedInfo.get_system_static_info(solar_system_id) do
|
||||
{:ok, system} ->
|
||||
# Get basic system data
|
||||
data = static_system_to_json(system)
|
||||
|
||||
# Enhance with wormhole type information if statics exist
|
||||
enhanced_data = enhance_with_static_details(data)
|
||||
|
||||
# Return the enhanced data
|
||||
{:ok, %{data: enhanced_data}}
|
||||
|
||||
{:error, :not_found} ->
|
||||
{:error, :not_found, "System not found"}
|
||||
end
|
||||
else
|
||||
{:error, msg} ->
|
||||
{:error, :bad_request, msg}
|
||||
end
|
||||
end
|
||||
|
||||
# Helper function to convert a system to JSON format
|
||||
defp static_system_to_json(system) do
|
||||
system
|
||||
|> Map.take([
|
||||
:solar_system_id,
|
||||
:region_id,
|
||||
:constellation_id,
|
||||
:solar_system_name,
|
||||
:solar_system_name_lc,
|
||||
:constellation_name,
|
||||
:region_name,
|
||||
:system_class,
|
||||
:security,
|
||||
:type_description,
|
||||
:class_title,
|
||||
:is_shattered,
|
||||
:effect_name,
|
||||
:effect_power,
|
||||
:statics,
|
||||
:wandering,
|
||||
:triglavian_invasion_status,
|
||||
:sun_type_id
|
||||
])
|
||||
end
|
||||
|
||||
# Helper function to enhance system data with wormhole type information
|
||||
defp enhance_with_static_details(data) do
|
||||
if data[:statics] && length(data[:statics]) > 0 do
|
||||
# Add the enhanced static details to the response
|
||||
Map.put(data, :static_details, get_static_details(data[:statics]))
|
||||
else
|
||||
# No statics, return the original data
|
||||
data
|
||||
end
|
||||
end
|
||||
|
||||
# Helper function to get detailed information for each static wormhole
|
||||
defp get_static_details(statics) do
|
||||
# Get wormhole data from CachedInfo
|
||||
{:ok, wormhole_types} = MockCachedInfo.get_wormhole_types()
|
||||
wormhole_classes = MockCachedInfo.get_wormhole_classes!()
|
||||
|
||||
# Create a map of wormhole classes by ID for quick lookup
|
||||
classes_by_id = Enum.reduce(wormhole_classes, %{}, fn class, acc ->
|
||||
Map.put(acc, class.id, class)
|
||||
end)
|
||||
|
||||
# Find detailed information for each static
|
||||
Enum.map(statics, fn static_name ->
|
||||
# Find the wormhole type by name
|
||||
wh_type = Enum.find(wormhole_types, fn type -> type.name == static_name end)
|
||||
|
||||
if wh_type do
|
||||
create_wormhole_details(wh_type, classes_by_id)
|
||||
else
|
||||
create_fallback_wormhole_details(static_name)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
# Helper function to create detailed wormhole information
|
||||
defp create_wormhole_details(wh_type, classes_by_id) do
|
||||
# Get destination class info
|
||||
dest_class = Map.get(classes_by_id, wh_type.dest)
|
||||
|
||||
# Create enhanced static info
|
||||
%{
|
||||
name: wh_type.name,
|
||||
destination: %{
|
||||
id: to_string(wh_type.dest),
|
||||
name: (if dest_class, do: dest_class.title, else: wh_type.dest),
|
||||
short_name: (if dest_class, do: dest_class.short_name, else: wh_type.dest)
|
||||
},
|
||||
properties: %{
|
||||
lifetime: wh_type.lifetime,
|
||||
max_mass: wh_type.total_mass,
|
||||
max_jump_mass: wh_type.max_mass_per_jump,
|
||||
mass_regeneration: wh_type.mass_regen
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
# Helper function to create fallback information
|
||||
defp create_fallback_wormhole_details(static_name) do
|
||||
%{
|
||||
name: static_name,
|
||||
destination: %{
|
||||
id: nil,
|
||||
name: "Unknown",
|
||||
short_name: "?"
|
||||
},
|
||||
properties: %{
|
||||
lifetime: nil,
|
||||
max_mass: nil,
|
||||
max_jump_mass: nil,
|
||||
mass_regeneration: nil
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
describe "show_system_static/1" do
|
||||
test "returns system static info for a high-sec system" do
|
||||
params = %{"id" => "30000142"}
|
||||
result = MockCommonAPIController.show_system_static(params)
|
||||
|
||||
assert {:ok, %{data: data}} = result
|
||||
assert data.solar_system_id == 30000142
|
||||
assert data.solar_system_name == "Jita"
|
||||
assert data.region_name == "The Forge"
|
||||
assert data.security == "0.9"
|
||||
assert data.type_description == "High Security"
|
||||
refute Map.has_key?(data, :static_details)
|
||||
end
|
||||
|
||||
test "returns system static info with static details for a wormhole system" do
|
||||
params = %{"id" => "31000005"}
|
||||
result = MockCommonAPIController.show_system_static(params)
|
||||
|
||||
assert {:ok, %{data: data}} = result
|
||||
assert data.solar_system_id == 31000005
|
||||
assert data.solar_system_name == "J123456"
|
||||
assert data.region_name == "Wormhole Space"
|
||||
assert data.system_class == 1
|
||||
assert data.security == "-0.9"
|
||||
assert data.type_description == "Wormhole"
|
||||
assert data.effect_name == "Wolf-Rayet Star"
|
||||
|
||||
# Check static details
|
||||
assert Map.has_key?(data, :static_details)
|
||||
assert length(data.static_details) == 1
|
||||
|
||||
static = List.first(data.static_details)
|
||||
assert static.name == "N110"
|
||||
assert static.destination.id == "1"
|
||||
assert static.destination.name == "Class 1 Wormhole"
|
||||
assert static.destination.short_name == "C1"
|
||||
assert static.properties.lifetime == "16h"
|
||||
assert static.properties.max_mass == 500000000
|
||||
end
|
||||
|
||||
test "returns error when system is not found" do
|
||||
params = %{"id" => "99999999"}
|
||||
result = MockCommonAPIController.show_system_static(params)
|
||||
|
||||
assert {:error, :not_found, "System not found"} = result
|
||||
end
|
||||
|
||||
test "returns error when system_id is not provided" do
|
||||
params = %{}
|
||||
result = MockCommonAPIController.show_system_static(params)
|
||||
|
||||
assert {:error, :bad_request, message} = result
|
||||
assert message == "Missing required param: id"
|
||||
end
|
||||
|
||||
test "returns error when system_id is not a valid integer" do
|
||||
params = %{"id" => "not-an-integer"}
|
||||
result = MockCommonAPIController.show_system_static(params)
|
||||
|
||||
assert {:error, :bad_request, message} = result
|
||||
assert message =~ "Invalid integer for param id"
|
||||
end
|
||||
end
|
||||
end
|
||||
213
test/unit/map_api_controller_test.exs
Normal file
@@ -0,0 +1,213 @@
|
||||
# Standalone test for the MapAPIController
|
||||
#
|
||||
# This file can be run directly with:
|
||||
# elixir test/standalone/map_api_controller_test.exs
|
||||
#
|
||||
# It doesn't require any database connections or external dependencies.
|
||||
|
||||
# Start ExUnit
|
||||
ExUnit.start()
|
||||
|
||||
defmodule MapAPIControllerTest do
|
||||
use ExUnit.Case
|
||||
|
||||
# Mock modules to simulate the behavior of the controller's dependencies
|
||||
defmodule MockUtil do
|
||||
def require_param(params, key) do
|
||||
case params[key] do
|
||||
nil -> {:error, "Missing required param: #{key}"}
|
||||
"" -> {:error, "Param #{key} cannot be empty"}
|
||||
val -> {:ok, val}
|
||||
end
|
||||
end
|
||||
|
||||
def parse_int(str) do
|
||||
case Integer.parse(str) do
|
||||
{num, ""} -> {:ok, num}
|
||||
_ -> {:error, "Invalid integer for param id=#{str}"}
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_map_id(params) do
|
||||
cond do
|
||||
params["map_id"] ->
|
||||
case parse_int(params["map_id"]) do
|
||||
{:ok, map_id} -> {:ok, map_id}
|
||||
{:error, _} -> {:error, "Invalid map_id format"}
|
||||
end
|
||||
params["slug"] ->
|
||||
# In a real app, this would look up the map by slug
|
||||
# For testing, we'll just use a simple mapping
|
||||
case params["slug"] do
|
||||
"test-map" -> {:ok, 1}
|
||||
"another-map" -> {:ok, 2}
|
||||
_ -> {:error, "Map not found"}
|
||||
end
|
||||
true ->
|
||||
{:error, "Missing required param: map_id or slug"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule MockMapSystemRepo do
|
||||
def get_visible_systems_by_map_id(1) do
|
||||
[
|
||||
%{id: 30000142, name: "Jita", security: 0.9, region_id: 10000002},
|
||||
%{id: 30002659, name: "Dodixie", security: 0.9, region_id: 10000032},
|
||||
%{id: 30002187, name: "Amarr", security: 1.0, region_id: 10000043}
|
||||
]
|
||||
end
|
||||
|
||||
def get_visible_systems_by_map_id(_) do
|
||||
[]
|
||||
end
|
||||
|
||||
def get_system_by_id(1, 30000142) do
|
||||
%{id: 30000142, name: "Jita", security: 0.9, region_id: 10000002}
|
||||
end
|
||||
|
||||
def get_system_by_id(1, 30002659) do
|
||||
%{id: 30002659, name: "Dodixie", security: 0.9, region_id: 10000032}
|
||||
end
|
||||
|
||||
def get_system_by_id(1, 30002187) do
|
||||
%{id: 30002187, name: "Amarr", security: 1.0, region_id: 10000043}
|
||||
end
|
||||
|
||||
def get_system_by_id(_, _) do
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defmodule MockMapSolarSystem do
|
||||
def get_name_by_id(30000142), do: "Jita"
|
||||
def get_name_by_id(30002659), do: "Dodixie"
|
||||
def get_name_by_id(30002187), do: "Amarr"
|
||||
def get_name_by_id(_), do: nil
|
||||
end
|
||||
|
||||
# Mock controller that uses our mock dependencies
|
||||
defmodule MockMapAPIController do
|
||||
# Simplified version of list_systems from MapAPIController
|
||||
def list_systems(params) do
|
||||
with {:ok, map_id} <- MockUtil.fetch_map_id(params) do
|
||||
systems = MockMapSystemRepo.get_visible_systems_by_map_id(map_id)
|
||||
|
||||
if systems == [] do
|
||||
{:error, :not_found, "No systems found for this map"}
|
||||
else
|
||||
# Format the response
|
||||
formatted_systems = Enum.map(systems, fn system ->
|
||||
%{
|
||||
id: system.id,
|
||||
name: system.name,
|
||||
security: system.security
|
||||
}
|
||||
end)
|
||||
|
||||
{:ok, %{data: formatted_systems}}
|
||||
end
|
||||
else
|
||||
{:error, msg} ->
|
||||
{:error, :bad_request, msg}
|
||||
end
|
||||
end
|
||||
|
||||
# Simplified version of show_system from MapAPIController
|
||||
def show_system(params) do
|
||||
with {:ok, map_id} <- MockUtil.fetch_map_id(params),
|
||||
{:ok, system_id_str} <- MockUtil.require_param(params, "id"),
|
||||
{:ok, system_id} <- MockUtil.parse_int(system_id_str) do
|
||||
|
||||
system = MockMapSystemRepo.get_system_by_id(map_id, system_id)
|
||||
|
||||
if system == nil do
|
||||
{:error, :not_found, "System not found"}
|
||||
else
|
||||
# Format the response
|
||||
formatted_system = %{
|
||||
id: system.id,
|
||||
name: system.name,
|
||||
security: system.security
|
||||
}
|
||||
|
||||
{:ok, %{data: formatted_system}}
|
||||
end
|
||||
else
|
||||
{:error, msg} ->
|
||||
{:error, :bad_request, msg}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_systems/1" do
|
||||
test "returns systems with valid map_id" do
|
||||
params = %{"map_id" => "1"}
|
||||
result = MockMapAPIController.list_systems(params)
|
||||
|
||||
assert {:ok, %{data: data}} = result
|
||||
assert length(data) == 3
|
||||
|
||||
# Check that the data contains the expected systems
|
||||
jita = Enum.find(data, fn system -> system.id == 30000142 end)
|
||||
assert jita.name == "Jita"
|
||||
assert jita.security == 0.9
|
||||
|
||||
dodixie = Enum.find(data, fn system -> system.id == 30002659 end)
|
||||
assert dodixie.name == "Dodixie"
|
||||
assert dodixie.security == 0.9
|
||||
end
|
||||
|
||||
test "returns systems with valid slug" do
|
||||
params = %{"slug" => "test-map"}
|
||||
result = MockMapAPIController.list_systems(params)
|
||||
|
||||
assert {:ok, %{data: data}} = result
|
||||
assert length(data) == 3
|
||||
end
|
||||
|
||||
test "returns error when no systems found" do
|
||||
params = %{"map_id" => "2"}
|
||||
result = MockMapAPIController.list_systems(params)
|
||||
|
||||
assert {:error, :not_found, message} = result
|
||||
assert message == "No systems found for this map"
|
||||
end
|
||||
|
||||
test "returns error when map_id is missing" do
|
||||
params = %{}
|
||||
result = MockMapAPIController.list_systems(params)
|
||||
|
||||
assert {:error, :bad_request, message} = result
|
||||
assert message == "Missing required param: map_id or slug"
|
||||
end
|
||||
|
||||
test "returns error when invalid map_id is provided" do
|
||||
params = %{"slug" => "non-existent-map"}
|
||||
result = MockMapAPIController.list_systems(params)
|
||||
|
||||
assert {:error, :bad_request, message} = result
|
||||
assert message == "Map not found"
|
||||
end
|
||||
end
|
||||
|
||||
describe "show_system/1" do
|
||||
test "returns system with valid parameters" do
|
||||
params = %{"map_id" => "1", "id" => "30000142"}
|
||||
result = MockMapAPIController.show_system(params)
|
||||
|
||||
assert {:ok, %{data: data}} = result
|
||||
assert data.id == 30000142
|
||||
assert data.name == "Jita"
|
||||
assert data.security == 0.9
|
||||
end
|
||||
|
||||
test "returns error when system is not found" do
|
||||
params = %{"map_id" => "1", "id" => "99999999"}
|
||||
result = MockMapAPIController.show_system(params)
|
||||
|
||||
assert {:error, :not_found, message} = result
|
||||
assert message == "System not found"
|
||||
end
|
||||
end
|
||||
end
|
||||
321
test/unit/map_route_api_controller_test.exs
Normal file
@@ -0,0 +1,321 @@
|
||||
# Standalone test for the MapAPIController route functionality
|
||||
#
|
||||
# This file can be run directly with:
|
||||
# elixir test/standalone/map_route_api_controller_test.exs
|
||||
#
|
||||
# It doesn't require any database connections or external dependencies.
|
||||
|
||||
# Start ExUnit
|
||||
ExUnit.start()
|
||||
|
||||
defmodule MapRouteAPIControllerTest do
|
||||
use ExUnit.Case
|
||||
|
||||
# Mock modules to simulate the behavior of the controller's dependencies
|
||||
defmodule MockUtil do
|
||||
def require_param(params, key) do
|
||||
case params[key] do
|
||||
nil -> {:error, "Missing required param: #{key}"}
|
||||
"" -> {:error, "Param #{key} cannot be empty"}
|
||||
val -> {:ok, val}
|
||||
end
|
||||
end
|
||||
|
||||
def parse_int(str) do
|
||||
case Integer.parse(str) do
|
||||
{num, ""} -> {:ok, num}
|
||||
_ -> {:error, "Invalid integer for param id=#{str}"}
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_map_id(params) do
|
||||
cond do
|
||||
params["map_id"] ->
|
||||
case parse_int(params["map_id"]) do
|
||||
{:ok, map_id} -> {:ok, map_id}
|
||||
{:error, _} -> {:error, "Invalid map_id format"}
|
||||
end
|
||||
params["slug"] ->
|
||||
# In a real app, this would look up the map by slug
|
||||
# For testing, we'll just use a simple mapping
|
||||
case params["slug"] do
|
||||
"test-map" -> {:ok, 1}
|
||||
"another-map" -> {:ok, 2}
|
||||
_ -> {:error, "Map not found"}
|
||||
end
|
||||
true ->
|
||||
{:error, "Missing required param: map_id or slug"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule MockMapSystemRepo do
|
||||
# Mock data for systems
|
||||
def get_systems_by_ids(map_id, system_ids) when map_id == 1 do
|
||||
systems = %{
|
||||
30000142 => %{id: 30000142, name: "Jita", security: 0.9, region_id: 10000002},
|
||||
30002659 => %{id: 30002659, name: "Dodixie", security: 0.9, region_id: 10000032},
|
||||
30002187 => %{id: 30002187, name: "Amarr", security: 1.0, region_id: 10000043}
|
||||
}
|
||||
|
||||
Enum.map(system_ids, fn id -> Map.get(systems, id) end)
|
||||
|> Enum.filter(&(&1 != nil))
|
||||
end
|
||||
|
||||
def get_systems_by_ids(_, _), do: []
|
||||
|
||||
# Mock data for connections
|
||||
def get_connections_between(map_id, _system_ids) when map_id == 1 do
|
||||
[
|
||||
%{source_id: 30000142, target_id: 30002659, distance: 15},
|
||||
%{source_id: 30002659, target_id: 30002187, distance: 12},
|
||||
%{source_id: 30000142, target_id: 30002187, distance: 20}
|
||||
]
|
||||
end
|
||||
|
||||
def get_connections_between(_, _), do: []
|
||||
end
|
||||
|
||||
defmodule MockRouteCalculator do
|
||||
# Simplified route calculator that just returns a predefined route
|
||||
def calculate_route(systems, _connections, source_id, target_id, _options \\ []) do
|
||||
cond do
|
||||
source_id == 30000142 and target_id == 30002187 ->
|
||||
# Direct route from Jita to Amarr
|
||||
route = [
|
||||
Enum.find(systems, fn s -> s.id == 30000142 end),
|
||||
Enum.find(systems, fn s -> s.id == 30002187 end)
|
||||
]
|
||||
{:ok, %{route: route, jumps: 1, distance: 20}}
|
||||
|
||||
source_id == 30000142 and target_id == 30002659 ->
|
||||
# Direct route from Jita to Dodixie
|
||||
route = [
|
||||
Enum.find(systems, fn s -> s.id == 30000142 end),
|
||||
Enum.find(systems, fn s -> s.id == 30002659 end)
|
||||
]
|
||||
{:ok, %{route: route, jumps: 1, distance: 15}}
|
||||
|
||||
source_id == 30002659 and target_id == 30002187 ->
|
||||
# Direct route from Dodixie to Amarr
|
||||
route = [
|
||||
Enum.find(systems, fn s -> s.id == 30002659 end),
|
||||
Enum.find(systems, fn s -> s.id == 30002187 end)
|
||||
]
|
||||
{:ok, %{route: route, jumps: 1, distance: 12}}
|
||||
|
||||
source_id == 30002659 and target_id == 30000142 ->
|
||||
# Direct route from Dodixie to Jita
|
||||
route = [
|
||||
Enum.find(systems, fn s -> s.id == 30002659 end),
|
||||
Enum.find(systems, fn s -> s.id == 30000142 end)
|
||||
]
|
||||
{:ok, %{route: route, jumps: 1, distance: 15}}
|
||||
|
||||
true ->
|
||||
{:error, "No route found"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Mock controller that uses our mock dependencies
|
||||
defmodule MockMapAPIController do
|
||||
# Simplified version of calculate_route from MapAPIController
|
||||
def calculate_route(params) do
|
||||
with {:ok, map_id} <- MockUtil.fetch_map_id(params),
|
||||
{:ok, source_id_str} <- MockUtil.require_param(params, "source"),
|
||||
{:ok, source_id} <- MockUtil.parse_int(source_id_str),
|
||||
{:ok, target_id_str} <- MockUtil.require_param(params, "target"),
|
||||
{:ok, target_id} <- MockUtil.parse_int(target_id_str) do
|
||||
|
||||
# Get the systems involved in the route
|
||||
systems = MockMapSystemRepo.get_systems_by_ids(map_id, [source_id, target_id])
|
||||
|
||||
# Check if both systems exist
|
||||
source_system = Enum.find(systems, fn s -> s.id == source_id end)
|
||||
target_system = Enum.find(systems, fn s -> s.id == target_id end)
|
||||
|
||||
if source_system == nil do
|
||||
{:error, :not_found, "Source system not found"}
|
||||
else
|
||||
if target_system == nil do
|
||||
{:error, :not_found, "Target system not found"}
|
||||
else
|
||||
# Get connections between systems
|
||||
connections = MockMapSystemRepo.get_connections_between(map_id, [source_id, target_id])
|
||||
|
||||
# Calculate the route
|
||||
case MockRouteCalculator.calculate_route(systems, connections, source_id, target_id) do
|
||||
{:ok, route_data} ->
|
||||
# Format the response
|
||||
formatted_route = format_route_response(route_data)
|
||||
{:ok, %{data: formatted_route}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, :not_found, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
{:error, msg} ->
|
||||
{:error, :bad_request, msg}
|
||||
end
|
||||
end
|
||||
|
||||
# Helper function to format the route response
|
||||
defp format_route_response(route_data) do
|
||||
%{
|
||||
route: Enum.map(route_data.route, fn system ->
|
||||
%{
|
||||
id: system.id,
|
||||
name: system.name,
|
||||
security: system.security
|
||||
}
|
||||
end),
|
||||
jumps: route_data.jumps,
|
||||
distance: route_data.distance
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
describe "calculate_route/1" do
|
||||
test "calculates route between two systems successfully" do
|
||||
params = %{
|
||||
"map_id" => "1",
|
||||
"source" => "30000142", # Jita
|
||||
"target" => "30002187" # Amarr
|
||||
}
|
||||
|
||||
result = MockMapAPIController.calculate_route(params)
|
||||
|
||||
assert {:ok, %{data: data}} = result
|
||||
assert length(data.route) == 2
|
||||
assert Enum.at(data.route, 0).id == 30000142
|
||||
assert Enum.at(data.route, 0).name == "Jita"
|
||||
assert Enum.at(data.route, 1).id == 30002187
|
||||
assert Enum.at(data.route, 1).name == "Amarr"
|
||||
assert data.jumps == 1
|
||||
assert data.distance == 20
|
||||
end
|
||||
|
||||
test "calculates route using map slug" do
|
||||
params = %{
|
||||
"slug" => "test-map",
|
||||
"source" => "30000142", # Jita
|
||||
"target" => "30002659" # Dodixie
|
||||
}
|
||||
|
||||
result = MockMapAPIController.calculate_route(params)
|
||||
|
||||
assert {:ok, %{data: data}} = result
|
||||
assert length(data.route) == 2
|
||||
assert Enum.at(data.route, 0).id == 30000142
|
||||
assert Enum.at(data.route, 0).name == "Jita"
|
||||
assert Enum.at(data.route, 1).id == 30002659
|
||||
assert Enum.at(data.route, 1).name == "Dodixie"
|
||||
assert data.jumps == 1
|
||||
assert data.distance == 15
|
||||
end
|
||||
|
||||
test "returns error when source system is not found" do
|
||||
params = %{
|
||||
"map_id" => "1",
|
||||
"source" => "99999999", # Non-existent system
|
||||
"target" => "30002187" # Amarr
|
||||
}
|
||||
|
||||
result = MockMapAPIController.calculate_route(params)
|
||||
|
||||
assert {:error, :not_found, message} = result
|
||||
assert message == "Source system not found"
|
||||
end
|
||||
|
||||
test "returns error when target system is not found" do
|
||||
params = %{
|
||||
"map_id" => "1",
|
||||
"source" => "30000142", # Jita
|
||||
"target" => "99999999" # Non-existent system
|
||||
}
|
||||
|
||||
result = MockMapAPIController.calculate_route(params)
|
||||
|
||||
assert {:error, :not_found, message} = result
|
||||
assert message == "Target system not found"
|
||||
end
|
||||
|
||||
test "returns error when map is not found" do
|
||||
params = %{
|
||||
"slug" => "non-existent-map",
|
||||
"source" => "30000142", # Jita
|
||||
"target" => "30002187" # Amarr
|
||||
}
|
||||
|
||||
result = MockMapAPIController.calculate_route(params)
|
||||
|
||||
assert {:error, :bad_request, message} = result
|
||||
assert message == "Map not found"
|
||||
end
|
||||
|
||||
test "returns error when source parameter is missing" do
|
||||
params = %{
|
||||
"map_id" => "1",
|
||||
"target" => "30002187" # Amarr
|
||||
}
|
||||
|
||||
result = MockMapAPIController.calculate_route(params)
|
||||
|
||||
assert {:error, :bad_request, message} = result
|
||||
assert message == "Missing required param: source"
|
||||
end
|
||||
|
||||
test "returns error when target parameter is missing" do
|
||||
params = %{
|
||||
"map_id" => "1",
|
||||
"source" => "30000142" # Jita
|
||||
}
|
||||
|
||||
result = MockMapAPIController.calculate_route(params)
|
||||
|
||||
assert {:error, :bad_request, message} = result
|
||||
assert message == "Missing required param: target"
|
||||
end
|
||||
|
||||
test "returns error when map_id and slug are both missing" do
|
||||
params = %{
|
||||
"source" => "30000142", # Jita
|
||||
"target" => "30002187" # Amarr
|
||||
}
|
||||
|
||||
result = MockMapAPIController.calculate_route(params)
|
||||
|
||||
assert {:error, :bad_request, message} = result
|
||||
assert message == "Missing required param: map_id or slug"
|
||||
end
|
||||
|
||||
test "returns error when source is not a valid integer" do
|
||||
params = %{
|
||||
"map_id" => "1",
|
||||
"source" => "not-an-integer",
|
||||
"target" => "30002187" # Amarr
|
||||
}
|
||||
|
||||
result = MockMapAPIController.calculate_route(params)
|
||||
|
||||
assert {:error, :bad_request, message} = result
|
||||
assert message =~ "Invalid integer for param id"
|
||||
end
|
||||
|
||||
test "returns error when target is not a valid integer" do
|
||||
params = %{
|
||||
"map_id" => "1",
|
||||
"source" => "30000142", # Jita
|
||||
"target" => "not-an-integer"
|
||||
}
|
||||
|
||||
result = MockMapAPIController.calculate_route(params)
|
||||
|
||||
assert {:error, :bad_request, message} = result
|
||||
assert message =~ "Invalid integer for param id"
|
||||
end
|
||||
end
|
||||
end
|
||||
332
test/unit/tracking_utils_test.exs
Normal file
@@ -0,0 +1,332 @@
|
||||
# Test for the check_tracking_consistency function in WandererApp.Character.TrackingUtils
|
||||
#
|
||||
# This file can be run directly with:
|
||||
# elixir test/unit/tracking_consistency_test.exs
|
||||
#
|
||||
# It doesn't require any database connections or external dependencies.
|
||||
|
||||
# Start ExUnit
|
||||
ExUnit.start()
|
||||
|
||||
defmodule WandererApp.Character.TrackingConsistencyTest do
|
||||
use ExUnit.Case
|
||||
require Logger
|
||||
import ExUnit.CaptureIO
|
||||
|
||||
# This is a copy of the function from TrackingUtils
|
||||
def check_tracking_consistency(tracking_data) do
|
||||
# Find any characters that are followed but not tracked
|
||||
inconsistent_characters = Enum.filter(tracking_data, fn data ->
|
||||
data.followed && !data.tracked
|
||||
end)
|
||||
|
||||
# Log a warning for each inconsistent character
|
||||
Enum.each(inconsistent_characters, fn data ->
|
||||
character = data.character
|
||||
# Use IO.puts instead of Logger to avoid dependencies
|
||||
eve_id = Map.get(character, :eve_id, "unknown")
|
||||
name = Map.get(character, :name, "unknown")
|
||||
IO.puts("WARNING: Inconsistent state detected: Character is followed but not tracked. Character ID: #{eve_id}, Name: #{name}")
|
||||
end)
|
||||
|
||||
# Return the original tracking data
|
||||
tracking_data
|
||||
end
|
||||
|
||||
describe "check_tracking_consistency/1" do
|
||||
test "logs a warning when a character is followed but not tracked" do
|
||||
# Create test data with inconsistent state
|
||||
tracking_data = [
|
||||
%{
|
||||
character: %{eve_id: "test-eve-id", name: "Test Character"},
|
||||
tracked: false,
|
||||
followed: true
|
||||
}
|
||||
]
|
||||
|
||||
# Call the function and capture output
|
||||
output = capture_io(fn ->
|
||||
check_tracking_consistency(tracking_data)
|
||||
end)
|
||||
|
||||
# Assert that the warning was logged
|
||||
assert output =~ "Inconsistent state detected: Character is followed but not tracked"
|
||||
assert output =~ "test-eve-id"
|
||||
assert output =~ "Test Character"
|
||||
end
|
||||
|
||||
test "does not log a warning when all followed characters are also tracked" do
|
||||
# Create test data with consistent state
|
||||
tracking_data = [
|
||||
%{
|
||||
character: %{eve_id: "test-eve-id", name: "Test Character"},
|
||||
tracked: true,
|
||||
followed: true
|
||||
}
|
||||
]
|
||||
|
||||
# Call the function and capture output
|
||||
output = capture_io(fn ->
|
||||
check_tracking_consistency(tracking_data)
|
||||
end)
|
||||
|
||||
# Assert that no warning was logged
|
||||
refute output =~ "Inconsistent state detected"
|
||||
end
|
||||
|
||||
test "does not log a warning when no characters are followed" do
|
||||
# Create test data with no followed characters
|
||||
tracking_data = [
|
||||
%{
|
||||
character: %{eve_id: "test-eve-id", name: "Test Character"},
|
||||
tracked: true,
|
||||
followed: false
|
||||
}
|
||||
]
|
||||
|
||||
# Call the function and capture output
|
||||
output = capture_io(fn ->
|
||||
check_tracking_consistency(tracking_data)
|
||||
end)
|
||||
|
||||
# Assert that no warning was logged
|
||||
refute output =~ "Inconsistent state detected"
|
||||
end
|
||||
|
||||
test "handles multiple characters with mixed states correctly" do
|
||||
# Create test data with multiple characters in different states
|
||||
tracking_data = [
|
||||
%{
|
||||
character: %{eve_id: "character-1", name: "Character 1"},
|
||||
tracked: true,
|
||||
followed: true
|
||||
},
|
||||
%{
|
||||
character: %{eve_id: "character-2", name: "Character 2"},
|
||||
tracked: false,
|
||||
followed: true
|
||||
},
|
||||
%{
|
||||
character: %{eve_id: "character-3", name: "Character 3"},
|
||||
tracked: true,
|
||||
followed: false
|
||||
},
|
||||
%{
|
||||
character: %{eve_id: "character-4", name: "Character 4"},
|
||||
tracked: false,
|
||||
followed: false
|
||||
}
|
||||
]
|
||||
|
||||
# Call the function and capture output
|
||||
output = capture_io(fn ->
|
||||
check_tracking_consistency(tracking_data)
|
||||
end)
|
||||
|
||||
# Assert that only the inconsistent character triggered a warning
|
||||
assert output =~ "Inconsistent state detected: Character is followed but not tracked"
|
||||
assert output =~ "character-2"
|
||||
assert output =~ "Character 2"
|
||||
refute output =~ "character-1"
|
||||
refute output =~ "character-3"
|
||||
refute output =~ "character-4"
|
||||
end
|
||||
|
||||
test "returns the original tracking data unchanged" do
|
||||
# Create test data
|
||||
tracking_data = [
|
||||
%{
|
||||
character: %{eve_id: "test-eve-id", name: "Test Character"},
|
||||
tracked: false,
|
||||
followed: true
|
||||
}
|
||||
]
|
||||
|
||||
# Call the function and get the result
|
||||
result = check_tracking_consistency(tracking_data)
|
||||
|
||||
# Assert that the returned data is the same as the input data
|
||||
assert result == tracking_data
|
||||
end
|
||||
|
||||
test "handles empty tracking data without errors" do
|
||||
# Create empty tracking data
|
||||
tracking_data = []
|
||||
|
||||
# Call the function and capture output
|
||||
output = capture_io(fn ->
|
||||
result = check_tracking_consistency(tracking_data)
|
||||
# Assert that the returned data is the same as the input data
|
||||
assert result == tracking_data
|
||||
end)
|
||||
|
||||
# Assert that no warning was logged
|
||||
refute output =~ "Inconsistent state detected"
|
||||
end
|
||||
|
||||
test "handles multiple inconsistent characters correctly" do
|
||||
# Create test data with multiple inconsistent characters
|
||||
tracking_data = [
|
||||
%{
|
||||
character: %{eve_id: "character-1", name: "Character 1"},
|
||||
tracked: false,
|
||||
followed: true
|
||||
},
|
||||
%{
|
||||
character: %{eve_id: "character-2", name: "Character 2"},
|
||||
tracked: false,
|
||||
followed: true
|
||||
},
|
||||
%{
|
||||
character: %{eve_id: "character-3", name: "Character 3"},
|
||||
tracked: true,
|
||||
followed: true
|
||||
}
|
||||
]
|
||||
|
||||
# Call the function and capture output
|
||||
output = capture_io(fn ->
|
||||
check_tracking_consistency(tracking_data)
|
||||
end)
|
||||
|
||||
# Assert that warnings were logged for both inconsistent characters
|
||||
assert output =~ "Character ID: character-1"
|
||||
assert output =~ "Name: Character 1"
|
||||
assert output =~ "Character ID: character-2"
|
||||
assert output =~ "Name: Character 2"
|
||||
refute output =~ "Character ID: character-3"
|
||||
end
|
||||
|
||||
test "handles characters with missing fields gracefully" do
|
||||
# Create test data with missing fields
|
||||
tracking_data = [
|
||||
%{
|
||||
character: %{eve_id: "character-1"}, # Missing name
|
||||
tracked: false,
|
||||
followed: true
|
||||
},
|
||||
%{
|
||||
character: %{name: "Character 2"}, # Missing eve_id
|
||||
tracked: false,
|
||||
followed: true
|
||||
}
|
||||
]
|
||||
|
||||
# Call the function and capture output
|
||||
output = capture_io(fn ->
|
||||
result = check_tracking_consistency(tracking_data)
|
||||
# Assert that the returned data is the same as the input data
|
||||
assert result == tracking_data
|
||||
end)
|
||||
|
||||
# Assert that warnings were logged with available information
|
||||
assert output =~ "Character ID: character-1"
|
||||
assert output =~ "Name: unknown"
|
||||
assert output =~ "Character ID: unknown"
|
||||
assert output =~ "Name: Character 2"
|
||||
end
|
||||
|
||||
test "handles characters with nil tracked or followed values" do
|
||||
# Create test data with nil values
|
||||
tracking_data = [
|
||||
%{
|
||||
character: %{eve_id: "character-1", name: "Character 1"},
|
||||
tracked: nil,
|
||||
followed: true
|
||||
},
|
||||
%{
|
||||
character: %{eve_id: "character-2", name: "Character 2"},
|
||||
tracked: false,
|
||||
followed: nil
|
||||
}
|
||||
]
|
||||
|
||||
# Call the function and capture output
|
||||
output = capture_io(fn ->
|
||||
result = check_tracking_consistency(tracking_data)
|
||||
# Assert that the returned data is the same as the input data
|
||||
assert result == tracking_data
|
||||
end)
|
||||
|
||||
# Assert that a warning was logged for the first character (nil tracked is treated as false)
|
||||
assert output =~ "Character ID: character-1"
|
||||
assert output =~ "Name: Character 1"
|
||||
# No warning for the second character (nil followed is treated as false)
|
||||
refute output =~ "Character ID: character-2"
|
||||
end
|
||||
|
||||
test "handles malformed tracking data gracefully" do
|
||||
# Create malformed tracking data (missing required fields)
|
||||
tracking_data = [
|
||||
%{
|
||||
# Missing character field
|
||||
tracked: false,
|
||||
followed: true
|
||||
}
|
||||
]
|
||||
|
||||
# Call the function and capture output, expecting it to handle errors gracefully
|
||||
assert_raise(KeyError, fn ->
|
||||
check_tracking_consistency(tracking_data)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
# Additional tests for edge cases in the filter logic
|
||||
describe "filter logic in check_tracking_consistency/1" do
|
||||
test "correctly identifies characters that are followed but not tracked" do
|
||||
# Create test data with various combinations
|
||||
tracking_data = [
|
||||
%{
|
||||
character: %{eve_id: "char-1", name: "Character 1"},
|
||||
tracked: false,
|
||||
followed: true
|
||||
},
|
||||
%{
|
||||
character: %{eve_id: "char-2", name: "Character 2"},
|
||||
tracked: true,
|
||||
followed: true
|
||||
},
|
||||
%{
|
||||
character: %{eve_id: "char-3", name: "Character 3"},
|
||||
tracked: false,
|
||||
followed: false
|
||||
},
|
||||
%{
|
||||
character: %{eve_id: "char-4", name: "Character 4"},
|
||||
tracked: true,
|
||||
followed: false
|
||||
}
|
||||
]
|
||||
|
||||
# Extract the filter logic from the function
|
||||
inconsistent_characters = Enum.filter(tracking_data, fn data ->
|
||||
data.followed && !data.tracked
|
||||
end)
|
||||
|
||||
# Assert that only the first character is identified as inconsistent
|
||||
assert length(inconsistent_characters) == 1
|
||||
assert hd(inconsistent_characters).character.eve_id == "char-1"
|
||||
end
|
||||
|
||||
test "handles boolean-like values correctly in filter logic" do
|
||||
# Create test data with various boolean-like values
|
||||
tracking_data = [
|
||||
%{
|
||||
character: %{eve_id: "char-1", name: "Character 1"},
|
||||
tracked: false,
|
||||
followed: "true" # String instead of boolean - in Elixir, only false and nil are falsy
|
||||
}
|
||||
]
|
||||
|
||||
# Extract the filter logic from the function
|
||||
inconsistent_characters = Enum.filter(tracking_data, fn data ->
|
||||
data.followed && !data.tracked
|
||||
end)
|
||||
|
||||
# Assert that the character is identified as inconsistent
|
||||
# (since in Elixir, only false and nil are falsy, everything else is truthy)
|
||||
assert length(inconsistent_characters) == 1
|
||||
end
|
||||
end
|
||||
end
|
||||
143
test/unit/util_api_controller_test.exs
Normal file
@@ -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
|
||||