Compare commits
1 Commits
codex/issu
...
0690370197
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0690370197 |
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -1 +1 @@
|
||||
* @calesthio
|
||||
* @MrSphay
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,5 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Security report
|
||||
url: mailto:celesthioailabs@gmail.com
|
||||
url: https://git.wilkensxl.de/MrSphay/intelligence-terminal
|
||||
about: Report security issues privately instead of opening a public issue.
|
||||
|
||||
68
.github/workflows/docker-publish.yml
vendored
68
.github/workflows/docker-publish.yml
vendored
@@ -1,68 +0,0 @@
|
||||
name: Build & Publish Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
tags: ['v*']
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GHCR
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
if: github.event_name != 'pull_request' && vars.DOCKERHUB_ENABLED == 'true'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
${{ vars.DOCKERHUB_ENABLED == 'true' && format('{0}/{1}', secrets.DOCKERHUB_USERNAME, 'crucix') || '' }}
|
||||
tags: |
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=sha,prefix=
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -1,6 +1,6 @@
|
||||
# Contributing to Crucix
|
||||
# Contributing to Intelligence Terminal
|
||||
|
||||
Crucix moves quickly, but review bandwidth is limited. The easiest way to get a change merged is to keep it small, well-scoped, and aligned with the project's direction.
|
||||
Intelligence Terminal moves quickly, but review bandwidth is limited. The easiest way to get a change merged is to keep it small, well-scoped, and aligned with the project's private home-server deployment direction.
|
||||
|
||||
## What Contributions Are Most Helpful
|
||||
|
||||
|
||||
38
README.md
38
README.md
@@ -4,22 +4,12 @@
|
||||
|
||||
**Your own intelligence terminal. 27 sources. One command. Zero cloud.**
|
||||
|
||||
## [Visit The Live Site: crucix.live](https://www.crucix.live/)
|
||||
|
||||
[](https://www.crucix.live/)
|
||||
[](https://www.crucix.live/)
|
||||
|
||||
[](#quick-start)
|
||||
[](LICENSE)
|
||||
[-orange)](#architecture)
|
||||
[](#data-sources-27)
|
||||
[](#docker)
|
||||
|
||||
**Enter The Signal Network**
|
||||
|
||||
[](https://x.com/crucixmonitor)
|
||||
[](https://discord.gg/ChVy7SF4)
|
||||
|
||||

|
||||
|
||||
<details>
|
||||
@@ -37,15 +27,13 @@
|
||||
|
||||
</div>
|
||||
|
||||
> **Live website:** [https://www.crucix.live/](https://www.crucix.live/)
|
||||
> Explore the public demo first, then clone the repo to run Crucix locally.
|
||||
> **Supported deployment:** private home-server or lab deployment through Docker, Dockge, Pangolin, or local Node.js.
|
||||
> Runtime data stays in your configured `runs/` volume and API keys are operator-owned.
|
||||
|
||||
Crucix pulls satellite fire detection, flight tracking, radiation monitoring, satellite constellation tracking, economic indicators, live market prices, conflict data, sanctions lists, and social sentiment from 27 open-source intelligence feeds — in parallel, every 15 minutes — and renders everything on a single self-contained Jarvis-style dashboard.
|
||||
|
||||
Hook it up to an LLM and it becomes a **two-way intelligence assistant** — pushing multi-tier alerts to Telegram and Discord when something meaningful changes, responding to commands like `/brief` and `/sweep` from your phone, and generating actionable trade ideas grounded in real cross-domain data. Your own analyst that watches the world while you sleep.
|
||||
|
||||
Try the live demo first at [https://www.crucix.live/](https://www.crucix.live/), then clone the repo when you want the full local stack.
|
||||
|
||||
No cloud. No telemetry. No subscriptions. Just `node server.mjs` and you're running.
|
||||
|
||||
## Token / Asset Warning
|
||||
@@ -71,8 +59,8 @@ It was built for anyone who wants to understand what's actually happening in the
|
||||
|
||||
```bash
|
||||
# 1. Clone the repo
|
||||
git clone https://github.com/calesthio/Crucix.git
|
||||
cd Crucix
|
||||
git clone ssh://git@git.wilkensxl.de:2222/MrSphay/intelligence-terminal.git
|
||||
cd intelligence-terminal
|
||||
|
||||
# 2. Install dependencies (just Express)
|
||||
npm install
|
||||
@@ -640,27 +628,13 @@ To update them: run the dashboard, wait for a sweep to complete, then use your b
|
||||
|
||||
Found a bug? Want to add a 28th source? PRs welcome. Each source is a standalone module in `apis/sources/` — just export a `briefing()` function that returns structured data and add it to the orchestrator in `apis/briefing.mjs`.
|
||||
|
||||
If you find this useful, a star helps others find it too.
|
||||
|
||||
For contribution guidelines, review expectations, and source-add rules, see `CONTRIBUTING.md`. For security reports, see `SECURITY.md`.
|
||||
|
||||
## Contact
|
||||
|
||||
For partnerships, integrations, or other non-issue inquiries, you can reach me at `celesthioailabs@gmail.com`.
|
||||
For bugs and feature requests, use the Gitea issues in this repository so discussion stays visible and actionable.
|
||||
|
||||
For bugs and feature requests, please use GitHub Issues so discussion stays visible and actionable.
|
||||
|
||||
---
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://www.star-history.com/?repos=calesthio%2FCrucix&type=date&legend=top-left">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/image?repos=calesthio/Crucix&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/image?repos=calesthio/Crucix&type=date&legend=top-left" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/image?repos=calesthio/Crucix&type=date&legend=top-left" />
|
||||
</picture>
|
||||
</a>
|
||||
This fork keeps upstream Crucix attribution through the AGPL-3.0 license and commit history while documenting the supported private Gitea/Docker deployment path here.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security issue in Crucix, please report it privately instead of opening a public GitHub issue.
|
||||
If you discover a security issue in Intelligence Terminal, please report it privately instead of opening a public issue.
|
||||
|
||||
Email: `celesthioailabs@gmail.com`
|
||||
Use the private security contact configured for this Gitea repository or contact the repository owner directly.
|
||||
|
||||
Use a subject line like:
|
||||
|
||||
`[Crucix Security] short description`
|
||||
`[Intelligence Terminal Security] short description`
|
||||
|
||||
Please include:
|
||||
|
||||
|
||||
@@ -15,31 +15,6 @@ const API_BASE = 'https://acleddata.com/api/acled/read';
|
||||
// Session cache
|
||||
let sessionCache = { cookies: null, token: null, method: null, expires: 0 };
|
||||
|
||||
function acledCredentials() {
|
||||
return {
|
||||
email: process.env.ACLED_EMAIL || process.env.ACLED_USER || process.env.ACLED_USERNAME,
|
||||
password: process.env.ACLED_PASSWORD,
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeAuthHeaders(headers) {
|
||||
const safe = { ...headers };
|
||||
if (safe.Authorization) safe.Authorization = 'Bearer [redacted]';
|
||||
if (safe.Cookie) safe.Cookie = '[redacted]';
|
||||
return safe;
|
||||
}
|
||||
|
||||
function classifyAuthFailure(status, body = '') {
|
||||
if (status === 401 || status === 403) return 'auth_denied';
|
||||
if (status >= 500) return 'auth_endpoint_failed';
|
||||
if (/invalid|denied|unauthorized|forbidden/i.test(body)) return 'auth_denied';
|
||||
return 'auth_endpoint_failed';
|
||||
}
|
||||
|
||||
export function resetAcledSessionForTests() {
|
||||
sessionCache = { cookies: null, token: null, method: null, expires: 0 };
|
||||
}
|
||||
|
||||
// Strategy 1: Cookie-based session login (mirrors browser login)
|
||||
async function loginCookie(email, password) {
|
||||
const controller = new AbortController();
|
||||
@@ -68,14 +43,11 @@ async function loginCookie(email, password) {
|
||||
}
|
||||
|
||||
const errText = await res.text().catch(() => '');
|
||||
return {
|
||||
error: `Cookie login failed (HTTP ${res.status}): ${errText.slice(0, 200)}`,
|
||||
code: classifyAuthFailure(res.status, errText),
|
||||
};
|
||||
return { error: `Cookie login failed (HTTP ${res.status}): ${errText.slice(0, 200)}` };
|
||||
} catch (e) {
|
||||
clearTimeout(timer);
|
||||
const cause = e.cause ? ` → ${e.cause.message || e.cause.code || e.cause}` : '';
|
||||
return { error: `Cookie login error: ${e.message}${cause}`, code: 'auth_endpoint_failed' };
|
||||
return { error: `Cookie login error: ${e.message}${cause}` };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,30 +73,28 @@ async function loginOAuth(email, password) {
|
||||
|
||||
if (!res.ok) {
|
||||
const errText = await res.text().catch(() => '');
|
||||
return {
|
||||
error: `OAuth failed (HTTP ${res.status}): ${errText.slice(0, 200)}`,
|
||||
code: classifyAuthFailure(res.status, errText),
|
||||
};
|
||||
return { error: `OAuth failed (HTTP ${res.status}): ${errText.slice(0, 200)}` };
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
if (!data.access_token) {
|
||||
return { error: `OAuth response missing access_token: ${JSON.stringify(data).slice(0, 200)}`, code: 'auth_endpoint_failed' };
|
||||
return { error: `OAuth response missing access_token: ${JSON.stringify(data).slice(0, 200)}` };
|
||||
}
|
||||
|
||||
return { token: data.access_token };
|
||||
} catch (e) {
|
||||
clearTimeout(timer);
|
||||
const cause = e.cause ? ` → ${e.cause.message || e.cause.code || e.cause}` : '';
|
||||
return { error: `OAuth error: ${e.message}${cause}`, code: 'auth_endpoint_failed' };
|
||||
return { error: `OAuth error: ${e.message}${cause}` };
|
||||
}
|
||||
}
|
||||
|
||||
// Try both auth strategies
|
||||
async function authenticate() {
|
||||
const { email, password } = acledCredentials();
|
||||
const email = process.env.ACLED_EMAIL;
|
||||
const password = process.env.ACLED_PASSWORD;
|
||||
if (!email || !password) {
|
||||
return { error: 'No ACLED credentials. Set ACLED_EMAIL or ACLED_USER and ACLED_PASSWORD in .env.', code: 'no_credentials' };
|
||||
return { error: 'No ACLED credentials. Set ACLED_EMAIL and ACLED_PASSWORD in .env.' };
|
||||
}
|
||||
|
||||
// Return cached session if still valid
|
||||
@@ -138,7 +108,7 @@ async function authenticate() {
|
||||
// Try OAuth first (official programmatic method per ACLED docs)
|
||||
const oauthResult = await loginOAuth(email, password);
|
||||
if (oauthResult.token) {
|
||||
if (debug) console.error('[ACLED DEBUG] OAuth OK');
|
||||
if (debug) console.error(`[ACLED DEBUG] OAuth OK — token: ${oauthResult.token.slice(0, 20)}...`);
|
||||
sessionCache = { cookies: null, token: oauthResult.token, method: 'oauth', expires: Date.now() + 23 * 60 * 60 * 1000 };
|
||||
return sessionCache;
|
||||
}
|
||||
@@ -148,14 +118,13 @@ async function authenticate() {
|
||||
// Fall back to cookie-based session
|
||||
const cookieResult = await loginCookie(email, password);
|
||||
if (cookieResult.cookies) {
|
||||
if (debug) console.error('[ACLED DEBUG] Cookie OK');
|
||||
if (debug) console.error(`[ACLED DEBUG] Cookie OK — cookies: ${cookieResult.cookies.slice(0, 80)}...`);
|
||||
sessionCache = { cookies: cookieResult.cookies, token: null, method: 'cookie', expires: Date.now() + 12 * 60 * 60 * 1000 };
|
||||
return sessionCache;
|
||||
}
|
||||
errors.push(`Cookie: ${cookieResult.error}`);
|
||||
|
||||
const code = [oauthResult.code, cookieResult.code].includes('auth_denied') ? 'auth_denied' : 'auth_endpoint_failed';
|
||||
return { error: `All ACLED auth methods failed.\n${errors.join('\n')}`, code };
|
||||
return { error: `All ACLED auth methods failed.\n${errors.join('\n')}` };
|
||||
}
|
||||
|
||||
// Build headers based on auth method
|
||||
@@ -191,7 +160,7 @@ export async function getEvents(opts = {}) {
|
||||
} = opts;
|
||||
|
||||
const session = await authenticate();
|
||||
if (session.error) return { error: session.error, code: session.code || 'auth_failed' };
|
||||
if (session.error) return { error: session.error };
|
||||
|
||||
const params = new URLSearchParams({ _format: 'json', limit: String(limit) });
|
||||
if (eventDateStart && eventDateEnd) {
|
||||
@@ -208,7 +177,7 @@ export async function getEvents(opts = {}) {
|
||||
const hdrs = authHeaders(session);
|
||||
if (debug) {
|
||||
console.error(`[ACLED DEBUG] Data request: GET ${url}`);
|
||||
console.error(`[ACLED DEBUG] Headers: ${JSON.stringify(sanitizeAuthHeaders(hdrs))}`);
|
||||
console.error(`[ACLED DEBUG] Headers: ${JSON.stringify(hdrs)}`);
|
||||
}
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 25000);
|
||||
@@ -232,28 +201,25 @@ export async function getEvents(opts = {}) {
|
||||
+ ' 3. Ensure your account has the "API" access group\n'
|
||||
+ ' Contact access@acleddata.com if issues persist.'
|
||||
: '';
|
||||
return {
|
||||
error: `ACLED data access denied (HTTP ${res.status}, auth method: ${session.method}). Response: ${errText.slice(0, 300)}${hint}`,
|
||||
code: 'auth_denied',
|
||||
};
|
||||
return { error: `ACLED data access denied (HTTP ${res.status}, auth method: ${session.method}). Response: ${errText.slice(0, 300)}${hint}` };
|
||||
}
|
||||
return { error: `HTTP ${res.status}: ${errText.slice(0, 200)}`, code: 'api_failed' };
|
||||
return { error: `HTTP ${res.status}: ${errText.slice(0, 200)}` };
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
// ACLED may return a 200 with an error status in the body
|
||||
if (data?.status && data.status !== 200) {
|
||||
return { error: `ACLED API error: status ${data.status} - ${data.message || 'Unknown error'}`, code: 'api_failed' };
|
||||
return { error: `ACLED API error: status ${data.status} — ${data.message || 'Unknown error'}` };
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
if (e.name === 'AbortError') {
|
||||
return { error: 'ACLED data request timed out (25s)', code: 'api_failed' };
|
||||
return { error: 'ACLED data request timed out (25s)' };
|
||||
}
|
||||
const rootCause = e.cause ? `${e.cause.message || e.cause.code || e.cause}` : '';
|
||||
return { error: `ACLED data error: ${e.message}${rootCause ? ' - ' + rootCause : ''}`, code: 'api_failed' };
|
||||
return { error: `ACLED data error: ${e.message}${rootCause ? ' → ' + rootCause : ''}` };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,13 +237,12 @@ function groupBy(events, field) {
|
||||
|
||||
// Briefing — last 7 days of global conflict events
|
||||
export async function briefing() {
|
||||
const { email, password } = acledCredentials();
|
||||
if (!email || !password) {
|
||||
if (!process.env.ACLED_EMAIL || !process.env.ACLED_PASSWORD) {
|
||||
return {
|
||||
source: 'ACLED',
|
||||
timestamp: new Date().toISOString(),
|
||||
status: 'no_credentials',
|
||||
message: 'Set ACLED_EMAIL or ACLED_USER and ACLED_PASSWORD in .env. Register at https://acleddata.com/user/register',
|
||||
message: 'Set ACLED_EMAIL and ACLED_PASSWORD in .env. Register at https://acleddata.com/user/register',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -291,8 +256,7 @@ export async function briefing() {
|
||||
});
|
||||
|
||||
if (data?.error) {
|
||||
const status = data.code === 'auth_denied' ? 'auth_failed' : 'api_failed';
|
||||
return { source: 'ACLED', timestamp: new Date().toISOString(), status, error: data.error };
|
||||
return { source: 'ACLED', timestamp: new Date().toISOString(), error: data.error };
|
||||
}
|
||||
|
||||
let events = data?.data || [];
|
||||
@@ -336,7 +300,6 @@ export async function briefing() {
|
||||
return {
|
||||
source: 'ACLED',
|
||||
timestamp: new Date().toISOString(),
|
||||
status: 'live',
|
||||
period: { start, end },
|
||||
totalEvents: events.length,
|
||||
totalFatalities,
|
||||
@@ -344,7 +307,6 @@ export async function briefing() {
|
||||
byType,
|
||||
topCountries,
|
||||
deadliestEvents,
|
||||
message: events.length === 0 ? 'ACLED returned a valid empty event set for the requested period.' : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
|
||||
Provides conflict events, fatalities, event types, and locations.
|
||||
|
||||
- Auth: `ACLED_EMAIL` or `ACLED_USER`, plus `ACLED_PASSWORD`.
|
||||
- Auth: `ACLED_EMAIL` and `ACLED_PASSWORD`.
|
||||
- Flow: OAuth password grant is tried first, then cookie session fallback.
|
||||
- Failure modes: missing credentials (`no_credentials`), rejected credentials or access denied (`auth_failed`), token/API endpoint failure (`api_failed`), and valid empty event sets (`totalEvents: 0`).
|
||||
- Failure modes: missing credentials, terms/profile not completed, expired token, account missing API access.
|
||||
- Behavior: missing or rejected credentials produce degraded source health with the ACLED error text.
|
||||
- Debug logs redact bearer tokens and cookies.
|
||||
- Test: set credentials, run `node apis/sources/acled.mjs`, then check `/api/health`.
|
||||
|
||||
@@ -34,129 +34,3 @@ test('safeFetchText returns text and byte count', async () => {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
function jsonResponse(payload, ok = true, status = 200) {
|
||||
return {
|
||||
ok,
|
||||
status,
|
||||
headers: { getSetCookie: () => [], get: () => 'application/json' },
|
||||
text: async () => JSON.stringify(payload),
|
||||
json: async () => payload,
|
||||
};
|
||||
}
|
||||
|
||||
function textResponse(text, ok = false, status = 500) {
|
||||
return {
|
||||
ok,
|
||||
status,
|
||||
headers: { getSetCookie: () => [], get: () => 'text/plain' },
|
||||
text: async () => text,
|
||||
json: async () => JSON.parse(text),
|
||||
};
|
||||
}
|
||||
|
||||
async function withAcledEnv(mockFetch, fn) {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const saved = {
|
||||
ACLED_EMAIL: process.env.ACLED_EMAIL,
|
||||
ACLED_USER: process.env.ACLED_USER,
|
||||
ACLED_USERNAME: process.env.ACLED_USERNAME,
|
||||
ACLED_PASSWORD: process.env.ACLED_PASSWORD,
|
||||
};
|
||||
globalThis.fetch = mockFetch;
|
||||
delete process.env.ACLED_EMAIL;
|
||||
delete process.env.ACLED_USER;
|
||||
delete process.env.ACLED_USERNAME;
|
||||
delete process.env.ACLED_PASSWORD;
|
||||
const acled = await import('../apis/sources/acled.mjs');
|
||||
acled.resetAcledSessionForTests();
|
||||
try {
|
||||
return await fn(acled);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
for (const [key, value] of Object.entries(saved)) {
|
||||
if (value === undefined) delete process.env[key];
|
||||
else process.env[key] = value;
|
||||
}
|
||||
acled.resetAcledSessionForTests();
|
||||
}
|
||||
}
|
||||
|
||||
test('ACLED credentialed OAuth success returns live events and supports ACLED_USER', async () => {
|
||||
const responses = [
|
||||
jsonResponse({ access_token: 'secret-token' }),
|
||||
jsonResponse({
|
||||
status: 200,
|
||||
data: [{
|
||||
event_date: '2026-05-17',
|
||||
event_type: 'Protests',
|
||||
sub_event_type: 'Peaceful protest',
|
||||
country: 'Example',
|
||||
region: 'Example Region',
|
||||
location: 'Example City',
|
||||
fatalities: '0',
|
||||
latitude: '1.23',
|
||||
longitude: '4.56',
|
||||
}],
|
||||
}),
|
||||
];
|
||||
|
||||
await withAcledEnv(async () => responses.shift(), async ({ briefing }) => {
|
||||
process.env.ACLED_USER = 'operator@example.test';
|
||||
process.env.ACLED_PASSWORD = 'password';
|
||||
const data = await briefing();
|
||||
|
||||
assert.equal(data.status, 'live');
|
||||
assert.equal(data.totalEvents, 1);
|
||||
assert.equal(data.topCountries.Example.count, 1);
|
||||
});
|
||||
});
|
||||
|
||||
test('ACLED rejected credentials return auth_failed diagnostics', async () => {
|
||||
const responses = [
|
||||
textResponse('invalid credentials', false, 401),
|
||||
textResponse('forbidden', false, 403),
|
||||
];
|
||||
|
||||
await withAcledEnv(async () => responses.shift(), async ({ briefing }) => {
|
||||
process.env.ACLED_EMAIL = 'operator@example.test';
|
||||
process.env.ACLED_PASSWORD = 'wrong-password';
|
||||
const data = await briefing();
|
||||
|
||||
assert.equal(data.status, 'auth_failed');
|
||||
assert.match(data.error, /All ACLED auth methods failed/);
|
||||
});
|
||||
});
|
||||
|
||||
test('ACLED token endpoint failure returns api_failed diagnostics', async () => {
|
||||
const responses = [
|
||||
textResponse('temporary outage', false, 503),
|
||||
textResponse('temporary outage', false, 503),
|
||||
];
|
||||
|
||||
await withAcledEnv(async () => responses.shift(), async ({ briefing }) => {
|
||||
process.env.ACLED_EMAIL = 'operator@example.test';
|
||||
process.env.ACLED_PASSWORD = 'password';
|
||||
const data = await briefing();
|
||||
|
||||
assert.equal(data.status, 'api_failed');
|
||||
assert.match(data.error, /All ACLED auth methods failed/);
|
||||
});
|
||||
});
|
||||
|
||||
test('ACLED valid empty response is live with zero events', async () => {
|
||||
const responses = [
|
||||
jsonResponse({ access_token: 'secret-token' }),
|
||||
jsonResponse({ status: 200, data: [] }),
|
||||
];
|
||||
|
||||
await withAcledEnv(async () => responses.shift(), async ({ briefing }) => {
|
||||
process.env.ACLED_EMAIL = 'operator@example.test';
|
||||
process.env.ACLED_PASSWORD = 'password';
|
||||
const data = await briefing();
|
||||
|
||||
assert.equal(data.status, 'live');
|
||||
assert.equal(data.totalEvents, 0);
|
||||
assert.match(data.message, /valid empty/);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user