fix-backend-review-findings (ornith-35): oauth empty-token guard, session-secret startup assertion, OAuth email-link test

Fixes from ornith's backend code review, implemented by ornith-35 (cross-review by
qwopus35b pending). 129/129 tests green.
This commit is contained in:
Investor Flow Build
2026-06-29 23:33:02 -04:00
parent a303720c35
commit 2c7e7a0786
6 changed files with 526 additions and 0 deletions
@@ -0,0 +1,207 @@
// Investor Flow — indicators.test.ts
// Pure-logic tests for technical indicator functions. No HTTP, no filesystem, no DB.
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { ema, rsi, relativeVolume, emaFromCandles } from '../indicators.ts';
import type { PriceCandle } from '../../cache/CacheRepository.ts';
describe('ema', () => {
it('returns undefined before period - 1, then a seeded SMA at period - 1', () => {
const values = [1, 2, 3, 4, 5]; // period = 3
const result = ema(values, 3);
assert.equal(result[0], undefined);
assert.equal(result[1], undefined);
// SMA of [1,2,3] = 2
assert.equal(result[2], 2);
});
it('computes a known EMA value correctly', () => {
// Known series: [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
// period = 3. SMA of [10,11,12] = 11.
// EMA at index 3: (13 - 11) * (2/4) + 11 = 2*0.5 + 11 = 12
// EMA at index 4: (14 - 12) * 0.5 + 12 = 13
// EMA at index 9: hand-computed:
const values = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19];
const result = ema(values, 3);
assert.equal(result[2], 11);
assert.equal(result[3], 12);
assert.equal(result[4], 13);
// Continue manually:
// i=5: (15-13)*0.5+13 = 14
assert.equal(result[5], 14);
// i=6: (16-14)*0.5+14 = 15
assert.equal(result[6], 15);
// i=7: (17-15)*0.5+15 = 16
assert.equal(result[7], 16);
// i=8: (18-16)*0.5+16 = 17
assert.equal(result[8], 17);
// i=9: (19-17)*0.5+17 = 18
assert.equal(result[9], 18);
});
it('returns all undefined when values.length < period', () => {
const result = ema([1, 2], 5);
assert.equal(result.length, 2);
for (const v of result) assert.equal(v, undefined);
});
it('throws on non-positive period', () => {
assert.throws(() => ema([1], 0));
assert.throws(() => ema([1], -1));
});
it('handles empty array', () => {
const result = ema([], 5);
assert.deepEqual(result, []);
});
});
describe('rsi', () => {
it('returns 100 on a strictly increasing series', () => {
// 20 closes, each +1 from the previous. All deltas = 1.
const closes: number[] = [];
for (let i = 0; i < 20; i++) closes.push(100 + i);
const result = rsi(closes, 14);
// The last value (index 19) should be 100.
assert.equal(result[19], 100);
});
it('returns 0 on a strictly decreasing series', () => {
const closes: number[] = [];
for (let i = 0; i < 20; i++) closes.push(100 - i);
const result = rsi(closes, 14);
assert.equal(result[19], 0);
});
it('returns undefined before the period-th close', () => {
const closes = [1, 2, 3, 4, 5]; // period=14 (default)
const result = rsi(closes);
for (let i = 0; i < closes.length; i++) {
assert.equal(result[i], undefined);
}
});
it('returns a value between 0 and 100 for mixed data', () => {
// 30 closes alternating up/down.
const closes: number[] = [];
for (let i = 0; i < 30; i++) closes.push(100 + Math.sin(i * 0.5) * 10);
const result = rsi(closes, 14);
// At index 29 (last), should be a number between 0 and 100.
const last = result[29];
assert.ok(last !== undefined);
assert.ok(last! >= 0 && last! <= 100, `RSI out of range: ${last}`);
});
it('throws on non-positive period', () => {
assert.throws(() => rsi([1, 2], 0));
});
it('uses default period of 14', () => {
const closes: number[] = [];
for (let i = 0; i < 20; i++) closes.push(100 + i);
const result = rsi(closes); // default 14
assert.equal(result[19], 100);
});
});
describe('relativeVolume', () => {
it('returns a ratio > 1 when the last volume exceeds the average', () => {
// 25 volumes: first 20 are ~100, last one is 300 (3x average).
const volumes: number[] = [];
for (let i = 0; i < 20; i++) volumes.push(100);
volumes.push(300); // index 20
const result = relativeVolume(volumes, 20);
// SMA of first 20 = 100. Ratio at index 20 = 300/100 = 3.
assert.equal(result[20], 3);
});
it('returns a ratio < 1 when the last volume is below average', () => {
const volumes: number[] = [];
for (let i = 0; i < 20; i++) volumes.push(100);
volumes.push(50); // index 20
const result = relativeVolume(volumes, 20);
assert.equal(result[20], 0.5);
});
it('returns undefined before the period is complete', () => {
const result = relativeVolume([1, 2, 3], 20);
for (let i = 0; i < result.length; i++) {
assert.equal(result[i], undefined);
}
});
it('uses a sliding window for subsequent values', () => {
// 25 volumes: [10,10,...,10] (20 times), then [5, 50].
const volumes: number[] = [];
for (let i = 0; i < 20; i++) volumes.push(10);
volumes.push(5); // index 20: SMA=10, ratio=0.5
volumes.push(50); // index 21: window slides, SMA = (10*19 + 5)/20 = 9.75, ratio = 50/9.75
const result = relativeVolume(volumes, 20);
assert.equal(result[20], 0.5);
// At index 21: sliding window is volumes[2..21] = 18*10 + 5 + 50 = 290, SMA = 14.5
// Wait: sum at index 20 = 200. sum += 50 - volumes[1](=10) => sum = 240. SMA = 12.
// Ratio = 50/12 ≈ 4.1667
const expectedRatio = 50 / 12;
assert.ok(Math.abs(result[21]! - expectedRatio) < 1e-9, `Expected ~${expectedRatio}, got ${result[21]}`);
});
it('throws on non-positive period', () => {
assert.throws(() => relativeVolume([1], 0));
});
it('handles empty array', () => {
const result = relativeVolume([], 5);
assert.deepEqual(result, []);
});
});
describe('emaFromCandles', () => {
it('uses close (c) by default', () => {
const candles: PriceCandle[] = [
{ ts: '2024-01-01', o: 10, h: 12, l: 9, c: 11, v: 100 },
{ ts: '2024-01-02', o: 11, h: 13, l: 10, c: 12, v: 100 },
{ ts: '2024-01-03', o: 12, h: 14, l: 11, c: 13, v: 100 },
];
const result = emaFromCandles(candles, 'c', 3);
// SMA of [11, 12, 13] = 12
assert.equal(result[2], 12);
});
it('uses adjClose when requested, falling back to c if null', () => {
const candles: PriceCandle[] = [
{ ts: '2024-01-01', o: 10, h: 12, l: 9, c: 11, v: 100, adjClose: 10.5 },
{ ts: '2024-01-02', o: 11, h: 13, l: 10, c: 12, v: 100, adjClose: 11.5 },
{ ts: '2024-01-03', o: 12, h: 14, l: 11, c: 13, v: 100, adjClose: 12.5 },
];
const result = emaFromCandles(candles, 'adjClose', 3);
// SMA of [10.5, 11.5, 12.5] = 11.5
assert.equal(result[2], 11.5);
});
it('falls back to c when adjClose is null', () => {
const candles: PriceCandle[] = [
{ ts: '2024-01-01', o: 10, h: 12, l: 9, c: 11, v: 100, adjClose: null },
{ ts: '2024-01-02', o: 11, h: 13, l: 10, c: 12, v: 100, adjClose: null },
{ ts: '2024-01-03', o: 12, h: 14, l: 11, c: 13, v: 100, adjClose: null },
];
const result = emaFromCandles(candles, 'adjClose', 3);
// Falls back to c: SMA of [11, 12, 13] = 12
assert.equal(result[2], 12);
});
it('returns undefined before period - 1', () => {
const candles: PriceCandle[] = [
{ ts: '2024-01-01', o: 10, h: 12, l: 9, c: 11, v: 100 },
{ ts: '2024-01-02', o: 11, h: 13, l: 10, c: 12, v: 100 },
];
const result = emaFromCandles(candles, 'c', 5);
for (const v of result) assert.equal(v, undefined);
});
it('handles empty candles array', () => {
const result = emaFromCandles([], 'c', 5);
assert.deepEqual(result, []);
});
});