Skip to main content
Glama

SAP OData to MCP Server

by Raistlin82
test-scope-mapping.js20.5 kB
#!/usr/bin/env node /** * Scope Mapping Test Suite * Tests the JWT validator scope mapping functionality */ import { Logger } from '../dist/utils/logger.js'; import chalk from 'chalk'; class ScopeMappingTest { constructor(options = {}) { this.verbose = options.verbose || false; this.results = { passed: 0, failed: 0, tests: [] }; this.logger = new Logger('ScopeMappingTest'); } log(message, type = 'info') { if (!this.verbose && type === 'debug') return; const prefix = { info: chalk.blue('ℹ'), success: chalk.green('✅'), error: chalk.red('❌'), debug: chalk.gray('🔍'), warning: chalk.yellow('⚠️') }[type] || ''; console.log(`${prefix} ${message}`); } recordResult(testName, result) { this.results.tests.push({ name: testName, ...result }); if (result.success) { this.results.passed++; this.log(`${testName}: PASSED`, 'success'); } else { this.results.failed++; this.log(`${testName}: FAILED - ${result.error}`, 'error'); if (result.details && this.verbose) { this.log(`Details: ${JSON.stringify(result.details, null, 2)}`, 'debug'); } } } async runTests() { this.log('🔐 Testing Scope Mapping Logic', 'info'); console.log(chalk.gray('=' .repeat(50))); try { // Test 1: MCPAdministrator role collection mapping const adminResult = await this.testAdministratorMapping(); this.recordResult('MCPAdministrator Scope Mapping', adminResult); // Test 2: MCPUser role collection mapping const userResult = await this.testUserMapping(); this.recordResult('MCPUser Scope Mapping', userResult); // Test 3: MCPUIUser role collection mapping const uiUserResult = await this.testUIUserMapping(); this.recordResult('MCPUIUser Scope Mapping', uiUserResult); // Test 4: MCPUIDesigner role collection mapping const uiDesignerResult = await this.testUIDesignerMapping(); this.recordResult('MCPUIDesigner Scope Mapping', uiDesignerResult); // Test 5: Multiple role collections mapping const multipleResult = await this.testMultipleRoles(); this.recordResult('Multiple Role Collections Mapping', multipleResult); // Test 6: Scope validation in hasRequiredScope method const scopeValidationResult = await this.testScopeValidation(); this.recordResult('Scope Validation Logic', scopeValidationResult); return this.results; } catch (error) { this.log(`Test suite failed: ${error.message}`, 'error'); return this.results; } } async testAdministratorMapping() { try { this.log('🔍 Testing MCPAdministrator scope mapping...', 'debug'); // Create a mock JWT validator instance for testing const MockJWTValidator = await this.createMockJWTValidator(); const validator = new MockJWTValidator(); const groups = ['MCPAdministrator']; const mappedScopes = validator.mapGroupsToScopes(groups); const expectedScopes = [ 'btp-sap-odata-to-mcp-server.read', 'btp-sap-odata-to-mcp-server.write', 'btp-sap-odata-to-mcp-server.delete', 'btp-sap-odata-to-mcp-server.admin', 'btp-sap-odata-to-mcp-server.discover', 'btp-sap-odata-to-mcp-server.ui.forms', 'btp-sap-odata-to-mcp-server.ui.grids', 'btp-sap-odata-to-mcp-server.ui.dashboards', 'btp-sap-odata-to-mcp-server.ui.workflows', 'btp-sap-odata-to-mcp-server.ui.reports' ]; const missingScopes = expectedScopes.filter(scope => !mappedScopes.includes(scope)); const extraScopes = mappedScopes.filter(scope => !expectedScopes.includes(scope)); if (missingScopes.length > 0 || extraScopes.length > 0) { return { success: false, error: `Scope mismatch for MCPAdministrator`, details: { expected: expectedScopes, actual: mappedScopes, missing: missingScopes, extra: extraScopes } }; } return { success: true }; } catch (error) { return { success: false, error: error.message }; } } async testUserMapping() { try { this.log('🔍 Testing MCPUser scope mapping...', 'debug'); const MockJWTValidator = await this.createMockJWTValidator(); const validator = new MockJWTValidator(); const groups = ['MCPUser']; const mappedScopes = validator.mapGroupsToScopes(groups); const expectedScopes = [ 'btp-sap-odata-to-mcp-server.read', 'btp-sap-odata-to-mcp-server.write', 'btp-sap-odata-to-mcp-server.discover' ]; const missingScopes = expectedScopes.filter(scope => !mappedScopes.includes(scope)); const hasUIScopes = mappedScopes.some(scope => scope.includes('.ui.')); if (missingScopes.length > 0) { return { success: false, error: `Missing scopes for MCPUser: ${missingScopes.join(', ')}`, details: { expected: expectedScopes, actual: mappedScopes } }; } if (hasUIScopes) { return { success: false, error: `MCPUser should not have UI scopes`, details: { actual: mappedScopes } }; } return { success: true }; } catch (error) { return { success: false, error: error.message }; } } async testUIUserMapping() { try { this.log('🔍 Testing MCPUIUser scope mapping...', 'debug'); const MockJWTValidator = await this.createMockJWTValidator(); const validator = new MockJWTValidator(); const groups = ['MCPUIUser']; const mappedScopes = validator.mapGroupsToScopes(groups); const expectedScopes = [ 'btp-sap-odata-to-mcp-server.read', 'btp-sap-odata-to-mcp-server.discover', 'btp-sap-odata-to-mcp-server.ui.forms', 'btp-sap-odata-to-mcp-server.ui.grids' ]; const missingScopes = expectedScopes.filter(scope => !mappedScopes.includes(scope)); const hasUnexpectedUIScopes = mappedScopes.filter(scope => scope.includes('.ui.') && !['btp-sap-odata-to-mcp-server.ui.forms', 'btp-sap-odata-to-mcp-server.ui.grids'].includes(scope) ); if (missingScopes.length > 0) { return { success: false, error: `Missing scopes for MCPUIUser: ${missingScopes.join(', ')}`, details: { expected: expectedScopes, actual: mappedScopes } }; } if (hasUnexpectedUIScopes.length > 0) { return { success: false, error: `MCPUIUser has unexpected UI scopes: ${hasUnexpectedUIScopes.join(', ')}`, details: { actual: mappedScopes } }; } return { success: true }; } catch (error) { return { success: false, error: error.message }; } } async testUIDesignerMapping() { try { this.log('🔍 Testing MCPUIDesigner scope mapping...', 'debug'); const MockJWTValidator = await this.createMockJWTValidator(); const validator = new MockJWTValidator(); const groups = ['MCPUIDesigner']; const mappedScopes = validator.mapGroupsToScopes(groups); const expectedScopes = [ 'btp-sap-odata-to-mcp-server.read', 'btp-sap-odata-to-mcp-server.write', 'btp-sap-odata-to-mcp-server.discover', 'btp-sap-odata-to-mcp-server.ui.forms', 'btp-sap-odata-to-mcp-server.ui.grids', 'btp-sap-odata-to-mcp-server.ui.dashboards', 'btp-sap-odata-to-mcp-server.ui.workflows', 'btp-sap-odata-to-mcp-server.ui.reports' ]; const missingScopes = expectedScopes.filter(scope => !mappedScopes.includes(scope)); if (missingScopes.length > 0) { return { success: false, error: `Missing scopes for MCPUIDesigner: ${missingScopes.join(', ')}`, details: { expected: expectedScopes, actual: mappedScopes } }; } return { success: true }; } catch (error) { return { success: false, error: error.message }; } } async testMultipleRoles() { try { this.log('🔍 Testing multiple role collections mapping...', 'debug'); const MockJWTValidator = await this.createMockJWTValidator(); const validator = new MockJWTValidator(); // Test user with both MCPUser and MCPUIUser roles const groups = ['MCPUser', 'MCPUIUser']; const mappedScopes = validator.mapGroupsToScopes(groups); const expectedCoreScopes = [ 'btp-sap-odata-to-mcp-server.read', 'btp-sap-odata-to-mcp-server.write', 'btp-sap-odata-to-mcp-server.discover' ]; const expectedUIScopes = [ 'btp-sap-odata-to-mcp-server.ui.forms', 'btp-sap-odata-to-mcp-server.ui.grids' ]; const missingCoreScopes = expectedCoreScopes.filter(scope => !mappedScopes.includes(scope)); const missingUIScopes = expectedUIScopes.filter(scope => !mappedScopes.includes(scope)); if (missingCoreScopes.length > 0 || missingUIScopes.length > 0) { return { success: false, error: `Missing scopes for multiple roles`, details: { expected: [...expectedCoreScopes, ...expectedUIScopes], actual: mappedScopes, missingCore: missingCoreScopes, missingUI: missingUIScopes } }; } return { success: true }; } catch (error) { return { success: false, error: error.message }; } } async testScopeValidation() { try { this.log('🔍 Testing hasRequiredScope validation logic...', 'debug'); const MockJWTValidator = await this.createMockJWTValidator(); const validator = new MockJWTValidator(); // Test scenarios const tests = [ { name: 'Admin has all permissions', userScopes: ['btp-sap-odata-to-mcp-server-mcp_noprod_space!t25364.admin'], requiredScope: 'ui.forms', expected: true }, { name: 'User with UI forms scope', userScopes: ['btp-sap-odata-to-mcp-server-mcp_noprod_space!t25364.ui.forms'], requiredScope: 'ui.forms', expected: true }, { name: 'User without required UI scope', userScopes: ['btp-sap-odata-to-mcp-server-mcp_noprod_space!t25364.read'], requiredScope: 'ui.forms', expected: false }, { name: 'User with short scope name', userScopes: ['ui.forms'], requiredScope: 'ui.forms', expected: true } ]; for (const test of tests) { const result = validator.hasRequiredScope(test.userScopes, test.requiredScope); if (result !== test.expected) { return { success: false, error: `Scope validation failed for: ${test.name}`, details: { test: test, actualResult: result } }; } } return { success: true }; } catch (error) { return { success: false, error: error.message }; } } async createMockJWTValidator() { // Create a minimal mock of JWTValidator for testing class MockJWTValidator { constructor() { this.xsuaaCredentials = { xsappname: 'btp-sap-odata-to-mcp-server' }; } mapGroupsToScopes(groups) { const xsappname = this.xsuaaCredentials?.xsappname || process.env.XSUAA_XSAPPNAME || 'btp-sap-odata-to-mcp-server'; const scopes = []; for (const group of groups) { switch (group) { case 'MCPAdministrator': scopes.push(`${xsappname}.read`); scopes.push(`${xsappname}.write`); scopes.push(`${xsappname}.delete`); scopes.push(`${xsappname}.admin`); scopes.push(`${xsappname}.discover`); scopes.push(`${xsappname}.ui.forms`); scopes.push(`${xsappname}.ui.grids`); scopes.push(`${xsappname}.ui.dashboards`); scopes.push(`${xsappname}.ui.workflows`); scopes.push(`${xsappname}.ui.reports`); break; case 'MCPUser': scopes.push(`${xsappname}.read`); scopes.push(`${xsappname}.write`); scopes.push(`${xsappname}.discover`); break; case 'MCPManager': scopes.push(`${xsappname}.read`); scopes.push(`${xsappname}.write`); scopes.push(`${xsappname}.delete`); scopes.push(`${xsappname}.discover`); break; case 'MCPViewer': scopes.push(`${xsappname}.read`); scopes.push(`${xsappname}.discover`); break; case 'MCPUIUser': scopes.push(`${xsappname}.read`); scopes.push(`${xsappname}.discover`); scopes.push(`${xsappname}.ui.forms`); scopes.push(`${xsappname}.ui.grids`); break; case 'MCPUIAnalyst': scopes.push(`${xsappname}.read`); scopes.push(`${xsappname}.discover`); scopes.push(`${xsappname}.ui.dashboards`); scopes.push(`${xsappname}.ui.reports`); break; case 'MCPUIDesigner': scopes.push(`${xsappname}.read`); scopes.push(`${xsappname}.write`); scopes.push(`${xsappname}.discover`); scopes.push(`${xsappname}.ui.forms`); scopes.push(`${xsappname}.ui.grids`); scopes.push(`${xsappname}.ui.dashboards`); scopes.push(`${xsappname}.ui.workflows`); scopes.push(`${xsappname}.ui.reports`); break; } } return scopes; } hasRequiredScope(userScopes, requiredScope) { // Admin scope has all permissions if (userScopes.includes('admin')) { return true; } // Check for exact scope match (including full scope names with prefix) if (userScopes.includes(requiredScope)) { return true; } // For UI scopes, also check with full scope prefix if (requiredScope.startsWith('ui.')) { const xsappname = process.env.XSUAA_XSAPPNAME || 'btp-sap-odata-to-mcp-server'; const fullScopeName = `${xsappname}.${requiredScope}`; if (userScopes.includes(fullScopeName)) { return true; } } // Check scope hierarchy (write includes read, delete includes write and read) const scopeHierarchy = { 'read': ['read'], 'write': ['read', 'write'], 'delete': ['read', 'write', 'delete'], 'admin': ['read', 'write', 'delete', 'admin', 'discover', 'ui.forms', 'ui.grids', 'ui.dashboards', 'ui.workflows', 'ui.reports'] }; for (const userScope of userScopes) { // Handle full scope names with prefix (including complex BTP formats) let scopeToCheck = userScope; // Handle BTP scope format: app-name-space!tenant.scope or app-name.scope // Extract the scope part after the last occurrence of app name const lastDotIndex = userScope.lastIndexOf('.'); if (lastDotIndex !== -1) { const afterLastDot = userScope.substring(lastDotIndex + 1); // Check if this looks like a UI scope (contains ui prefix in the remaining part) if (userScope.includes('.ui.')) { // For UI scopes like "app.ui.forms", extract "ui.forms" const uiIndex = userScope.indexOf('.ui.'); scopeToCheck = userScope.substring(uiIndex + 1); // +1 to skip the first dot } else { // For simple scopes like "app.read", extract "read" scopeToCheck = afterLastDot; } } const allowedScopes = scopeHierarchy[scopeToCheck] || [scopeToCheck]; if (allowedScopes.includes(requiredScope)) { return true; } } return false; } } return MockJWTValidator; } } // Run tests if called directly if (import.meta.url === `file://${process.argv[1]}`) { const test = new ScopeMappingTest({ verbose: true }); test.runTests().then(results => { console.log('\n' + chalk.bold('Scope Mapping Test Results:')); console.log(chalk.green(`✅ Passed: ${results.passed}`)); console.log(chalk.red(`❌ Failed: ${results.failed}`)); console.log(chalk.blue(`📊 Total: ${results.tests.length}`)); if (results.failed > 0) { console.log('\n' + chalk.red('Failed Tests:')); results.tests.filter(test => !test.success).forEach(test => { console.log(chalk.red(` • ${test.name}: ${test.error}`)); }); process.exit(1); } else { console.log('\n' + chalk.green('🎉 All scope mapping tests passed!')); process.exit(0); } }).catch(error => { console.error(chalk.red('Test execution failed:'), error); process.exit(1); }); } export { ScopeMappingTest };

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/Raistlin82/btp-sap-odata-to-mcp-server-optimized'

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