diff --git a/src/common/registry-base.ts b/src/common/registry-base.ts index ffbc622..4f7fe95 100644 --- a/src/common/registry-base.ts +++ b/src/common/registry-base.ts @@ -19,33 +19,14 @@ export abstract class RegistryBase { ) {} /** - * Determines if the current LeetCode service has valid authentication credentials. - * - * @returns True if authenticated, false otherwise - */ - get isAuthenticated(): boolean { - return this.leetcodeService.isAuthenticated(); - } - - /** - * Registers all applicable components based on authentication status. + * Registers all public components. */ public register(): void { this.registerPublic(); - if (this.isAuthenticated) { - this.registerAuthenticated(); - } } /** - * Hook for registering components that don't require authentication. - * Override this in subclasses. + * Hook for registering components. Override in subclasses. */ protected registerPublic(): void {} - - /** - * Hook for registering components that require authentication. - * Override this in subclasses. - */ - protected registerAuthenticated(): void {} } diff --git a/src/index.ts b/src/index.ts index 9b6b0f3..ecc098e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,7 @@ import { registerProblemResources } from "./mcp/resources/problem-resources.js"; import { registerSolutionResources } from "./mcp/resources/solution-resources.js"; import { registerAuthTools } from "./mcp/tools/auth-tools.js"; import { registerContestTools } from "./mcp/tools/contest-tools.js"; +import { registerOnboardingTools } from "./mcp/tools/onboarding-tools.js"; import { registerProblemTools } from "./mcp/tools/problem-tools.js"; import { registerSolutionTools } from "./mcp/tools/solution-tools.js"; import { registerSubmissionTools } from "./mcp/tools/submission-tools.js"; @@ -126,6 +127,8 @@ async function main() { // Register MCP prompts for authentication guidance registerAuthPrompts(server, leetcodeService); + registerOnboardingTools(server, leetcodeService); + registerProblemTools(server, leetcodeService); registerUserTools(server, leetcodeService); registerContestTools(server, leetcodeService); diff --git a/src/leetcode/leetcode-global-service.ts b/src/leetcode/leetcode-global-service.ts index 64e577e..9aecc07 100644 --- a/src/leetcode/leetcode-global-service.ts +++ b/src/leetcode/leetcode-global-service.ts @@ -347,4 +347,9 @@ export class LeetCodeGlobalService implements LeetcodeServiceInterface { !!this.credential.session ); } + + updateCredentials(csrf: string, session: string): void { + this.credential.csrf = csrf; + this.credential.session = session; + } } diff --git a/src/leetcode/leetcode-service-interface.ts b/src/leetcode/leetcode-service-interface.ts index 3b6abb0..70f8141 100644 --- a/src/leetcode/leetcode-service-interface.ts +++ b/src/leetcode/leetcode-service-interface.ts @@ -151,6 +151,15 @@ export interface LeetcodeServiceInterface { */ isAuthenticated(): boolean; + /** + * Updates the in-memory credentials so authenticated API calls + * work immediately after save without a server restart. + * + * @param csrf - The CSRF token + * @param session - The session token + */ + updateCredentials(csrf: string, session: string): void; + /** * Retrieves a list of solutions for a specific problem. * diff --git a/src/mcp/tools/auth-tools.ts b/src/mcp/tools/auth-tools.ts index 8c52dfd..63b42d5 100644 --- a/src/mcp/tools/auth-tools.ts +++ b/src/mcp/tools/auth-tools.ts @@ -160,6 +160,10 @@ export class AuthToolRegistry extends ToolRegistry { createdAt: new Date().toISOString() }); + // Update in-memory credentials so authenticated + // tools work immediately without server restart + this.leetcodeService.updateCredentials(csrftoken, session); + return { content: [ { diff --git a/src/mcp/tools/onboarding-tools.ts b/src/mcp/tools/onboarding-tools.ts new file mode 100644 index 0000000..66ae70a --- /dev/null +++ b/src/mcp/tools/onboarding-tools.ts @@ -0,0 +1,89 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { LeetcodeServiceInterface } from "../../leetcode/leetcode-service-interface.js"; +import { ToolRegistry } from "./tool-registry.js"; + +const USAGE_GUIDE = `# Interactive LeetCode MCP — Usage Guide + +## Prompts (must be explicitly invoked — not auto-active) + +| Prompt | When to invoke | Params | +|--------|---------------|--------| +| leetcode_learning_mode | START of any practice session, before discussing a problem | none | +| leetcode_problem_workflow | Once the user selects a specific problem | problemSlug, difficulty | +| leetcode_workspace_setup | After problem selection, before user starts coding | language, problemSlug, codeTemplate | +| leetcode_authentication_guide | Whenever auth is needed — first use, 401 errors, or expired credentials | none | + +## Session Start Flow + +1. Invoke leetcode_learning_mode +2. User picks a problem (daily challenge or search) +3. Invoke leetcode_problem_workflow (problemSlug, difficulty) +4. Invoke leetcode_workspace_setup (language, problemSlug, codeTemplate) +5. Guide user with progressive hints (4 levels) +6. Submit with submit_solution when ready + +## Learning Mode Rules + +- Never show a full solution without first working through hint levels 1 → 2 → 3 +- Level 1: Guiding questions ("What pattern do you see?") +- Level 2: General approaches ("Consider a hash map...") +- Level 3: Specific hints ("Iterate once while tracking seen values...") +- Level 4: Pseudocode or partial implementation +- Only show complete solutions when explicitly requested AFTER earlier hints have been delivered +- get_problem_solution and list_problem_solutions return full community solutions — only use at Level 4 or on explicit "show me the solution" after hints + +## Auth Flow + +1. Before any auth-sensitive action, call check_auth_status first +2. If not authenticated or expired → invoke leetcode_authentication_guide prompt (do NOT ad-hoc the instructions) +3. The prompt guides: start_leetcode_auth → user extracts cookies → save_leetcode_credentials +4. On success, retry the original action +5. On any 401 from a tool: call check_auth_status → follow steps 2-4 + +## Submission Language Map + +| User says | Pass this to submit_solution | +|-----------|------------------------------| +| Python / Python 3 | python3 | +| Python 2 | python | +| Java | java | +| C++ | cpp | +| JavaScript | javascript | +| TypeScript | typescript | + +Default: "Python" without version → python3`; + +/** + * Onboarding tool registry. Provides a get_started tool that returns + * usage guidance via its response — prompt invocation rules, session + * flow, learning mode rules, auth flow, and language map. + */ +export class OnboardingToolRegistry extends ToolRegistry { + protected registerPublic(): void { + this.server.registerTool( + "get_started", + { + description: + "Returns the usage guide for this MCP server: which prompts to invoke and when, session flow, auth flow, and submission language map. Call this at the start of any LeetCode practice session." + }, + async () => { + return { + content: [ + { + type: "text", + text: USAGE_GUIDE + } + ] + }; + } + ); + } +} + +export function registerOnboardingTools( + server: McpServer, + leetcodeService: LeetcodeServiceInterface +): void { + const registry = new OnboardingToolRegistry(server, leetcodeService); + registry.register(); +} diff --git a/src/mcp/tools/problem-tools.ts b/src/mcp/tools/problem-tools.ts index ad19e6a..a32f337 100644 --- a/src/mcp/tools/problem-tools.ts +++ b/src/mcp/tools/problem-tools.ts @@ -15,7 +15,7 @@ export class ProblemToolRegistry extends ToolRegistry { "get_daily_challenge", { description: - "Retrieves today's LeetCode Daily Challenge problem with complete details, including problem description, constraints, and examples" + "Retrieves today's LeetCode Daily Challenge problem with complete details, including problem description, constraints, and examples. After fetching, invoke the leetcode_learning_mode and leetcode_problem_workflow prompts before helping the user work on it." }, async () => { const data = await this.leetcodeService.fetchDailyChallenge(); @@ -38,7 +38,7 @@ export class ProblemToolRegistry extends ToolRegistry { "get_problem", { description: - "Retrieves details about a specific LeetCode problem, including its description, examples, constraints, and related information", + "Retrieves details about a specific LeetCode problem, including its description, examples, constraints, and related information. After fetching, invoke the leetcode_learning_mode and leetcode_problem_workflow prompts before helping the user work on it.", inputSchema: { titleSlug: z diff --git a/src/mcp/tools/solution-tools.ts b/src/mcp/tools/solution-tools.ts index 08844bb..ba01ffd 100644 --- a/src/mcp/tools/solution-tools.ts +++ b/src/mcp/tools/solution-tools.ts @@ -112,7 +112,7 @@ export class SolutionToolRegistry extends ToolRegistry { "get_problem_solution", { description: - "Retrieves the complete content and metadata of a specific solution, including the full article text, author information, and related navigation links", + "Retrieves the complete content and metadata of a specific solution, including the full article text, author information, and related navigation links. This returns a FULL community solution — only call this after the user has exhausted progressive hints or has explicitly requested the solution after receiving earlier hints.", inputSchema: { topicId: z diff --git a/src/mcp/tools/user-tools.ts b/src/mcp/tools/user-tools.ts index 19fb8ca..645c43d 100644 --- a/src/mcp/tools/user-tools.ts +++ b/src/mcp/tools/user-tools.ts @@ -8,10 +8,6 @@ import { ToolRegistry } from "./tool-registry.js"; * This class manages tools for accessing user profiles, submissions, and progress data. */ export class UserToolRegistry extends ToolRegistry { - protected get requiresAuthentication(): boolean { - return true; - } - protected registerPublic(): void { // User profile tool this.server.registerTool( @@ -154,12 +150,8 @@ export class UserToolRegistry extends ToolRegistry { } } ); - } - /** - * Registers tools specific to the Global LeetCode site that require authentication. - */ - protected registerAuthenticated(): void { + // User status tool this.server.registerTool( "get_user_status", { diff --git a/tests/helpers/mock-leetcode.ts b/tests/helpers/mock-leetcode.ts index 0d2e7ff..b6e495e 100644 --- a/tests/helpers/mock-leetcode.ts +++ b/tests/helpers/mock-leetcode.ts @@ -158,7 +158,8 @@ export function createMockLeetCodeService(): LeetcodeServiceInterface { }), // Authentication - isAuthenticated: vi.fn().mockReturnValue(false) + isAuthenticated: vi.fn().mockReturnValue(false), + updateCredentials: vi.fn() } as unknown as LeetcodeServiceInterface; } @@ -195,6 +196,7 @@ export function createMockFailingService(): LeetcodeServiceInterface { fetchQuestionSolutionArticles: vi.fn().mockRejectedValue(error), fetchSolutionArticleDetail: vi.fn().mockRejectedValue(error), submitSolution: vi.fn().mockRejectedValue(error), - isAuthenticated: vi.fn().mockReturnValue(false) + isAuthenticated: vi.fn().mockReturnValue(false), + updateCredentials: vi.fn() } as unknown as LeetcodeServiceInterface; } diff --git a/tests/helpers/test-client.ts b/tests/helpers/test-client.ts index 2d5f765..76d7d89 100644 --- a/tests/helpers/test-client.ts +++ b/tests/helpers/test-client.ts @@ -151,6 +151,8 @@ export async function createTestClientWithServer( await import("../../src/mcp/prompts/learning-prompts.js"); const { registerAuthPrompts } = await import("../../src/mcp/prompts/auth-prompts.js"); + const { registerOnboardingTools } = + await import("../../src/mcp/tools/onboarding-tools.js"); // Register learning prompts registerLearningPrompts(server, leetcodeService); @@ -158,6 +160,9 @@ export async function createTestClientWithServer( // Register auth prompts registerAuthPrompts(server, leetcodeService); + // Register onboarding tools + registerOnboardingTools(server, leetcodeService); + // Register all tools registerProblemTools(server, leetcodeService); registerUserTools(server, leetcodeService); diff --git a/tests/integration/auth-tools-integration.test.ts b/tests/integration/auth-tools-integration.test.ts index 8a68fd4..7a6b874 100644 --- a/tests/integration/auth-tools-integration.test.ts +++ b/tests/integration/auth-tools-integration.test.ts @@ -2,13 +2,37 @@ * Auth Tools Integration Tests * Tests all authentication-related tools through MCP protocol */ -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { registerAuthTools } from "../../src/mcp/tools/auth-tools.js"; import { createMockLeetCodeService } from "../helpers/mock-leetcode.js"; import type { TestClientPair } from "../helpers/test-client.js"; import { createTestClient } from "../helpers/test-client.js"; import { INTEGRATION_TEST_TIMEOUT, assertions } from "./setup.js"; +vi.mock("axios", () => ({ + default: { + post: vi.fn().mockResolvedValue({ + data: { + data: { + userStatus: { + username: "testuser", + isSignedIn: true + } + } + } + }) + }, + isAxiosError: vi.fn().mockReturnValue(false) +})); + +vi.mock("../../src/utils/credentials.js", () => ({ + credentialsStorage: { + save: vi.fn().mockResolvedValue(undefined), + load: vi.fn().mockResolvedValue(null), + exists: vi.fn().mockResolvedValue(false) + } +})); + describe("Auth Tools Integration", () => { let testClient: TestClientPair; let mockService: ReturnType; @@ -132,4 +156,30 @@ describe("Auth Tools Integration", () => { INTEGRATION_TEST_TIMEOUT ); }); + + describe("save_leetcode_credentials updates in-memory credentials", () => { + it( + "should call updateCredentials on the service after successful save", + async () => { + const result: any = await testClient.client.callTool({ + name: "save_leetcode_credentials", + arguments: { + csrftoken: "test-csrf-token", + session: "test-session-token" + } + }); + + assertions.hasToolResultStructure(result); + const data = JSON.parse(result.content[0].text as string); + + expect(data.status).toBe("success"); + expect(data.username).toBe("testuser"); + expect(mockService.updateCredentials).toHaveBeenCalledWith( + "test-csrf-token", + "test-session-token" + ); + }, + INTEGRATION_TEST_TIMEOUT + ); + }); }); diff --git a/tests/integration/onboarding-tools-integration.test.ts b/tests/integration/onboarding-tools-integration.test.ts new file mode 100644 index 0000000..a537d53 --- /dev/null +++ b/tests/integration/onboarding-tools-integration.test.ts @@ -0,0 +1,122 @@ +/** + * Onboarding Tools Integration Tests + * Tests the get_started tool through MCP protocol + */ +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { registerOnboardingTools } from "../../src/mcp/tools/onboarding-tools.js"; +import { createMockLeetCodeService } from "../helpers/mock-leetcode.js"; +import type { TestClientPair } from "../helpers/test-client.js"; +import { createTestClient } from "../helpers/test-client.js"; +import { INTEGRATION_TEST_TIMEOUT, assertions } from "./setup.js"; + +describe("Onboarding Tools Integration", () => { + let testClient: TestClientPair; + + beforeEach(async () => { + const mockService = createMockLeetCodeService(); + testClient = await createTestClient({}, (server) => { + registerOnboardingTools(server, mockService as any); + }); + }, INTEGRATION_TEST_TIMEOUT); + + afterEach(async () => { + if (testClient) { + await testClient.cleanup(); + } + }); + + describe("get_started", () => { + it( + "should be registered and description tells Claude to call it first", + async () => { + const { tools } = await testClient.client.listTools(); + const tool = tools.find((t) => t.name === "get_started"); + + expect(tool).toBeDefined(); + expect(tool?.description).toContain("start"); + expect(tool?.description).toContain("prompt"); + }, + INTEGRATION_TEST_TIMEOUT + ); + + it( + "should return prompt invocation rules", + async () => { + const result: any = await testClient.client.callTool({ + name: "get_started", + arguments: {} + }); + + assertions.hasToolResultStructure(result); + const text = result.content[0].text as string; + + expect(text).toContain("leetcode_learning_mode"); + expect(text).toContain("leetcode_problem_workflow"); + expect(text).toContain("leetcode_workspace_setup"); + expect(text).toContain("leetcode_authentication_guide"); + }, + INTEGRATION_TEST_TIMEOUT + ); + + it( + "should return session start flow", + async () => { + const result: any = await testClient.client.callTool({ + name: "get_started", + arguments: {} + }); + + const text = result.content[0].text as string; + expect(text).toContain("Session Start Flow"); + }, + INTEGRATION_TEST_TIMEOUT + ); + + it( + "should return learning mode hint levels", + async () => { + const result: any = await testClient.client.callTool({ + name: "get_started", + arguments: {} + }); + + const text = result.content[0].text as string; + expect(text).toContain("Level 1"); + expect(text).toContain("Level 2"); + expect(text).toContain("Level 3"); + expect(text).toContain("Level 4"); + }, + INTEGRATION_TEST_TIMEOUT + ); + + it( + "should return auth flow with check_auth_status gate", + async () => { + const result: any = await testClient.client.callTool({ + name: "get_started", + arguments: {} + }); + + const text = result.content[0].text as string; + expect(text).toContain("check_auth_status"); + expect(text).toContain("save_leetcode_credentials"); + }, + INTEGRATION_TEST_TIMEOUT + ); + + it( + "should return submission language map with python3 default", + async () => { + const result: any = await testClient.client.callTool({ + name: "get_started", + arguments: {} + }); + + const text = result.content[0].text as string; + expect(text).toContain("python3"); + expect(text).toContain("Language Map"); + }, + INTEGRATION_TEST_TIMEOUT + ); + }); +}); diff --git a/tests/integration/problem-tools-integration.test.ts b/tests/integration/problem-tools-integration.test.ts index 483ed97..c5dbf0a 100644 --- a/tests/integration/problem-tools-integration.test.ts +++ b/tests/integration/problem-tools-integration.test.ts @@ -43,6 +43,9 @@ describe("Problem Tools Integration", () => { expect(dailyChallengeTool?.description).toContain( "Daily Challenge" ); + expect(dailyChallengeTool?.description).toContain( + "leetcode_learning_mode" + ); }, INTEGRATION_TEST_TIMEOUT ); @@ -100,6 +103,9 @@ describe("Problem Tools Integration", () => { const problemTool = tools.find((t) => t.name === "get_problem"); expect(problemTool).toBeDefined(); expect(problemTool?.description).toContain("problem"); + expect(problemTool?.description).toContain( + "leetcode_learning_mode" + ); }, INTEGRATION_TEST_TIMEOUT ); diff --git a/tests/integration/solution-tools-integration.test.ts b/tests/integration/solution-tools-integration.test.ts index d920563..ba5a4bc 100644 --- a/tests/integration/solution-tools-integration.test.ts +++ b/tests/integration/solution-tools-integration.test.ts @@ -102,6 +102,7 @@ describe("Solution Tools Integration", () => { ); expect(tool).toBeDefined(); expect(tool?.description).toContain("solution"); + expect(tool?.description).toContain("hints"); }, INTEGRATION_TEST_TIMEOUT ); diff --git a/tests/integration/user-tools-integration.test.ts b/tests/integration/user-tools-integration.test.ts index 7f34d22..db0de61 100644 --- a/tests/integration/user-tools-integration.test.ts +++ b/tests/integration/user-tools-integration.test.ts @@ -2,7 +2,7 @@ * User Tools Integration Tests * Tests all user-related tools through MCP protocol */ -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { registerUserTools } from "../../src/mcp/tools/user-tools.js"; import { createMockLeetCodeService } from "../helpers/mock-leetcode.js"; import type { TestClientPair } from "../helpers/test-client.js"; @@ -123,4 +123,274 @@ describe("User Tools Integration", () => { INTEGRATION_TEST_TIMEOUT ); }); + + describe("get_user_status", () => { + it( + "should list get_user_status tool", + async () => { + const { tools } = await testClient.client.listTools(); + const tool = tools.find((t) => t.name === "get_user_status"); + + expect(tool).toBeDefined(); + expect(tool?.description).toContain("status"); + }, + INTEGRATION_TEST_TIMEOUT + ); + + it( + "should execute get_user_status successfully", + async () => { + const result: any = await testClient.client.callTool({ + name: "get_user_status", + arguments: {} + }); + + assertions.hasToolResultStructure(result); + const data = JSON.parse(result.content[0].text as string); + expect(data.status).toBeDefined(); + expect(mockService.fetchUserStatus).toHaveBeenCalledOnce(); + }, + INTEGRATION_TEST_TIMEOUT + ); + + it( + "should return error when service throws auth error", + async () => { + const failingService = createMockLeetCodeService(); + vi.mocked(failingService.fetchUserStatus).mockRejectedValue( + new Error("Authentication required to fetch user status") + ); + const failingClient = await createTestClient({}, (server) => { + registerUserTools(server, failingService as any); + }); + + try { + const result: any = await failingClient.client.callTool({ + name: "get_user_status", + arguments: {} + }); + + assertions.hasToolResultStructure(result); + const data = JSON.parse(result.content[0].text as string); + expect(data.error).toContain("Authentication required"); + } finally { + await failingClient.cleanup(); + } + }, + INTEGRATION_TEST_TIMEOUT + ); + }); + + describe("get_problem_submission_report", () => { + it( + "should list get_problem_submission_report tool", + async () => { + const { tools } = await testClient.client.listTools(); + const tool = tools.find( + (t) => t.name === "get_problem_submission_report" + ); + + expect(tool).toBeDefined(); + expect(tool?.description).toContain("submission"); + }, + INTEGRATION_TEST_TIMEOUT + ); + + it( + "should execute get_problem_submission_report successfully", + async () => { + const result: any = await testClient.client.callTool({ + name: "get_problem_submission_report", + arguments: { id: 12345 } + }); + + assertions.hasToolResultStructure(result); + const data = JSON.parse(result.content[0].text as string); + expect(data.submissionId).toBe(12345); + expect( + mockService.fetchUserSubmissionDetail + ).toHaveBeenCalledWith(12345); + }, + INTEGRATION_TEST_TIMEOUT + ); + + it( + "should return error when service throws auth error", + async () => { + const failingService = createMockLeetCodeService(); + vi.mocked( + failingService.fetchUserSubmissionDetail + ).mockRejectedValue( + new Error( + "Authentication required to fetch user submission detail" + ) + ); + const failingClient = await createTestClient({}, (server) => { + registerUserTools(server, failingService as any); + }); + + try { + const result: any = await failingClient.client.callTool({ + name: "get_problem_submission_report", + arguments: { id: 12345 } + }); + + assertions.hasToolResultStructure(result); + const data = JSON.parse(result.content[0].text as string); + expect(data.error).toContain("Authentication required"); + } finally { + await failingClient.cleanup(); + } + }, + INTEGRATION_TEST_TIMEOUT + ); + }); + + describe("get_problem_progress", () => { + it( + "should list get_problem_progress tool", + async () => { + const { tools } = await testClient.client.listTools(); + const tool = tools.find( + (t) => t.name === "get_problem_progress" + ); + + expect(tool).toBeDefined(); + expect(tool?.description).toContain("problem-solving"); + }, + INTEGRATION_TEST_TIMEOUT + ); + + it( + "should execute get_problem_progress successfully", + async () => { + const result: any = await testClient.client.callTool({ + name: "get_problem_progress", + arguments: { + offset: 0, + limit: 10, + questionStatus: "SOLVED", + difficulty: ["EASY"] + } + }); + + assertions.hasToolResultStructure(result); + expect( + mockService.fetchUserProgressQuestionList + ).toHaveBeenCalledWith({ + offset: 0, + limit: 10, + questionStatus: "SOLVED", + difficulty: ["EASY"] + }); + }, + INTEGRATION_TEST_TIMEOUT + ); + + it( + "should return error when service throws auth error", + async () => { + const failingService = createMockLeetCodeService(); + vi.mocked( + failingService.fetchUserProgressQuestionList + ).mockRejectedValue( + new Error( + "Authentication required to fetch user progress question list" + ) + ); + const failingClient = await createTestClient({}, (server) => { + registerUserTools(server, failingService as any); + }); + + try { + const result: any = await failingClient.client.callTool({ + name: "get_problem_progress", + arguments: { offset: 0, limit: 10 } + }); + + assertions.hasToolResultStructure(result); + const data = JSON.parse(result.content[0].text as string); + expect(data.error).toContain( + "Failed to fetch user progress questions" + ); + } finally { + await failingClient.cleanup(); + } + }, + INTEGRATION_TEST_TIMEOUT + ); + }); + + describe("get_all_submissions", () => { + it( + "should list get_all_submissions tool", + async () => { + const { tools } = await testClient.client.listTools(); + const tool = tools.find( + (t) => t.name === "get_all_submissions" + ); + + expect(tool).toBeDefined(); + expect(tool?.description).toContain("submissions"); + }, + INTEGRATION_TEST_TIMEOUT + ); + + it( + "should execute get_all_submissions successfully", + async () => { + const result: any = await testClient.client.callTool({ + name: "get_all_submissions", + arguments: { + limit: 10, + offset: 0, + questionSlug: "two-sum" + } + }); + + assertions.hasToolResultStructure(result); + expect( + mockService.fetchUserAllSubmissions + ).toHaveBeenCalledWith({ + offset: 0, + limit: 10, + questionSlug: "two-sum" + }); + }, + INTEGRATION_TEST_TIMEOUT + ); + + it( + "should return error when service throws auth error", + async () => { + const failingService = createMockLeetCodeService(); + vi.mocked( + failingService.fetchUserAllSubmissions + ).mockRejectedValue( + new Error( + "Authentication required to fetch user submissions" + ) + ); + const failingClient = await createTestClient({}, (server) => { + registerUserTools(server, failingService as any); + }); + + try { + const result: any = await failingClient.client.callTool({ + name: "get_all_submissions", + arguments: { limit: 10, offset: 0 } + }); + + assertions.hasToolResultStructure(result); + const data = JSON.parse(result.content[0].text as string); + expect(data.error).toContain( + "Failed to fetch user submissions" + ); + } finally { + await failingClient.cleanup(); + } + }, + INTEGRATION_TEST_TIMEOUT + ); + }); }); diff --git a/tests/mcp/tools/auth-tools.test.ts b/tests/mcp/tools/auth-tools.test.ts index 1fc6903..9df2144 100644 --- a/tests/mcp/tools/auth-tools.test.ts +++ b/tests/mcp/tools/auth-tools.test.ts @@ -47,7 +47,8 @@ describe("AuthToolRegistry", () => { } as unknown as McpServer; mockLeetCodeService = { - isAuthenticated: vi.fn().mockReturnValue(false) + isAuthenticated: vi.fn().mockReturnValue(false), + updateCredentials: vi.fn() } as unknown as LeetcodeServiceInterface; registry = new AuthToolRegistry(mockServer, mockLeetCodeService);