import { describe, it, expect, vi, beforeEach, afterEach, beforeAll, } from "vitest"; import nock from "nock"; import { BigNumber } from "bignumber.js"; import { Bot } from "../src/bot.js"; vi.mock("console", () => ({ log: vi.fn(), error: vi.fn(), })); vi.mock("../src/db.js", () => ({ insertIntoDB: vi.fn(), initDB: vi.fn(), })); describe("Test Suite", () => { const PAIR = "BTC-USD"; const INTERVAL = 100; const THRESHOLD = 0.01; let bot; beforeAll(() => { // Ensure BigNumber config matches what we expect BigNumber.config({ DECIMAL_PLACES: 10, ROUNDING_MODE: BigNumber.ROUND_HALF_UP, }); }); beforeEach(() => { bot = new Bot(PAIR, INTERVAL, THRESHOLD); vi.useFakeTimers(); }); afterEach(() => { bot.stop(); vi.runAllTimers(); vi.useRealTimers(); nock.cleanAll(); vi.restoreAllMocks(); }); describe("Initialization and first fetch", () => { it("should set lastPrice on first successful API call", async () => { nock("https://api.uphold.com") .get(`/v0/ticker/${PAIR}`) .reply(200, { ask: "60000.50", bid: "59990.00", currency: "USD" }); await bot.check(); expect(bot.lastPrice).toBeDefined(); expect(bot.lastPrice.toFixed(2)).toBe("60000.50"); }); }); describe("Price change detection", () => { beforeEach(async () => { // Establish baseline nock("https://api.uphold.com") .get(`/v0/ticker/${PAIR}`) .reply(200, { ask: "50000.00" }); await bot.check(); nock.cleanAll(); }); it("should NOT alert when change is below threshold", async () => { const alertSpy = vi.spyOn(bot, "alert"); nock("https://api.uphold.com") .get(`/v0/ticker/${PAIR}`) .reply(200, { ask: "50004.99" }); // +0.00998% await bot.check(); expect(alertSpy).not.toHaveBeenCalled(); }); it("should alert when price moves exactly 0.01% (threshold)", async () => { const alertSpy = vi.spyOn(bot, "alert"); nock("https://api.uphold.com") .get(`/v0/ticker/${PAIR}`) .reply(200, { ask: "50005.00" }); await bot.check(); // Check specifically for arguments passed to alert expect(alertSpy).toHaveBeenCalledWith("UP", "50005.00", 0.01); }); it("should alert on downward move", async () => { const alertSpy = vi.spyOn(bot, "alert"); nock("https://api.uphold.com") .get(`/v0/ticker/${PAIR}`) .reply(200, { ask: "49500.00" }); // -1% await bot.check(); expect(alertSpy).toHaveBeenCalledWith("DOWN", "49500.00", -1); }); }); describe("Alert method", () => { it("should log correct message for price increase", () => { bot.lastPrice = new BigNumber("50000"); const consoleSpy = vi.spyOn(console, "log"); bot.alert("UP", "50500.00", 1); expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("UP")); expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("1.00%")); }); }); describe("Polling mechanism", () => { it("should repeatedly call check()", async () => { const checkSpy = vi.spyOn(bot, "check").mockResolvedValue(); bot.start(); vi.advanceTimersByTime(INTERVAL * 3); await Promise.resolve(); expect(checkSpy).toHaveBeenCalledTimes(4); }); }); describe("Error handling", () => { it("should bubble up errors when check() is called directly", async () => { nock("https://api.uphold.com") .get(`/v0/ticker/${PAIR}`) .replyWithError("Connection Error"); await expect(bot.check()).rejects.toThrow("Connection Error"); }); it("should reject invalid data formats", async () => { nock("https://api.uphold.com") .get(`/v0/ticker/${PAIR}`) .reply(200, { bid: "100" }); await expect(bot.check()).rejects.toThrow("Invalid data format"); }); }); });