forked from syntaxbullet/AuroraBot-discord
249 lines
8.9 KiB
TypeScript
249 lines
8.9 KiB
TypeScript
import { describe, expect, test, mock, beforeEach, afterAll, spyOn } from "bun:test";
|
|
import * as fs from "fs/promises";
|
|
|
|
// Mock child_process BEFORE importing the service
|
|
const mockExec = mock((cmd: string, callback?: any) => {
|
|
// Handle calls without callback (like exec().unref())
|
|
if (!callback) {
|
|
return { unref: () => { } };
|
|
}
|
|
|
|
if (cmd.includes("git rev-parse")) {
|
|
callback(null, { stdout: "main\n" });
|
|
} else if (cmd.includes("git fetch")) {
|
|
callback(null, { stdout: "" });
|
|
} else if (cmd.includes("git log")) {
|
|
callback(null, { stdout: "abcdef Update 1\n123456 Update 2" });
|
|
} else if (cmd.includes("git diff")) {
|
|
callback(null, { stdout: "package.json\nsrc/index.ts" });
|
|
} else if (cmd.includes("git reset")) {
|
|
callback(null, { stdout: "HEAD is now at abcdef Update 1" });
|
|
} else if (cmd.includes("bun install")) {
|
|
callback(null, { stdout: "Installed dependencies" });
|
|
} else if (cmd.includes("drizzle-kit migrate")) {
|
|
callback(null, { stdout: "Migrations applied" });
|
|
} else {
|
|
callback(null, { stdout: "" });
|
|
}
|
|
});
|
|
|
|
mock.module("child_process", () => ({
|
|
exec: mockExec
|
|
}));
|
|
|
|
// Mock fs/promises
|
|
const mockWriteFile = mock((path: string, content: string) => Promise.resolve());
|
|
const mockReadFile = mock((path: string, encoding: string) => Promise.resolve("{}"));
|
|
const mockUnlink = mock((path: string) => Promise.resolve());
|
|
|
|
mock.module("fs/promises", () => ({
|
|
writeFile: mockWriteFile,
|
|
readFile: mockReadFile,
|
|
unlink: mockUnlink
|
|
}));
|
|
|
|
// Mock view module to avoid import issues
|
|
mock.module("./update.view", () => ({
|
|
getPostRestartEmbed: () => ({ title: "Update Complete" }),
|
|
getInstallingDependenciesEmbed: () => ({ title: "Installing..." }),
|
|
}));
|
|
|
|
describe("UpdateService", () => {
|
|
let UpdateService: any;
|
|
|
|
beforeEach(async () => {
|
|
mockExec.mockClear();
|
|
mockWriteFile.mockClear();
|
|
mockReadFile.mockClear();
|
|
mockUnlink.mockClear();
|
|
|
|
// Dynamically import to ensure mock is used
|
|
const module = await import("./update.service");
|
|
UpdateService = module.UpdateService;
|
|
});
|
|
|
|
afterAll(() => {
|
|
mock.restore();
|
|
});
|
|
|
|
describe("checkForUpdates", () => {
|
|
test("should return updates if git log has output", async () => {
|
|
const result = await UpdateService.checkForUpdates();
|
|
|
|
expect(result.hasUpdates).toBe(true);
|
|
expect(result.branch).toBe("main");
|
|
expect(result.log).toContain("Update 1");
|
|
});
|
|
|
|
test("should call git rev-parse, fetch, and log commands", async () => {
|
|
await UpdateService.checkForUpdates();
|
|
|
|
const calls = mockExec.mock.calls.map((c: any) => c[0]);
|
|
expect(calls.some((cmd: string) => cmd.includes("git rev-parse"))).toBe(true);
|
|
expect(calls.some((cmd: string) => cmd.includes("git fetch"))).toBe(true);
|
|
expect(calls.some((cmd: string) => cmd.includes("git log"))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("performUpdate", () => {
|
|
test("should run git reset --hard with correct branch", async () => {
|
|
await UpdateService.performUpdate("main");
|
|
|
|
const lastCall = mockExec.mock.lastCall;
|
|
expect(lastCall).toBeDefined();
|
|
expect(lastCall![0]).toContain("git reset --hard origin/main");
|
|
});
|
|
});
|
|
|
|
describe("checkUpdateRequirements", () => {
|
|
test("should detect package.json and schema.ts changes", async () => {
|
|
const result = await UpdateService.checkUpdateRequirements("main");
|
|
|
|
expect(result.needsInstall).toBe(true);
|
|
expect(result.needsMigrations).toBe(false); // mock doesn't include schema.ts
|
|
expect(result.error).toBeUndefined();
|
|
});
|
|
|
|
test("should call git diff with correct branch", async () => {
|
|
await UpdateService.checkUpdateRequirements("develop");
|
|
|
|
const lastCall = mockExec.mock.lastCall;
|
|
expect(lastCall).toBeDefined();
|
|
expect(lastCall![0]).toContain("git diff HEAD..origin/develop");
|
|
});
|
|
});
|
|
|
|
describe("installDependencies", () => {
|
|
test("should run bun install and return output", async () => {
|
|
const output = await UpdateService.installDependencies();
|
|
|
|
expect(output).toBe("Installed dependencies");
|
|
const lastCall = mockExec.mock.lastCall;
|
|
expect(lastCall![0]).toBe("bun install");
|
|
});
|
|
});
|
|
|
|
describe("prepareRestartContext", () => {
|
|
test("should write context to file", async () => {
|
|
const context = {
|
|
channelId: "123",
|
|
userId: "456",
|
|
timestamp: Date.now(),
|
|
runMigrations: true,
|
|
installDependencies: false
|
|
};
|
|
|
|
await UpdateService.prepareRestartContext(context);
|
|
|
|
expect(mockWriteFile).toHaveBeenCalled();
|
|
const lastCall = mockWriteFile.mock.lastCall as [string, string] | undefined;
|
|
expect(lastCall).toBeDefined();
|
|
expect(lastCall![0]).toContain("restart_context");
|
|
expect(JSON.parse(lastCall![1])).toEqual(context);
|
|
});
|
|
});
|
|
|
|
describe("triggerRestart", () => {
|
|
test("should use RESTART_COMMAND env var when set", async () => {
|
|
const originalEnv = process.env.RESTART_COMMAND;
|
|
process.env.RESTART_COMMAND = "pm2 restart bot";
|
|
|
|
await UpdateService.triggerRestart();
|
|
|
|
const lastCall = mockExec.mock.lastCall;
|
|
expect(lastCall).toBeDefined();
|
|
expect(lastCall![0]).toBe("pm2 restart bot");
|
|
|
|
process.env.RESTART_COMMAND = originalEnv;
|
|
});
|
|
|
|
test("should write to trigger file when no env var", async () => {
|
|
const originalEnv = process.env.RESTART_COMMAND;
|
|
delete process.env.RESTART_COMMAND;
|
|
|
|
await UpdateService.triggerRestart();
|
|
|
|
expect(mockWriteFile).toHaveBeenCalled();
|
|
const lastCall = mockWriteFile.mock.lastCall as [string, string] | undefined;
|
|
expect(lastCall).toBeDefined();
|
|
expect(lastCall![0]).toContain("restart_trigger");
|
|
|
|
process.env.RESTART_COMMAND = originalEnv;
|
|
});
|
|
});
|
|
|
|
describe("handlePostRestart", () => {
|
|
const createMockClient = (channel: any = null) => ({
|
|
channels: {
|
|
fetch: mock(() => Promise.resolve(channel))
|
|
}
|
|
});
|
|
|
|
const createMockChannel = () => ({
|
|
isSendable: () => true,
|
|
send: mock(() => Promise.resolve())
|
|
});
|
|
|
|
test("should ignore stale context (>10 mins old)", async () => {
|
|
const staleContext = {
|
|
channelId: "123",
|
|
userId: "456",
|
|
timestamp: Date.now() - (15 * 60 * 1000), // 15 mins ago
|
|
runMigrations: true,
|
|
installDependencies: true
|
|
};
|
|
|
|
mockReadFile.mockImplementationOnce(() => Promise.resolve(JSON.stringify(staleContext)));
|
|
|
|
const mockChannel = createMockChannel();
|
|
// Create mock with instanceof support
|
|
const channel = Object.assign(mockChannel, { constructor: { name: "TextChannel" } });
|
|
Object.setPrototypeOf(channel, Object.create({ constructor: { name: "TextChannel" } }));
|
|
|
|
const mockClient = createMockClient(channel);
|
|
|
|
await UpdateService.handlePostRestart(mockClient);
|
|
|
|
// Should not send any message for stale context
|
|
expect(mockChannel.send).not.toHaveBeenCalled();
|
|
// Should clean up the context file
|
|
expect(mockUnlink).toHaveBeenCalled();
|
|
});
|
|
|
|
test("should do nothing if no context file exists", async () => {
|
|
mockReadFile.mockImplementationOnce(() => Promise.reject(new Error("ENOENT")));
|
|
|
|
const mockClient = createMockClient();
|
|
|
|
await UpdateService.handlePostRestart(mockClient);
|
|
|
|
// Should not throw and not try to clean up
|
|
expect(mockUnlink).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("should clean up context file after processing", async () => {
|
|
const validContext = {
|
|
channelId: "123",
|
|
userId: "456",
|
|
timestamp: Date.now(),
|
|
runMigrations: false,
|
|
installDependencies: false
|
|
};
|
|
|
|
mockReadFile.mockImplementationOnce(() => Promise.resolve(JSON.stringify(validContext)));
|
|
|
|
// Create a proper TextChannel mock
|
|
const { TextChannel } = await import("discord.js");
|
|
const mockChannel = Object.create(TextChannel.prototype);
|
|
mockChannel.isSendable = () => true;
|
|
mockChannel.send = mock(() => Promise.resolve());
|
|
|
|
const mockClient = createMockClient(mockChannel);
|
|
|
|
await UpdateService.handlePostRestart(mockClient);
|
|
|
|
expect(mockUnlink).toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|