import { performLogin } from "./oauth.js";
import { loadConfig, saveConfig } from "./auth.js";
import axios from "axios";
import open from "open";
import express from "express";
jest.mock("./auth.js");
jest.mock("axios");
jest.mock("open");
jest.mock("express");
describe("OAuth", () => {
let mockApp: any;
let mockServer: any;
let mockListen: jest.Mock;
let mockGet: jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
mockGet = jest.fn();
mockServer = { close: jest.fn((cb) => cb?.()) };
mockListen = jest.fn((port, cb) => {
cb?.();
return mockServer;
});
mockApp = {
listen: mockListen,
get: mockGet,
};
(express as unknown as jest.Mock).mockReturnValue(mockApp);
});
it("should process successful login flow", async () => {
// Setup config
(loadConfig as jest.Mock).mockReturnValue({
clientId: "cid",
clientSecret: "csec",
});
// Setup axios token response
(axios.post as jest.Mock).mockResolvedValue({
data: { access_token: "new_token" },
});
// Start the login process
const loginPromise = performLogin();
// Verify server started
expect(mockApp.listen).toHaveBeenCalledWith(3000, expect.any(Function));
// Verify browser opened (we can check args if we want)
expect(open).toHaveBeenCalledWith(expect.stringContaining("client_id=cid"));
// Simulate callback request
const callbackHandler = mockGet.mock.calls.find(call => call[0] === "/callback")[1];
const mockRes = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
const mockReq = { query: { code: "auth_code" } };
await callbackHandler(mockReq, mockRes);
// Verify token exchange
expect(axios.post).toHaveBeenCalledWith(
"https://api.clickup.com/api/v2/oauth/token",
null,
{
params: {
client_id: "cid",
client_secret: "csec",
code: "auth_code",
},
}
);
// Verify config saved
expect(saveConfig).toHaveBeenCalledWith({
clientId: "cid",
clientSecret: "csec",
accessToken: "new_token",
});
// Verify success response
expect(mockRes.send).toHaveBeenCalledWith(expect.stringContaining("Authentication successful"));
await loginPromise;
expect(mockServer.close).toHaveBeenCalled();
});
it("should reject if config is missing", async () => {
(loadConfig as jest.Mock).mockReturnValue({});
await expect(performLogin()).rejects.toThrow("ClickUp client ID and secret not configured");
});
it("should handle token exchange failure", async () => {
(loadConfig as jest.Mock).mockReturnValue({
clientId: "cid",
clientSecret: "csec",
});
const error = new Error("Token failed");
(axios.post as jest.Mock).mockRejectedValue(error);
const loginPromise = performLogin();
// Simulate callback
const callbackHandler = mockGet.mock.calls.find(call => call[0] === "/callback")[1];
const mockRes = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
const mockReq = { query: { code: "bad_code" } };
// We expect the promise to be rejected when the handler fails?
// Actually performLogin returns a promise that resolves/rejects based on server.close callbacks usually?
// In the code: server.close(() => reject(error));
await callbackHandler(mockReq, mockRes);
await expect(loginPromise).rejects.toThrow("Token failed");
});
it("should return 400 if code is missing", async () => {
(loadConfig as jest.Mock).mockReturnValue({
clientId: "cid",
clientSecret: "csec",
});
const loginPromise = performLogin();
// Simulate callback without code
const callbackHandler = mockGet.mock.calls.find(call => call[0] === "/callback")[1];
const mockRes = {
send: jest.fn(),
status: jest.fn().mockReturnThis(),
};
const mockReq = { query: {} };
await callbackHandler(mockReq, mockRes);
expect(mockRes.status).toHaveBeenCalledWith(400);
expect(mockRes.send).toHaveBeenCalledWith("Missing code parameter.");
// The server is still running, so we manually close it to finish the test/promise?
// In the implementation we didn't close logic.
// We can just end the test here as we verified the behavior.
// To clean up the open promise in "real" scenarios we'd need to emit close.
});
});