uphold/tests/bot.test.js
2025-11-28 20:33:15 +00:00

187 lines
5.1 KiB
JavaScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import nock from "nock";
import { BigNumber } from "bignumber.js";
vi.mock("../src/db.js", () => ({
insertIntoDB: vi.fn().mockResolvedValue(undefined),
initDB: vi.fn().mockResolvedValue(undefined),
closePool: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("../src/logger.js", () => ({
default: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
fatal: vi.fn(),
},
}));
describe("Bot", () => {
const CONFIG = {
pair: "BTC-USD",
interval: 100,
threshold: 0.01,
};
let BotClass;
let bot;
let fetchRateMock;
let onAlertSpy;
beforeEach(async () => {
vi.resetModules();
fetchRateMock = vi.fn();
vi.doMock("../src/api.js", () => ({
fetchRate: fetchRateMock,
prefetchRates: vi.fn(),
}));
const botModule = await import("../src/bot.js");
BotClass = botModule.Bot;
onAlertSpy = vi.fn();
bot = new BotClass(CONFIG, onAlertSpy);
bot.running = false;
});
afterEach(() => {
vi.clearAllMocks();
});
describe("Initialization", () => {
it("should initialize with correct parameters", () => {
expect(bot.pair).toBe(CONFIG.pair);
expect(bot.interval).toBe(CONFIG.interval);
expect(bot.threshold).toBe(CONFIG.threshold);
expect(bot.running).toBe(false);
expect(bot.lastPrice).toBeNull();
});
it("should set baseline price on first check without alerting", async () => {
fetchRateMock.mockResolvedValue(new BigNumber("60000.50"));
await bot.loop();
expect(bot.lastPrice).toBeDefined();
expect(bot.lastPrice.toFixed(2)).toBe("60000.50");
expect(onAlertSpy).not.toHaveBeenCalled();
});
});
describe("Threshold Detection", () => {
beforeEach(async () => {
fetchRateMock.mockResolvedValue(new BigNumber("50000.00"));
await bot.loop();
fetchRateMock.mockReset();
});
it("should NOT alert when change is below threshold", async () => {
fetchRateMock.mockResolvedValue(new BigNumber("50004.99"));
await bot.loop();
expect(onAlertSpy).not.toHaveBeenCalled();
expect(bot.lastPrice.toFixed(2)).toBe("50000.00");
});
it("should alert when price moves exactly at threshold", async () => {
fetchRateMock.mockResolvedValue(new BigNumber("50005.00"));
await bot.loop();
expect(onAlertSpy).toHaveBeenCalledTimes(1);
const alertData = onAlertSpy.mock.calls[0][0];
expect(alertData).toMatchObject({
pair: "BTC-USD",
direction: "UP",
previousPrice: "50000.000000",
newPrice: "50005.000000",
});
expect(bot.lastPrice.toFixed(2)).toBe("50005.00");
});
it("should alert on downward price movement", async () => {
fetchRateMock.mockResolvedValue(new BigNumber("49995.00"));
await bot.loop();
expect(onAlertSpy).toHaveBeenCalledTimes(1);
expect(onAlertSpy.mock.calls[0][0].direction).toBe("DOWN");
});
});
describe("Error Handling", () => {
it("should handle network errors gracefully without crashing", async () => {
fetchRateMock.mockRejectedValue(new Error("Network error"));
await expect(bot.loop()).resolves.not.toThrow();
});
});
});
describe("API", () => {
let api;
beforeEach(async () => {
vi.resetModules();
nock.cleanAll();
vi.doMock("../src/api.js", async () => {
return await vi.importActual("../src/api.js");
});
api = await import("../src/api.js");
});
afterEach(() => {
nock.cleanAll();
});
describe("fetchRate", () => {
it("should return a BigNumber on success", async () => {
const scope = nock("https://api.uphold.com")
.get("/v0/ticker/BTC-USD")
.reply(200, { ask: "60000" });
const price = await api.fetchRate("BTC-USD");
expect(price).toBeInstanceOf(BigNumber);
expect(price.toFixed(0)).toBe("60000");
expect(scope.isDone()).toBe(true);
});
it("should retry on failure and eventually success", async () => {
const scope = nock("https://api.uphold.com")
.get("/v0/ticker/BTC-USD")
.replyWithError("Socket Hangup") // Fail 1
.get("/v0/ticker/BTC-USD")
.reply(200, { ask: "60000" }); // Success 2
const price = await api.fetchRate("BTC-USD");
expect(price.toFixed(0)).toBe("60000");
expect(scope.isDone()).toBe(true);
});
it("should throw after max retries", async () => {
const scope = nock("https://api.uphold.com")
.get("/v0/ticker/BTC-USD")
.times(3)
.replyWithError("Persistent Fail");
await expect(api.fetchRate("BTC-USD")).rejects.toThrow("Persistent Fail");
expect(scope.isDone()).toBe(true);
});
it("should handle 429 Rate Limits by retrying and eventually throwing", async () => {
const scope = nock("https://api.uphold.com")
.get("/v0/ticker/BTC-USD")
// TODO: Find better way to test, takes too long
.times(3)
.reply(429, { error: "Too Many Requests" });
await expect(api.fetchRate("BTC-USD")).rejects.toThrow("Rate limited");
expect(scope.isDone()).toBe(true);
});
});
});