188 lines
5.2 KiB
JavaScript
188 lines
5.2 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();
|
|
|
|
// Import the actual module for these tests
|
|
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);
|
|
});
|
|
});
|
|
});
|