Skip to main content
Glama
integration.test.ts17 kB
/** * Integration Tests for PartnerCore Proxy * * Tests the proxy with real AL workspace structures. * These tests use mock/fixture data to simulate real BC projects. */ import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import { loadConfig } from '../src/config/loader.js'; import { ToolRouter } from '../src/router/tool-router.js'; import { sanitizePath } from '../src/utils/security.js'; // Test fixture paths const TEST_FIXTURES_DIR = path.join(__dirname, 'fixtures'); const TEMP_WORKSPACE = path.join(os.tmpdir(), 'partnercore-proxy-test-' + Date.now()); /** * Create a mock AL workspace structure for testing */ function createMockALWorkspace(workspacePath: string): void { // Create directory structure const dirs = [ '', 'src', 'src/table', 'src/page', 'src/codeunit', 'src/enum', '.vscode', ]; for (const dir of dirs) { const fullPath = path.join(workspacePath, dir); if (!fs.existsSync(fullPath)) { fs.mkdirSync(fullPath, { recursive: true }); } } // Create app.json const appJson = { id: "11111111-1111-1111-1111-111111111111", name: "Test BC App", publisher: "Test Publisher", version: "1.0.0.0", brief: "Test application for integration tests", description: "Integration test fixture for PartnerCore Proxy", privacyStatement: "", EULA: "", help: "", url: "", logo: "", dependencies: [], screenshots: [], platform: "1.0.0.0", application: "22.0.0.0", idRanges: [ { from: 50100, to: 50199 } ], resourceExposurePolicy: { allowDebugging: true, allowDownloadingSource: true, includeSourceInSymbolFile: true }, runtime: "12.0", target: "Cloud" }; fs.writeFileSync( path.join(workspacePath, 'app.json'), JSON.stringify(appJson, null, 2) ); // Create a sample table const sampleTable = `table 50100 "Test Table" { Caption = 'Test Table'; DataClassification = CustomerContent; fields { field(1; "Code"; Code[20]) { Caption = 'Code'; DataClassification = CustomerContent; } field(2; "Description"; Text[100]) { Caption = 'Description'; DataClassification = CustomerContent; } field(3; "Amount"; Decimal) { Caption = 'Amount'; DataClassification = CustomerContent; } field(4; "Created Date"; Date) { Caption = 'Created Date'; DataClassification = CustomerContent; } } keys { key(PK; "Code") { Clustered = true; } } }`; fs.writeFileSync( path.join(workspacePath, 'src/table/TestTable.Table.al'), sampleTable ); // Create a sample page const samplePage = `page 50100 "Test Card" { Caption = 'Test Card'; PageType = Card; SourceTable = "Test Table"; UsageCategory = Administration; ApplicationArea = All; layout { area(Content) { group(General) { field("Code"; Rec."Code") { ApplicationArea = All; ToolTip = 'Specifies the code.'; } field("Description"; Rec."Description") { ApplicationArea = All; ToolTip = 'Specifies the description.'; } field("Amount"; Rec."Amount") { ApplicationArea = All; ToolTip = 'Specifies the amount.'; } } } } actions { area(Processing) { action(DoSomething) { Caption = 'Do Something'; ApplicationArea = All; Image = Process; trigger OnAction() begin Message('Action executed!'); end; } } } }`; fs.writeFileSync( path.join(workspacePath, 'src/page/TestCard.Page.al'), samplePage ); // Create a sample codeunit const sampleCodeunit = `codeunit 50100 "Test Management" { procedure ProcessRecord(var TestRec: Record "Test Table") begin if TestRec.Amount < 0 then Error('Amount cannot be negative'); TestRec."Created Date" := Today; TestRec.Modify(true); end; procedure CalculateTotal(): Decimal var TestRec: Record "Test Table"; Total: Decimal; begin Total := 0; if TestRec.FindSet() then repeat Total += TestRec.Amount; until TestRec.Next() = 0; exit(Total); end; }`; fs.writeFileSync( path.join(workspacePath, 'src/codeunit/TestManagement.Codeunit.al'), sampleCodeunit ); // Create a sample enum const sampleEnum = `enum 50100 "Test Status" { Extensible = true; Caption = 'Test Status'; value(0; "New") { Caption = 'New'; } value(1; "In Progress") { Caption = 'In Progress'; } value(2; "Completed") { Caption = 'Completed'; } }`; fs.writeFileSync( path.join(workspacePath, 'src/enum/TestStatus.Enum.al'), sampleEnum ); // Create .vscode/launch.json const launchJson = { version: "0.2.0", configurations: [ { name: "Your own server", type: "al", request: "launch", environmentType: "OnPrem", server: "http://localhost", serverInstance: "BC", authentication: "Windows" } ] }; fs.writeFileSync( path.join(workspacePath, '.vscode/launch.json'), JSON.stringify(launchJson, null, 2) ); } /** * Clean up test workspace */ function cleanupWorkspace(workspacePath: string): void { if (fs.existsSync(workspacePath)) { fs.rmSync(workspacePath, { recursive: true, force: true }); } } // ============================================================================= // WORKSPACE DETECTION TESTS // ============================================================================= describe('AL Workspace Detection', () => { beforeAll(() => { createMockALWorkspace(TEMP_WORKSPACE); }); afterAll(() => { cleanupWorkspace(TEMP_WORKSPACE); }); it('should detect valid AL workspace with app.json', () => { const appJsonPath = path.join(TEMP_WORKSPACE, 'app.json'); expect(fs.existsSync(appJsonPath)).toBe(true); const appJson = JSON.parse(fs.readFileSync(appJsonPath, 'utf-8')); expect(appJson.name).toBe('Test BC App'); expect(appJson.runtime).toBe('12.0'); }); it('should find AL files in workspace', () => { const alFiles: string[] = []; function findAlFiles(dir: string) { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { findAlFiles(fullPath); } else if (entry.name.endsWith('.al')) { alFiles.push(fullPath); } } } findAlFiles(TEMP_WORKSPACE); expect(alFiles.length).toBe(4); // Table, Page, Codeunit, Enum expect(alFiles.some(f => f.includes('TestTable.Table.al'))).toBe(true); expect(alFiles.some(f => f.includes('TestCard.Page.al'))).toBe(true); expect(alFiles.some(f => f.includes('TestManagement.Codeunit.al'))).toBe(true); expect(alFiles.some(f => f.includes('TestStatus.Enum.al'))).toBe(true); }); it('should validate workspace structure', () => { // Check required directories exist expect(fs.existsSync(path.join(TEMP_WORKSPACE, 'src'))).toBe(true); expect(fs.existsSync(path.join(TEMP_WORKSPACE, '.vscode'))).toBe(true); // Check app.json has valid structure const appJson = JSON.parse( fs.readFileSync(path.join(TEMP_WORKSPACE, 'app.json'), 'utf-8') ); expect(appJson.id).toMatch(/^[0-9a-f-]{36}$/i); expect(appJson.idRanges).toBeDefined(); expect(appJson.idRanges[0].from).toBe(50100); }); }); // ============================================================================= // PATH SECURITY IN WORKSPACE CONTEXT // ============================================================================= describe('Path Security with AL Workspace', () => { beforeAll(() => { createMockALWorkspace(TEMP_WORKSPACE); }); afterAll(() => { cleanupWorkspace(TEMP_WORKSPACE); }); it('should allow access to files within workspace', () => { const safePath = sanitizePath('src/table/TestTable.Table.al', TEMP_WORKSPACE); expect(safePath).toContain('TestTable.Table.al'); expect(safePath.startsWith(TEMP_WORKSPACE)).toBe(true); }); it('should block access to files outside workspace', () => { expect(() => sanitizePath('../../../etc/passwd', TEMP_WORKSPACE)).toThrow(); expect(() => sanitizePath('C:\\Windows\\System32', TEMP_WORKSPACE)).toThrow(); }); it('should block paths with traversal patterns even if they resolve safely', () => { // Our security is strict - any path containing .. is blocked // This is intentional for defense in depth expect(() => sanitizePath('src/table/../page/TestCard.Page.al', TEMP_WORKSPACE)).toThrow(); }); it('should allow deeply nested safe paths', () => { const deepPath = sanitizePath('src/table/TestTable.Table.al', TEMP_WORKSPACE); expect(deepPath).toContain('TestTable.Table.al'); }); }); // ============================================================================= // FILE OPERATIONS TESTS // ============================================================================= describe('File Operations in AL Workspace', () => { let testWorkspace: string; beforeEach(() => { testWorkspace = path.join(os.tmpdir(), 'partnercore-file-test-' + Date.now()); createMockALWorkspace(testWorkspace); }); afterAll(() => { // Clean up any remaining test workspaces const tmpDir = os.tmpdir(); const entries = fs.readdirSync(tmpDir); for (const entry of entries) { if (entry.startsWith('partnercore-file-test-') || entry.startsWith('partnercore-proxy-test-')) { try { fs.rmSync(path.join(tmpDir, entry), { recursive: true, force: true }); } catch { // Ignore errors during cleanup } } } }); it('should read AL file content', () => { const tablePath = path.join(testWorkspace, 'src/table/TestTable.Table.al'); const content = fs.readFileSync(tablePath, 'utf-8'); expect(content).toContain('table 50100'); expect(content).toContain('DataClassification = CustomerContent'); }); it('should handle file not found gracefully', () => { const nonExistentPath = path.join(testWorkspace, 'src/nonexistent.al'); expect(fs.existsSync(nonExistentPath)).toBe(false); }); it('should list files in directory', () => { const srcDir = path.join(testWorkspace, 'src'); const entries = fs.readdirSync(srcDir, { withFileTypes: true }); expect(entries.length).toBeGreaterThan(0); expect(entries.some(e => e.name === 'table' && e.isDirectory())).toBe(true); expect(entries.some(e => e.name === 'page' && e.isDirectory())).toBe(true); }); }); // ============================================================================= // TOOL ROUTER INTEGRATION TESTS // ============================================================================= describe('Tool Router with AL Workspace', () => { let toolRouter: ToolRouter; let testWorkspace: string; beforeAll(() => { testWorkspace = path.join(os.tmpdir(), 'partnercore-router-test-' + Date.now()); createMockALWorkspace(testWorkspace); // Create a mock tool router for local tools toolRouter = new ToolRouter(testWorkspace); }); afterAll(() => { cleanupWorkspace(testWorkspace); }); it('should have workspace root set', () => { expect(toolRouter).toBeDefined(); }); it('should get available tool definitions', async () => { const tools = await toolRouter.getToolDefinitions(); expect(Array.isArray(tools)).toBe(true); // Local tools should include file operations const toolNames = tools.map(t => t.name); expect(toolNames).toContain('read_file'); expect(toolNames).toContain('write_file'); expect(toolNames).toContain('list_files'); expect(toolNames).toContain('search_files'); expect(toolNames).toContain('al_get_symbols'); }); }); // ============================================================================= // CONFIG LOADING TESTS // ============================================================================= describe('Configuration Loading', () => { it('should load default config when no env file', () => { // loadConfig should not throw even without .env expect(() => loadConfig()).not.toThrow(); }); it('should provide sensible defaults', () => { const config = loadConfig(); expect(config.cloudUrl).toBeDefined(); expect(config.al.workspaceRoot).toBeDefined(); expect(config.port).toBeGreaterThan(0); expect(config.logLevel).toBeDefined(); }); }); // ============================================================================= // AL CODE PATTERN TESTS // ============================================================================= describe('AL Code Pattern Detection', () => { let testWorkspace: string; beforeAll(() => { testWorkspace = path.join(os.tmpdir(), 'partnercore-pattern-test-' + Date.now()); createMockALWorkspace(testWorkspace); }); afterAll(() => { cleanupWorkspace(testWorkspace); }); it('should detect DataClassification in table', () => { const tablePath = path.join(testWorkspace, 'src/table/TestTable.Table.al'); const content = fs.readFileSync(tablePath, 'utf-8'); // Check for DataClassification on each field const fieldMatches = content.match(/DataClassification\s*=\s*\w+/g); expect(fieldMatches).toBeDefined(); expect(fieldMatches!.length).toBeGreaterThanOrEqual(4); // Table + 4 fields }); it('should detect ApplicationArea in page', () => { const pagePath = path.join(testWorkspace, 'src/page/TestCard.Page.al'); const content = fs.readFileSync(pagePath, 'utf-8'); const areaMatches = content.match(/ApplicationArea\s*=\s*\w+/g); expect(areaMatches).toBeDefined(); expect(areaMatches!.length).toBeGreaterThanOrEqual(3); }); it('should detect ToolTip in page fields', () => { const pagePath = path.join(testWorkspace, 'src/page/TestCard.Page.al'); const content = fs.readFileSync(pagePath, 'utf-8'); const tooltipMatches = content.match(/ToolTip\s*=\s*'/g); expect(tooltipMatches).toBeDefined(); expect(tooltipMatches!.length).toBeGreaterThanOrEqual(3); }); it('should detect enum with Extensible property', () => { const enumPath = path.join(testWorkspace, 'src/enum/TestStatus.Enum.al'); const content = fs.readFileSync(enumPath, 'utf-8'); expect(content).toContain('Extensible = true'); expect(content).toContain("value(0; \"New\")"); }); }); // ============================================================================= // ERROR HANDLING TESTS // ============================================================================= describe('Error Handling', () => { it('should handle invalid workspace path gracefully', () => { const invalidPath = '/nonexistent/workspace/path'; // Creating router with invalid path should not throw const router = new ToolRouter([], null, null, invalidPath); expect(router).toBeDefined(); }); it('should handle empty workspace', () => { const emptyWorkspace = path.join(os.tmpdir(), 'empty-workspace-' + Date.now()); fs.mkdirSync(emptyWorkspace, { recursive: true }); try { // No app.json means not a valid AL workspace const appJsonExists = fs.existsSync(path.join(emptyWorkspace, 'app.json')); expect(appJsonExists).toBe(false); } finally { fs.rmSync(emptyWorkspace, { recursive: true, force: true }); } }); }); // ============================================================================= // SUMMARY // ============================================================================= describe('Integration Test Summary', () => { it('should have comprehensive integration coverage', () => { const testCategories = [ 'Workspace Detection', 'Path Security', 'File Operations', 'Tool Router', 'Configuration Loading', 'AL Code Patterns', 'Error Handling', ]; expect(testCategories.length).toBeGreaterThanOrEqual(7); console.log(`\n✅ Integration tests cover ${testCategories.length} categories`); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ciellosinc/partnercore-proxy'

If you have feedback or need assistance with the MCP directory API, please join our Discord server