devfocus
Version:
CLI tool for developers to block distracting websites
520 lines (421 loc) • 15.6 kB
JavaScript
// Mock dependencies first, before any imports to ensure they are mocked correctly
const mockExecPromise = jest.fn();
jest.mock("fs", () => ({
promises: {
readFile: jest.fn(),
writeFile: jest.fn(),
access: jest.fn(),
unlink: jest.fn(),
constants: {
W_OK: 2,
},
},
}));
jest.mock("child_process", () => ({
exec: jest.fn(),
}));
jest.mock("os", () => ({
platform: jest.fn(),
tmpdir: jest.fn(),
}));
jest.mock("util", () => ({
promisify: jest.fn(() => mockExecPromise),
}));
// Import modules after mocking
const fs = require("fs").promises;
const os = require("os");
// Import the module after mocking
const {
hasAdminPrivileges,
readHostsFile,
writeHostsFile,
readBlockList,
writeBlockList,
getBlockedWebsites,
isFocusModeOn,
addWebsites,
removeWebsites,
clearWebsites,
listWebsites,
applyBlockList,
clearHostsFile,
flushDnsCache,
} = require("./cli");
// Mock console methods
const consoleSpy = {
log: jest.spyOn(console, "log").mockImplementation(),
error: jest.spyOn(console, "error").mockImplementation(),
info: jest.spyOn(console, "info").mockImplementation(),
warn: jest.spyOn(console, "warn").mockImplementation(),
};
describe("Website Blocker CLI Tool", () => {
beforeEach(() => {
jest.clearAllMocks();
mockExecPromise.mockClear();
// Reset console spies
Object.values(consoleSpy).forEach((spy) => spy.mockClear());
});
describe("hasAdminPrivileges", () => {
it("should return true for Windows admin user", async () => {
os.platform.mockReturnValue("win32");
fs.writeFile.mockResolvedValue();
fs.unlink.mockResolvedValue();
const result = await hasAdminPrivileges();
expect(result).toBe(true);
expect(fs.writeFile).toHaveBeenCalledWith(
"C:\\Windows\\System32\\test.txt",
"test"
);
expect(fs.unlink).toHaveBeenCalledWith("C:\\Windows\\System32\\test.txt");
});
it("should return false for Windows non-admin user", async () => {
os.platform.mockReturnValue("win32");
fs.writeFile.mockRejectedValue(new Error("Access denied"));
const result = await hasAdminPrivileges();
expect(result).toBe(false);
});
it("should return true for Unix root user", async () => {
os.platform.mockReturnValue("linux");
mockExecPromise.mockResolvedValue({ stdout: "0\n" });
const result = await hasAdminPrivileges();
expect(result).toBe(true);
expect(mockExecPromise).toHaveBeenCalledWith("id -u");
});
it("should return false for Unix non-root user", async () => {
os.platform.mockReturnValue("linux");
mockExecPromise.mockResolvedValue({ stdout: "1000\n" });
const result = await hasAdminPrivileges();
expect(result).toBe(false);
});
it("should fallback to fs.access when id command fails", async () => {
os.platform.mockReturnValue("linux");
mockExecPromise.mockRejectedValue(new Error("Command not found"));
fs.access.mockResolvedValue();
const result = await hasAdminPrivileges();
expect(result).toBe(true);
expect(fs.access).toHaveBeenCalled();
});
});
describe("readHostsFile", () => {
it("should read hosts file successfully", async () => {
const mockContent = "127.0.0.1 localhost";
fs.readFile.mockResolvedValue(mockContent);
const result = await readHostsFile();
expect(result).toBe(mockContent);
expect(fs.readFile).toHaveBeenCalledWith(
expect.stringContaining("hosts"),
"utf8"
);
});
it("should throw error when hosts file cannot be read", async () => {
fs.readFile.mockRejectedValue(new Error("Permission denied"));
await expect(readHostsFile()).rejects.toThrow(
"Error reading hosts file: Permission denied"
);
});
});
describe("writeHostsFile", () => {
it("should write to hosts file on Windows using PowerShell", async () => {
os.platform.mockReturnValue("win32");
os.tmpdir.mockReturnValue("C:\\Windows\\Temp");
fs.writeFile.mockResolvedValue();
mockExecPromise.mockResolvedValue();
await writeHostsFile("test content");
expect(mockExecPromise).toHaveBeenCalledWith(
expect.stringContaining("powershell")
);
});
it("should write to hosts file on Unix using tee with sudo", async () => {
os.platform.mockReturnValue("linux");
mockExecPromise.mockResolvedValue();
await writeHostsFile("test content");
expect(mockExecPromise).toHaveBeenCalledWith(
expect.stringContaining("sudo tee")
);
});
it("should throw error when write fails", async () => {
os.platform.mockReturnValue("linux");
mockExecPromise.mockRejectedValue(new Error("Permission denied"));
await expect(writeHostsFile("test")).rejects.toThrow(
"Failed to write to hosts file"
);
});
});
describe("readBlockList", () => {
it("should read block list from file", async () => {
const mockData = { websites: ["example.com", "test.com"] };
fs.readFile.mockResolvedValue(JSON.stringify(mockData));
const result = await readBlockList();
expect(result).toEqual(["example.com", "test.com"]);
});
it("should return empty array when file doesn't exist", async () => {
const error = new Error("File not found");
error.code = "ENOENT";
fs.readFile.mockRejectedValue(error);
const result = await readBlockList();
expect(result).toEqual([]);
});
it("should throw error for other file read errors", async () => {
fs.readFile.mockRejectedValue(new Error("Permission denied"));
await expect(readBlockList()).rejects.toThrow("Error reading block list");
});
});
describe("writeBlockList", () => {
it("should write block list to file", async () => {
fs.writeFile.mockResolvedValue();
await writeBlockList(["example.com", "test.com"]);
expect(fs.writeFile).toHaveBeenCalledWith(
expect.stringContaining("blocklist.json"),
JSON.stringify({ websites: ["example.com", "test.com"] }, null, 2)
);
});
it("should throw error when write fails", async () => {
fs.writeFile.mockRejectedValue(new Error("Permission denied"));
await expect(writeBlockList(["example.com"])).rejects.toThrow(
"Error writing block list"
);
});
});
describe("getBlockedWebsites", () => {
it("should extract blocked websites from hosts file", async () => {
const hostsContent = `
127.0.0.1 localhost
# WEBSITE_BLOCKER_START
127.0.0.1 example.com
127.0.0.1 www.example.com
127.0.0.1 test.com
127.0.0.1 www.test.com
# WEBSITE_BLOCKER_END`;
fs.readFile.mockResolvedValue(hostsContent);
const result = await getBlockedWebsites();
expect(result).toEqual(["example.com", "test.com"]);
});
it("should return empty array when no blocked section exists", async () => {
fs.readFile.mockResolvedValue("127.0.0.1 localhost");
const result = await getBlockedWebsites();
expect(result).toEqual([]);
});
});
describe("isFocusModeOn", () => {
it("should return true when websites are blocked", async () => {
const hostsContent = `
# WEBSITE_BLOCKER_START
127.0.0.1 example.com
# WEBSITE_BLOCKER_END
`;
fs.readFile.mockResolvedValue(hostsContent);
const result = await isFocusModeOn();
expect(result).toBe(true);
});
it("should return false when no websites are blocked", async () => {
fs.readFile.mockResolvedValue("127.0.0.1 localhost");
const result = await isFocusModeOn();
expect(result).toBe(false);
});
});
describe("addWebsites", () => {
it("should add new websites to block list", async () => {
fs.readFile.mockResolvedValue(
JSON.stringify({ websites: ["existing.com"] })
);
fs.writeFile.mockResolvedValue();
await addWebsites(["new.com", "another.com"]);
expect(fs.writeFile).toHaveBeenCalledWith(
expect.stringContaining("blocklist.json"),
JSON.stringify(
{ websites: ["existing.com", "new.com", "another.com"] },
null,
2
)
);
expect(consoleSpy.log).toHaveBeenCalledWith(
expect.stringContaining("Added websites to block list")
);
});
it("should not add duplicate websites", async () => {
fs.readFile.mockResolvedValue(
JSON.stringify({ websites: ["existing.com"] })
);
fs.writeFile.mockResolvedValue();
await addWebsites(["existing.com"]);
expect(consoleSpy.log).toHaveBeenCalledWith(
"All provided websites are already in the block list."
);
});
});
describe("removeWebsites", () => {
it("should remove websites from block list", async () => {
fs.readFile.mockResolvedValue(
JSON.stringify({
websites: ["example.com", "test.com", "keep.com"],
})
);
fs.writeFile.mockResolvedValue();
await removeWebsites(["example.com", "test.com"]);
expect(fs.writeFile).toHaveBeenCalledWith(
expect.stringContaining("blocklist.json"),
JSON.stringify({ websites: ["keep.com"] }, null, 2)
);
expect(consoleSpy.log).toHaveBeenCalledWith(
expect.stringContaining("Removed websites from block list")
);
});
it("should handle removal of non-existent websites", async () => {
fs.readFile.mockResolvedValue(
JSON.stringify({ websites: ["existing.com"] })
);
await removeWebsites(["nonexistent.com"]);
expect(consoleSpy.log).toHaveBeenCalledWith(
"No matching websites found in the block list."
);
});
});
describe("clearWebsites", () => {
it("should clear all websites and block list", async () => {
fs.writeFile.mockResolvedValue();
fs.readFile.mockResolvedValue("127.0.0.1 localhost");
mockExecPromise.mockResolvedValue();
os.platform.mockReturnValue("linux");
await clearWebsites();
expect(fs.writeFile).toHaveBeenCalledWith(
expect.stringContaining("blocklist.json"),
JSON.stringify({ websites: [] }, null, 2)
);
expect(consoleSpy.log).toHaveBeenCalledWith(
expect.stringContaining("Cleared all blocked websites")
);
});
});
describe("listWebsites", () => {
it("should print websites when block list exists", async () => {
fs.readFile
.mockResolvedValueOnce(
JSON.stringify({ websites: ["example.com", "test.com"] })
)
.mockResolvedValueOnce("127.0.0.1 localhost");
await listWebsites();
expect(consoleSpy.log).toHaveBeenCalledWith(
expect.stringContaining("Block list (Focus mode: OFF)")
);
expect(consoleSpy.log).toHaveBeenCalledWith("- example.com");
expect(consoleSpy.log).toHaveBeenCalledWith("- test.com");
});
it("should print message when no websites in block list", async () => {
fs.readFile.mockResolvedValue(JSON.stringify({ websites: [] }));
await listWebsites();
expect(consoleSpy.log).toHaveBeenCalledWith(
"No websites in the block list."
);
});
});
describe("applyBlockList", () => {
it("should apply block list to hosts file", async () => {
fs.readFile
.mockResolvedValueOnce("127.0.0.1 localhost")
.mockResolvedValueOnce(JSON.stringify({ websites: ["example.com"] }));
mockExecPromise.mockResolvedValue();
os.platform.mockReturnValue("linux");
await applyBlockList();
expect(mockExecPromise).toHaveBeenCalledWith(
expect.stringContaining("sudo tee")
);
expect(consoleSpy.log).toHaveBeenCalledWith(
expect.stringContaining("Focus mode is ON")
);
});
it("should apply with specified timer", async () => {
fs.readFile
.mockResolvedValueOnce("127.0.0.1 localhost")
.mockResolvedValueOnce(JSON.stringify({ websites: ["example.com"] }));
mockExecPromise.mockResolvedValue();
os.platform.mockReturnValue("linux");
await applyBlockList({ timer: 0.001 }); // 0.001 hours = 3.6 seconds
expect(consoleSpy.log).toHaveBeenCalledWith(
expect.stringContaining("Focus mode is ON")
);
// Pause to allow timer to complete
await new Promise((resolve) => setTimeout(resolve, 4000));
expect(consoleSpy.log).toHaveBeenCalledWith(
expect.stringContaining("Focus mode is OFF")
);
});
});
describe("clearHostsFile", () => {
it("should remove blocked websites from hosts file", async () => {
const hostsWithBlocks = `127.0.0.1 localhost
# WEBSITE_BLOCKER_START
127.0.0.1 example.com
# WEBSITE_BLOCKER_END`;
fs.readFile.mockResolvedValue(hostsWithBlocks);
mockExecPromise.mockResolvedValue();
os.platform.mockReturnValue("linux");
await clearHostsFile();
expect(mockExecPromise).toHaveBeenCalledWith(
expect.stringContaining("sudo tee")
);
expect(consoleSpy.log).toHaveBeenCalledWith(
expect.stringContaining("Focus mode is OFF")
);
});
});
describe("flushDnsCache", () => {
it("should flush DNS cache on macOS", async () => {
os.platform.mockReturnValue("darwin");
mockExecPromise.mockResolvedValue();
await flushDnsCache();
expect(mockExecPromise).toHaveBeenCalledWith(
"sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder"
);
});
it("should flush DNS cache on Linux", async () => {
os.platform.mockReturnValue("linux");
mockExecPromise.mockResolvedValue();
await flushDnsCache();
expect(mockExecPromise).toHaveBeenCalledWith(
"sudo systemd-resolve --flush-caches || sudo resolvectl flush-caches"
);
});
it("should flush DNS cache on Windows", async () => {
os.platform.mockReturnValue("win32");
mockExecPromise.mockResolvedValue();
await flushDnsCache();
expect(mockExecPromise).toHaveBeenCalledWith("ipconfig /flushdns");
});
it("should handle DNS flush errors gracefully", async () => {
os.platform.mockReturnValue("linux");
mockExecPromise.mockRejectedValue(new Error("Command failed"));
await flushDnsCache();
expect(consoleSpy.warn).toHaveBeenCalledWith(
expect.stringContaining("Warning: Could not flush DNS cache")
);
});
});
describe("Edge cases and error handling", () => {
it("should handle malformed JSON in block list file", async () => {
fs.readFile.mockResolvedValue("invalid json");
await expect(readBlockList()).rejects.toThrow("Error reading block list");
});
it("should handle empty hosts file", async () => {
fs.readFile.mockResolvedValue("");
const result = await getBlockedWebsites();
expect(result).toEqual([]);
});
it("should handle hosts file without blocked section", async () => {
fs.readFile.mockResolvedValue("127.0.0.1 localhost\n::1 localhost");
const result = await getBlockedWebsites();
expect(result).toEqual([]);
});
it("should deduplicate websites in getBlockedWebsites", async () => {
const hostsContent = `
# WEBSITE_BLOCKER_START
127.0.0.1 example.com
127.0.0.1 www.example.com
::1 example.com
::1 www.example.com
# WEBSITE_BLOCKER_END`;
fs.readFile.mockResolvedValue(hostsContent);
const result = await getBlockedWebsites();
expect(result).toEqual(["example.com"]);
});
});
});