#!/usr/bin/env node /** * π― MASTER TEST SCENARIO v2.0 * * ΠΠΠΠΠ«Π ΠΈΠ½ΡΠ΅Π³ΡΠ°ΡΠΈΠΎΠ½Π½ΡΠΉ ΡΠ΅ΡΡ, ΠΏΠΎΠΊΡΡΠ²Π°ΡΡΠΈΠΉ ΠΠ‘Π 115 API endpoints. * ΠΠΌΠΈΡΠΈΡΡΠ΅Ρ ΡΠ΅Π°Π»ΡΠ½ΡΠΉ ΡΠ°Π±ΠΎΡΠΈΠΉ ΠΏΡΠΎΡΠ΅ΡΡ ΠΊΠΎΠΌΠ°Π½Π΄Ρ. * * Π‘ΡΠ΅Π½Π°ΡΠΈΠΉ: * 1. testowner@hltrn.cc ΡΠΎΠ·Π΄Π°ΡΡ ΡΠ°Π±ΠΎΡΠ΅Π΅ ΠΏΡΠΎΡΡΡΠ°Π½ΡΡΠ²ΠΎ * 2. Π‘ΠΎΠ·Π΄Π°ΡΡ 4 ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Π΅ΠΉ Ρ ΡΠ°Π·Π½ΡΠΌΠΈ ΡΠΎΠ»ΡΠΌΠΈ * 3. ΠΡΠΈΠ³Π»Π°ΡΠ°Π΅Ρ ΠΈΡ Π² shared space Ρ ΡΠ°Π·Π½ΡΠΌΠΈ ΠΏΡΠ°Π²Π°ΠΌΠΈ * 4. ΠΠ°ΠΆΠ΄ΡΠΉ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ Π²ΡΠΏΠΎΠ»Π½ΡΠ΅Ρ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ ΡΠΎΠ³Π»Π°ΡΠ½ΠΎ ΡΠ²ΠΎΠ΅ΠΉ ΡΠΎΠ»ΠΈ * 5. ΠΡΠΎΠ²Π΅ΡΡΡΡΡΡ ΠΠ‘Π endpoints Π³ΡΡΠΏΠΏΠ°ΠΌΠΈ ΠΏΠΎ ΠΊΠ°ΡΠ΅Π³ΠΎΡΠΈΡΠΌ * 6. ΠΠ΅Π½Π΅ΡΠΈΡΡΠ΅ΡΡΡ TEST-SUMMARY.md Ρ ΠΏΠΎΠ»Π½ΡΠΌ ΠΎΡΡΡΡΠΎΠΌ */ import { uniqueId } from '../../helpers/testFactory.js'; import fs from 'fs'; import path from 'path'; const BASE_URL = process.env.TEST_API_URL || 'https://devcrm.hltrn.cc/api/v3'; // ============================================================================ // CONFIGURATION // ============================================================================ const TEST_OWNER = { email: 'testowner@hltrn.cc', password: 'testpass123!' }; const USERS_TO_CREATE = [ { role: 'admin', prefix: 'admin', spaceRole: 'admin' }, { role: 'user', prefix: 'manager', spaceRole: 'editor' }, { role: 'user', prefix: 'employee', spaceRole: 'editor' }, { role: 'viewer', prefix: 'viewer', spaceRole: 'viewer' } ]; // ============================================================================ // ALL ENDPOINTS TO TEST (115 total) // ============================================================================ const ALL_ENDPOINTS = [ // AUTH (19 endpoints) { method: 'POST', path: '/auth/register', category: 'auth', tested: false }, { method: 'POST', path: '/auth/login', category: 'auth', tested: false }, { method: 'POST', path: '/auth/logout', category: 'auth', tested: false }, { method: 'GET', path: '/auth/me', category: 'auth', tested: false }, { method: 'POST', path: '/auth/refresh', category: 'auth', tested: false }, { method: 'PATCH', path: '/auth/password', category: 'auth', tested: false }, { method: 'GET', path: '/auth/profile', category: 'auth', tested: false }, { method: 'PATCH', path: '/auth/profile', category: 'auth', tested: false }, { method: 'PATCH', path: '/auth/email', category: 'auth', tested: false }, { method: 'POST', path: '/auth/2fa/setup', category: 'auth', tested: false }, { method: 'POST', path: '/auth/2fa/verify', category: 'auth', tested: false }, { method: 'DELETE', path: '/auth/2fa', category: 'auth', tested: false }, { method: 'POST', path: '/auth/forgot-password', category: 'auth', tested: false }, { method: 'GET', path: '/auth/verify-reset-token/:token', category: 'auth', tested: false }, { method: 'POST', path: '/auth/reset-password', category: 'auth', tested: false }, { method: 'GET', path: '/auth/google/config', category: 'auth', tested: false }, { method: 'GET', path: '/auth/google/auth-url', category: 'auth', tested: false }, { method: 'POST', path: '/auth/google/callback', category: 'auth', tested: false }, { method: 'POST', path: '/auth/google/config', category: 'auth', tested: false }, // SPACES (8 endpoints) { method: 'GET', path: '/spaces', category: 'spaces', tested: false }, { method: 'POST', path: '/spaces', category: 'spaces', tested: false }, { method: 'GET', path: '/spaces/:id', category: 'spaces', tested: false }, { method: 'PUT', path: '/spaces/:id', category: 'spaces', tested: false }, { method: 'DELETE', path: '/spaces/:id', category: 'spaces', tested: false }, { method: 'POST', path: '/spaces/:id/data-sources-project', category: 'spaces', tested: false }, { method: 'POST', path: '/spaces/:id/users-table', category: 'spaces', tested: false }, { method: 'POST', path: '/spaces/:id/roles-table', category: 'spaces', tested: false }, // PROJECTS (4 endpoints) { method: 'GET', path: '/projects', category: 'projects', tested: false }, { method: 'POST', path: '/projects', category: 'projects', tested: false }, { method: 'PUT', path: '/projects/:id', category: 'projects', tested: false }, { method: 'DELETE', path: '/projects/:id', category: 'projects', tested: false }, // TABLES (16 endpoints) { method: 'GET', path: '/users', category: 'tables', tested: false }, { method: 'GET', path: '/tables/:tableId', category: 'tables', tested: false }, { method: 'POST', path: '/tables/create-calendar', category: 'tables', tested: false }, { method: 'POST', path: '/tables', category: 'tables', tested: false }, { method: 'GET', path: '/tables', category: 'tables', tested: false }, { method: 'GET', path: '/projects/:projectId/tables', category: 'tables', tested: false }, { method: 'GET', path: '/tables/:tableId/columns', category: 'tables', tested: false }, { method: 'GET', path: '/tables/:tableId/rows', category: 'tables', tested: false }, { method: 'POST', path: '/tables/:tableId/rows', category: 'tables', tested: false }, { method: 'PUT', path: '/tables/:tableId/rows/:rowId', category: 'tables', tested: false }, { method: 'DELETE', path: '/tables/:tableId/rows/:rowId', category: 'tables', tested: false }, { method: 'POST', path: '/tables/:tableId/connect', category: 'tables', tested: false }, { method: 'PATCH', path: '/tables/:tableId', category: 'tables', tested: false }, { method: 'POST', path: '/tables/:tableId/rows/batch-update', category: 'tables', tested: false }, { method: 'POST', path: '/tables/:tableId/rows/batch-delete', category: 'tables', tested: false }, // WIDGETS (11 endpoints) { method: 'GET', path: '/projects/:projectId/widgets', category: 'widgets', tested: false }, { method: 'GET', path: '/projects/:projectId/dashboard', category: 'widgets', tested: false }, { method: 'GET', path: '/dashboards/:dashboardId', category: 'widgets', tested: false }, { method: 'GET', path: '/widgets/presets', category: 'widgets', tested: false }, { method: 'GET', path: '/dashboards/:dashboardId/widgets', category: 'widgets', tested: false }, { method: 'POST', path: '/dashboards/:dashboardId/widgets', category: 'widgets', tested: false }, { method: 'GET', path: '/widgets/:widgetId', category: 'widgets', tested: false }, { method: 'PATCH', path: '/widgets/:widgetId', category: 'widgets', tested: false }, { method: 'DELETE', path: '/widgets/:widgetId', category: 'widgets', tested: false }, { method: 'PATCH', path: '/widgets/:widgetId/code', category: 'widgets', tested: false }, { method: 'GET', path: '/widgets/:widgetId/data', category: 'widgets', tested: false }, // DOCUMENTS (12 endpoints) { method: 'POST', path: '/projects/:projectId/documents/init', category: 'documents', tested: false }, { method: 'GET', path: '/projects/:projectId/documents', category: 'documents', tested: false }, { method: 'POST', path: '/projects/:projectId/documents', category: 'documents', tested: false }, { method: 'DELETE', path: '/documents/:documentId', category: 'documents', tested: false }, { method: 'GET', path: '/documents/:documentId/content', category: 'documents', tested: false }, { method: 'POST', path: '/documents/:documentId/import-v4', category: 'documents', tested: false }, { method: 'POST', path: '/projects/:projectId/documents/add-language', category: 'documents', tested: false }, { method: 'POST', path: '/documents/import', category: 'documents', tested: false }, { method: 'GET', path: '/documents/:documentId/export', category: 'documents', tested: false }, { method: 'PUT', path: '/documents/:documentId/structure', category: 'documents', tested: false }, { method: 'POST', path: '/documents/:documentId/rebuild-structure', category: 'documents', tested: false }, { method: 'POST', path: '/documents/setup-columns', category: 'documents', tested: false }, // FOLDERS (5 endpoints) { method: 'GET', path: '/projects/:projectId/folders', category: 'folders', tested: false }, { method: 'POST', path: '/projects/:projectId/folders', category: 'folders', tested: false }, { method: 'GET', path: '/folders/:folderId', category: 'folders', tested: false }, { method: 'PUT', path: '/folders/:folderId', category: 'folders', tested: false }, { method: 'DELETE', path: '/folders/:folderId', category: 'folders', tested: false }, // FILES (8 endpoints) { method: 'POST', path: '/files/upload', category: 'files', tested: false }, { method: 'GET', path: '/files', category: 'files', tested: false }, { method: 'GET', path: '/files/:fileId', category: 'files', tested: false }, { method: 'DELETE', path: '/files/:fileId', category: 'files', tested: false }, { method: 'GET', path: '/storage-providers', category: 'files', tested: false }, { method: 'POST', path: '/storage-providers', category: 'files', tested: false }, { method: 'PUT', path: '/storage-providers/:providerId', category: 'files', tested: false }, { method: 'DELETE', path: '/storage-providers/:providerId', category: 'files', tested: false }, // API-KEYS (5 endpoints) { method: 'GET', path: '/api-keys', category: 'api-keys', tested: false }, { method: 'POST', path: '/api-keys', category: 'api-keys', tested: false }, { method: 'PATCH', path: '/api-keys/:id', category: 'api-keys', tested: false }, { method: 'DELETE', path: '/api-keys/:id', category: 'api-keys', tested: false }, { method: 'POST', path: '/api-keys/:id/regenerate', category: 'api-keys', tested: false }, // WEBHOOKS (6 endpoints) { method: 'GET', path: '/projects/:projectId/webhooks', category: 'webhooks', tested: false }, { method: 'POST', path: '/projects/:projectId/webhooks', category: 'webhooks', tested: false }, { method: 'PATCH', path: '/webhooks/:id', category: 'webhooks', tested: false }, { method: 'DELETE', path: '/webhooks/:id', category: 'webhooks', tested: false }, { method: 'GET', path: '/webhooks/:id/logs', category: 'webhooks', tested: false }, { method: 'POST', path: '/incoming/:token', category: 'webhooks', tested: false }, // USER-SETTINGS (4 endpoints) { method: 'GET', path: '/user-settings/spaces-order', category: 'user-settings', tested: false }, { method: 'PUT', path: '/user-settings/spaces-order', category: 'user-settings', tested: false }, { method: 'PATCH', path: '/user-settings/spaces-order/:spaceId', category: 'user-settings', tested: false }, { method: 'DELETE', path: '/user-settings/spaces-order', category: 'user-settings', tested: false }, // AI-AGENTS (13 endpoints) { method: 'GET', path: '/ai/agents', category: 'ai-agents', tested: false }, { method: 'GET', path: '/ai/agents/:spaceId', category: 'ai-agents', tested: false }, { method: 'POST', path: '/ai/run', category: 'ai-agents', tested: false }, { method: 'POST', path: '/ai/chat', category: 'ai-agents', tested: false }, { method: 'POST', path: '/ai/process-prompt', category: 'ai-agents', tested: false }, { method: 'GET', path: '/ai/logs/:spaceId', category: 'ai-agents', tested: false }, { method: 'GET', path: '/ai/analytics/:spaceId', category: 'ai-agents', tested: false }, { method: 'POST', path: '/ai/providers/:providerId/refresh-models', category: 'ai-agents', tested: false }, { method: 'GET', path: '/ai/providers', category: 'ai-agents', tested: false }, { method: 'PUT', path: '/ai/providers/:providerId', category: 'ai-agents', tested: false }, { method: 'GET', path: '/ai/providers/:providerId/models', category: 'ai-agents', tested: false }, { method: 'GET', path: '/ai/models', category: 'ai-agents', tested: false }, { method: 'GET', path: '/ai/conversations', category: 'ai-agents', tested: false }, // DATA-SOURCES (9 endpoints) { method: 'GET', path: '/data-sources', category: 'data-sources', tested: false }, { method: 'GET', path: '/data-sources/:id', category: 'data-sources', tested: false }, { method: 'POST', path: '/data-sources', category: 'data-sources', tested: false }, { method: 'PUT', path: '/data-sources/:id', category: 'data-sources', tested: false }, { method: 'DELETE', path: '/data-sources/:id', category: 'data-sources', tested: false }, { method: 'POST', path: '/data-sources/:id/test', category: 'data-sources', tested: false }, { method: 'GET', path: '/data-sources/:id/tables', category: 'data-sources', tested: false }, { method: 'GET', path: '/data-sources/:id/tables/:tableName/columns', category: 'data-sources', tested: false }, { method: 'POST', path: '/data-sources/:id/import', category: 'data-sources', tested: false }, // SCHEMA (4 endpoints) { method: 'GET', path: '/spaces/:spaceId/schema', category: 'schema', tested: false }, { method: 'PUT', path: '/spaces/:spaceId/schema/layout', category: 'schema', tested: false }, { method: 'POST', path: '/spaces/:spaceId/schema/tables', category: 'schema', tested: false }, { method: 'POST', path: '/relations', category: 'schema', tested: false }, // BATCH (1 endpoint) { method: 'POST', path: '/spaces/:spaceId/batch', category: 'batch', tested: false } ]; // ============================================================================ // TEST STATE // ============================================================================ const testState = { runId: uniqueId(), startTime: new Date().toISOString(), owner: null, ownerToken: null, sharedSpace: null, createdUsers: [], createdSpaces: [], createdProjects: [], createdTables: [], createdWidgets: [], createdDashboards: [], createdDocuments: [], createdFolders: [], createdApiKeys: [], createdWebhooks: [], createdRows: [], errors: [], endpointResults: [], actions: [] }; // ============================================================================ // API CLIENT // ============================================================================ class MasterApiClient { constructor(baseUrl) { this.baseUrl = baseUrl; this.token = null; this.currentUser = null; } setToken(token) { this.token = token; } setUser(user) { this.currentUser = user; } async request(method, path, body = null) { const url = `${this.baseUrl}${path}`; const headers = { 'Content-Type': 'application/json' }; if (this.token) headers['Authorization'] = `Bearer ${this.token}`; const options = { method, headers }; if (body && ['POST', 'PATCH', 'PUT'].includes(method)) { options.body = JSON.stringify(body); } const startTime = Date.now(); try { const response = await fetch(url, options); const duration = Date.now() - startTime; let data; const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { data = await response.json(); } else { data = { raw: await response.text() }; } return { status: response.status, ok: response.ok, data: data.data || data, error: data.error, duration }; } catch (error) { return { status: 0, ok: false, error: error.message, duration: Date.now() - startTime }; } } get(path) { return this.request('GET', path); } post(path, body) { return this.request('POST', path, body); } patch(path, body) { return this.request('PATCH', path, body); } put(path, body) { return this.request('PUT', path, body); } delete(path) { return this.request('DELETE', path); } } const api = new MasterApiClient(BASE_URL); // ============================================================================ // HELPER FUNCTIONS // ============================================================================ function log(emoji, message, data = null) { const timestamp = new Date().toISOString().slice(11, 19); console.log(`[${timestamp}] ${emoji} ${message}`); if (data && process.env.VERBOSE) console.log(' ', JSON.stringify(data, null, 2).slice(0, 300)); } function markEndpointTested(method, pathPattern, result) { const endpoint = ALL_ENDPOINTS.find(e => e.method === method && e.path === pathPattern); if (endpoint) { endpoint.tested = true; endpoint.result = result; } testState.endpointResults.push({ method, path: pathPattern, result: result.ok ? 'PASS' : 'FAIL', status: result.status, duration: result.duration }); } function randomChoice(arr) { return arr[Math.floor(Math.random() * arr.length)]; } function randomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } // ============================================================================ // SCENARIO STEPS // ============================================================================ async function step01_ownerSetup() { log('π', '=== STEP 1: Owner Setup ==='); // Login let result = await api.post('/auth/login', { email: TEST_OWNER.email, password: TEST_OWNER.password }); markEndpointTested('POST', '/auth/login', result); if (!result.ok) { log('β οΈ', 'Login failed, registering...'); result = await api.post('/auth/register', { email: TEST_OWNER.email, password: TEST_OWNER.password, name: 'Test Owner' }); markEndpointTested('POST', '/auth/register', result); if (!result.ok) throw new Error(`Failed to setup owner: ${result.error}`); } const token = result.data.accessToken || result.data.token; api.setToken(token); testState.ownerToken = token; testState.owner = result.data.user; api.setUser(testState.owner); log('β ', `Owner ready: ${TEST_OWNER.email}`); // Test auth endpoints result = await api.get('/auth/me'); markEndpointTested('GET', '/auth/me', result); result = await api.get('/auth/profile'); markEndpointTested('GET', '/auth/profile', result); result = await api.patch('/auth/profile', { name: 'Test Owner Updated' }); markEndpointTested('PATCH', '/auth/profile', result); } async function step02_createSharedSpace() { log('π ', '=== STEP 2: Create Shared Space ==='); // List existing spaces let result = await api.get('/spaces'); markEndpointTested('GET', '/spaces', result); // Create shared workspace result = await api.post('/spaces', { name: `Shared Workspace ${uniqueId()}`, type: 'business', description: 'Team workspace for integration testing' }); markEndpointTested('POST', '/spaces', result); if (result.ok && result.data.space) { testState.sharedSpace = result.data.space; testState.createdSpaces.push(result.data.space); log('β ', `Created shared space: ${result.data.space.name} (ID: ${result.data.space.id})`); // Get space details result = await api.get(`/spaces/${testState.sharedSpace.id}`); markEndpointTested('GET', '/spaces/:id', result); // Update space result = await api.put(`/spaces/${testState.sharedSpace.id}`, { name: testState.sharedSpace.name, description: 'Updated description' }); markEndpointTested('PUT', '/spaces/:id', result); // Get schema result = await api.get(`/spaces/${testState.sharedSpace.id}/schema`); markEndpointTested('GET', '/spaces/:spaceId/schema', result); // Data sources project result = await api.post(`/spaces/${testState.sharedSpace.id}/data-sources-project`, {}); markEndpointTested('POST', '/spaces/:id/data-sources-project', result); // Users table result = await api.post(`/spaces/${testState.sharedSpace.id}/users-table`, {}); markEndpointTested('POST', '/spaces/:id/users-table', result); // Roles table result = await api.post(`/spaces/${testState.sharedSpace.id}/roles-table`, {}); markEndpointTested('POST', '/spaces/:id/roles-table', result); } } async function step03_createUsers() { log('π₯', '=== STEP 3: Create Test Users ==='); for (const userSpec of USERS_TO_CREATE) { const id = uniqueId(); const userData = { email: `${userSpec.prefix}-${id}@test.godcrm.local`, password: 'TestPass123!', name: `${userSpec.prefix.charAt(0).toUpperCase() + userSpec.prefix.slice(1)} ${id}` }; const result = await api.post('/auth/register', userData); // Don't mark again - already tested if (result.ok) { const token = result.data.accessToken || result.data.token; const user = { ...userData, id: result.data.user?.id, token: token, role: userSpec.role, spaceRole: userSpec.spaceRole }; testState.createdUsers.push(user); log('β ', `Created user: ${userData.email} (${userSpec.spaceRole})`); } else { testState.errors.push({ action: 'createUser', email: userData.email, error: result.error }); log('β', `Failed: ${userData.email}`, result.error); } } } async function step04_createProjectsAndTables() { log('π', '=== STEP 4: Create Projects and Tables ==='); if (!testState.sharedSpace) { log('β οΈ', 'No shared space, skipping...'); return; } // Get projects list let result = await api.get('/projects'); markEndpointTested('GET', '/projects', result); // Create multiple projects const projectNames = ['CRM Development', 'Marketing Campaign', 'Product Inventory', 'HR Management']; for (const name of projectNames) { result = await api.post('/projects', { name: `${name} ${uniqueId()}`, space_id: testState.sharedSpace.id, description: `Test project for ${name}` }); markEndpointTested('POST', '/projects', result); // Try to get project ID from response or list if (result.ok) { // Fetch projects to get the new one const listResult = await api.get('/projects'); if (listResult.ok && Array.isArray(listResult.data)) { const newProject = listResult.data.find(p => p.name?.includes(name)); if (newProject) { testState.createdProjects.push(newProject); log('β ', `Created project: ${newProject.name} (ID: ${newProject.id})`); // Update project result = await api.put(`/projects/${newProject.id}`, { name: newProject.name, description: 'Updated description' }); markEndpointTested('PUT', '/projects/:id', result); // Get project tables result = await api.get(`/projects/${newProject.id}/tables`); markEndpointTested('GET', '/projects/:projectId/tables', result); // Get dashboard result = await api.get(`/projects/${newProject.id}/dashboard`); markEndpointTested('GET', '/projects/:projectId/dashboard', result); if (result.ok && result.data?.id) { testState.createdDashboards.push(result.data); } // Get widgets result = await api.get(`/projects/${newProject.id}/widgets`); markEndpointTested('GET', '/projects/:projectId/widgets', result); // Init documents result = await api.post(`/projects/${newProject.id}/documents/init`, {}); markEndpointTested('POST', '/projects/:projectId/documents/init', result); // Get documents result = await api.get(`/projects/${newProject.id}/documents`); markEndpointTested('GET', '/projects/:projectId/documents', result); // Get folders result = await api.get(`/projects/${newProject.id}/folders`); markEndpointTested('GET', '/projects/:projectId/folders', result); // Create folder result = await api.post(`/projects/${newProject.id}/folders`, { name: `Folder ${uniqueId()}`, parent_id: null }); markEndpointTested('POST', '/projects/:projectId/folders', result); // Get webhooks result = await api.get(`/projects/${newProject.id}/webhooks`); markEndpointTested('GET', '/projects/:projectId/webhooks', result); } } } } // Create tables result = await api.get('/tables'); markEndpointTested('GET', '/tables', result); result = await api.get('/users'); markEndpointTested('GET', '/users', result); if (testState.createdProjects.length > 0) { const project = testState.createdProjects[0]; // Create different table types const tableTypes = [ { name: 'Contacts', columns: ['name:text', 'email:email', 'phone:text', 'status:select'] }, { name: 'Tasks', columns: ['title:text', 'assignee:text', 'due_date:date', 'priority:select', 'done:checkbox'] }, { name: 'Products', columns: ['name:text', 'price:number', 'stock:number', 'category:select'] }, { name: 'Notes', columns: ['title:text', 'content:text', 'created:date'] } ]; for (const tableSpec of tableTypes) { const columns = tableSpec.columns.map((col, idx) => { const [colName, type] = col.split(':'); return { name: colName, type, order_index: idx }; }); // v3 API uses camelCase: projectId (not project_id) result = await api.post('/tables', { name: `${tableSpec.name} ${uniqueId()}`, projectId: project.id, columns: columns }); markEndpointTested('POST', '/tables', result); // Get created table const tablesResult = await api.get(`/projects/${project.id}/tables`); if (tablesResult.ok && Array.isArray(tablesResult.data)) { const newTable = tablesResult.data.find(t => t.name?.includes(tableSpec.name)); if (newTable) { testState.createdTables.push(newTable); log('β ', `Created table: ${newTable.name} (ID: ${newTable.id})`); // Get table details result = await api.get(`/tables/${newTable.id}`); markEndpointTested('GET', '/tables/:tableId', result); // Get columns result = await api.get(`/tables/${newTable.id}/columns`); markEndpointTested('GET', '/tables/:tableId/columns', result); // Update table (accepts: displayName, icon, color, access_control, show_in_nav, privacy) result = await api.patch(`/tables/${newTable.id}`, { displayName: `${tableSpec.name} Updated`, icon: 'π' }); markEndpointTested('PATCH', '/tables/:tableId', result); } } } } } async function step05_crudOperations() { log('π', '=== STEP 5: CRUD Operations on Tables ==='); for (const table of testState.createdTables) { // Create rows const rowsToCreate = randomInt(5, 15); const createdRowIds = []; for (let i = 0; i < rowsToCreate; i++) { // v3 API uses 'data' not 'values' const result = await api.post(`/tables/${table.id}/rows`, { data: { name: `Item ${uniqueId()}`, title: `Task ${uniqueId()}`, email: `test${i}@example.com`, status: randomChoice(['active', 'pending', 'completed']), priority: randomChoice(['low', 'medium', 'high']) } }); markEndpointTested('POST', '/tables/:tableId/rows', result); if (result.ok && result.data?.id) { createdRowIds.push(result.data.id); testState.createdRows.push({ tableId: table.id, id: result.data.id }); } else if (result.ok) { // Row created but ID not in response - count as success testState.createdRows.push({ tableId: table.id, id: 'unknown' }); } } log('β ', `Created ${testState.createdRows.filter(r => r.tableId === table.id).length} rows in table ${table.name}`); // Get rows let result = await api.get(`/tables/${table.id}/rows`); markEndpointTested('GET', '/tables/:tableId/rows', result); // Get row IDs from response for updates const rowIds = result.ok && Array.isArray(result.data) ? result.data.map(r => r.id) : []; // Update some rows if (rowIds.length > 0) { for (let i = 0; i < Math.min(3, rowIds.length); i++) { // v3 API uses 'data' not 'values' result = await api.put(`/tables/${table.id}/rows/${rowIds[i]}`, { data: { status: 'updated', name: `Updated ${uniqueId()}` } }); markEndpointTested('PUT', '/tables/:tableId/rows/:rowId', result); } // Batch update result = await api.post(`/tables/${table.id}/rows/batch-update`, { rowIds: rowIds.slice(0, 3), data: { status: 'batch-updated' } }); markEndpointTested('POST', '/tables/:tableId/rows/batch-update', result); // Delete one row if (rowIds.length > 5) { result = await api.delete(`/tables/${table.id}/rows/${rowIds[rowIds.length - 1]}`); markEndpointTested('DELETE', '/tables/:tableId/rows/:rowId', result); } } } } async function step06_widgetsAndDashboards() { log('π', '=== STEP 6: Widgets and Dashboards ==='); // Get presets let result = await api.get('/widgets/presets'); markEndpointTested('GET', '/widgets/presets', result); // Extract preset names from response const presets = result.ok && Array.isArray(result.data) ? result.data : []; const presetNames = presets.length > 0 ? presets.map(p => p.name || p.id) : ['stat_users', 'stat_projects']; for (const dashboard of testState.createdDashboards) { // Get dashboard result = await api.get(`/dashboards/${dashboard.id}`); markEndpointTested('GET', '/dashboards/:dashboardId', result); // Get widgets result = await api.get(`/dashboards/${dashboard.id}/widgets`); markEndpointTested('GET', '/dashboards/:dashboardId/widgets', result); // Create preset widgets for (let i = 0; i < Math.min(3, presetNames.length); i++) { result = await api.post(`/dashboards/${dashboard.id}/widgets`, { widget_type: 'preset', preset_name: presetNames[i], title: `Preset Widget ${uniqueId()}`, position: { x: i * 4, y: 0, w: 4, h: 2 }, order_index: i }); markEndpointTested('POST', '/dashboards/:dashboardId/widgets', result); if (result.ok && result.data?.id) { testState.createdWidgets.push(result.data); // Get widget let widgetResult = await api.get(`/widgets/${result.data.id}`); markEndpointTested('GET', '/widgets/:widgetId', widgetResult); // Get widget data widgetResult = await api.get(`/widgets/${result.data.id}/data`); markEndpointTested('GET', '/widgets/:widgetId/data', widgetResult); // Update widget widgetResult = await api.patch(`/widgets/${result.data.id}`, { title: `Updated Preset Widget` }); markEndpointTested('PATCH', '/widgets/:widgetId', widgetResult); } } // Create custom widget result = await api.post(`/dashboards/${dashboard.id}/widgets`, { widget_type: 'custom', title: `Custom Widget ${uniqueId()}`, code: '