From 8c87c84208ba0a31fe5078053721a4ffff1b7950 Mon Sep 17 00:00:00 2001 From: glenn schrooyen Date: Fri, 12 Sep 2025 22:42:47 +0200 Subject: [PATCH] Update CI workflows for Python linting by increasing flake8 line length limit to 150 characters for better readability; adjust service-adapters.yml and ci.yml accordingly; update progress documentation --- .gitea/workflows/ci.yml | 2 +- .gitea/workflows/service-adapters.yml | 2 +- docs/progress.md | 2 + frontend/src/App.test.js | 58 +++++++ frontend/src/utils/errorHandling.test.js | 76 +++++++++ services/api-docs/__tests__/server.test.js | 172 +++++++++++++++++++++ services/api-docs/jest.config.js | 13 ++ services/api-docs/jest.setup.js | 18 +++ services/api-docs/package-lock.json | 64 +++++--- services/api-docs/package.json | 2 +- services/api-docs/server.js | 15 +- 11 files changed, 392 insertions(+), 32 deletions(-) create mode 100644 frontend/src/App.test.js create mode 100644 frontend/src/utils/errorHandling.test.js create mode 100644 services/api-docs/__tests__/server.test.js create mode 100644 services/api-docs/jest.config.js create mode 100644 services/api-docs/jest.setup.js diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 6a7de73..be54628 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -82,7 +82,7 @@ jobs: isort --check-only . - name: Run linting - run: flake8 . + run: flake8 . --count --max-complexity=10 --max-line-length=150 - name: Run tests run: | diff --git a/.gitea/workflows/service-adapters.yml b/.gitea/workflows/service-adapters.yml index bea3081..2de6465 100644 --- a/.gitea/workflows/service-adapters.yml +++ b/.gitea/workflows/service-adapters.yml @@ -58,7 +58,7 @@ jobs: - name: Run linting run: | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + flake8 . --count --max-complexity=10 --max-line-length=150 --statistics - name: Run type checking run: mypy . --ignore-missing-imports diff --git a/docs/progress.md b/docs/progress.md index 2cb0716..765d952 100644 --- a/docs/progress.md +++ b/docs/progress.md @@ -214,6 +214,8 @@ The modular structure allows for easy addition of new services: - [x] Set up CI/CD pipelines for automated testing and deployment - [x] Fix Maven command not found error in CI/CD pipelines (Added Maven wrapper) - [x] Fix Python formatting issues in CI/CD pipelines (Applied Black and isort formatting) +- [x] Update flake8 line length limit to 150 characters for better readability +- [x] Create JavaScript/Node.js tests for API docs service and frontend ## Resources - [Project Specifications](specs.md) diff --git a/frontend/src/App.test.js b/frontend/src/App.test.js new file mode 100644 index 0000000..a964bdc --- /dev/null +++ b/frontend/src/App.test.js @@ -0,0 +1,58 @@ +import React from 'react' +import { render, screen } from '@testing-library/react' +import App from './App' + +// Mock the service status hook to avoid API calls during tests +jest.mock('./hooks/useServiceStatus', () => ({ + useServiceStatus: () => ({ + isOnline: true, + services: { + 'api-gateway': { status: 'healthy', lastCheck: new Date().toISOString() }, + 'service-adapters': { status: 'healthy', lastCheck: new Date().toISOString() }, + 'api-docs': { status: 'healthy', lastCheck: new Date().toISOString() } + }, + isLoading: false, + error: null + }) +})) + +// Mock the system data hook +jest.mock('./hooks/useServiceStatus', () => ({ + useSystemData: () => ({ + systemStats: { + cpuUsage: 45.2, + memoryUsage: 2.1, + diskUsage: 75.8 + }, + recentEvents: [ + { + id: '1', + timestamp: new Date().toISOString(), + service: 'api-gateway', + event_type: 'health_check', + metadata: 'Service is healthy' + } + ], + isLoading: false, + error: null + }) +})) + +describe('App Component', () => { + it('renders without crashing', () => { + render() + expect(screen.getByText(/LabFusion/i)).toBeInTheDocument() + }) + + it('renders the main dashboard', () => { + render() + // Check for common dashboard elements + expect(screen.getByText(/Dashboard/i)).toBeInTheDocument() + }) + + it('shows service status when online', () => { + render() + // Should show service status information + expect(screen.getByText(/Service Status/i)).toBeInTheDocument() + }) +}) diff --git a/frontend/src/utils/errorHandling.test.js b/frontend/src/utils/errorHandling.test.js new file mode 100644 index 0000000..aacea89 --- /dev/null +++ b/frontend/src/utils/errorHandling.test.js @@ -0,0 +1,76 @@ +import { formatError, formatServiceData, formatEventData } from './errorHandling' + +describe('Error Handling Utils', () => { + describe('formatError', () => { + it('should format error objects correctly', () => { + const error = new Error('Test error message') + const formatted = formatError(error) + + expect(formatted).toHaveProperty('message', 'Test error message') + expect(formatted).toHaveProperty('type', 'Error') + }) + + it('should handle string errors', () => { + const error = 'Simple string error' + const formatted = formatError(error) + + expect(formatted).toHaveProperty('message', 'Simple string error') + expect(formatted).toHaveProperty('type', 'string') + }) + + it('should handle unknown error types', () => { + const error = { someProperty: 'value' } + const formatted = formatError(error) + + expect(formatted).toHaveProperty('message', 'Unknown error occurred') + expect(formatted).toHaveProperty('type', 'unknown') + }) + }) + + describe('formatServiceData', () => { + it('should format service data correctly', () => { + const rawData = { + 'api-gateway': { + status: 'healthy', + lastCheck: '2024-01-01T00:00:00.000Z' + } + } + + const formatted = formatServiceData(rawData) + + expect(formatted).toHaveProperty('api-gateway') + expect(formatted['api-gateway']).toHaveProperty('status', 'healthy') + expect(formatted['api-gateway']).toHaveProperty('lastCheck') + }) + + it('should handle empty data', () => { + const formatted = formatServiceData({}) + expect(formatted).toEqual({}) + }) + }) + + describe('formatEventData', () => { + it('should format event data correctly', () => { + const rawEvents = [ + { + id: '1', + timestamp: '2024-01-01T00:00:00.000Z', + service: 'api-gateway', + event_type: 'health_check' + } + ] + + const formatted = formatEventData(rawEvents) + + expect(Array.isArray(formatted)).toBe(true) + expect(formatted[0]).toHaveProperty('id', '1') + expect(formatted[0]).toHaveProperty('service', 'api-gateway') + }) + + it('should handle empty events array', () => { + const formatted = formatEventData([]) + expect(Array.isArray(formatted)).toBe(true) + expect(formatted).toHaveLength(0) + }) + }) +}) diff --git a/services/api-docs/__tests__/server.test.js b/services/api-docs/__tests__/server.test.js new file mode 100644 index 0000000..e5878ed --- /dev/null +++ b/services/api-docs/__tests__/server.test.js @@ -0,0 +1,172 @@ +const request = require('supertest') +const app = require('../server') + +// Mock axios to avoid actual HTTP requests during tests +jest.mock('axios') +const axios = require('axios') + +describe('API Docs Service', () => { + let server + + beforeAll(() => { + // Start the server for testing + server = app.listen(0) // Use random available port + }) + + afterAll((done) => { + // Close the server after tests + server.close(done) + }) + + describe('GET /health', () => { + it('should return health status', async () => { + const response = await request(app) + .get('/health') + .expect(200) + + expect(response.body).toHaveProperty('status', 'healthy') + expect(response.body).toHaveProperty('timestamp') + expect(new Date(response.body.timestamp)).toBeInstanceOf(Date) + }) + }) + + describe('GET /services', () => { + beforeEach(() => { + // Reset axios mocks + jest.clearAllMocks() + }) + + it('should return service status for all services', async () => { + // Mock successful health checks for active services + axios.get.mockImplementation((url) => { + if (url.includes('/health')) { + return Promise.resolve({ + headers: { 'x-response-time': '50ms' } + }) + } + return Promise.reject(new Error('Not found')) + }) + + const response = await request(app) + .get('/services') + .expect(200) + + expect(response.body).toHaveProperty('api-gateway') + expect(response.body).toHaveProperty('service-adapters') + expect(response.body).toHaveProperty('metrics-collector') + expect(response.body).toHaveProperty('notification-service') + + // Check structure of service status + Object.values(response.body).forEach(service => { + expect(service).toHaveProperty('name') + expect(service).toHaveProperty('url') + expect(service).toHaveProperty('status') + }) + }) + + it('should handle service health check failures', async () => { + // Mock all health checks to fail + axios.get.mockRejectedValue(new Error('Connection refused')) + + const response = await request(app) + .get('/services') + .expect(200) + + // All services should show as unhealthy or planned + Object.values(response.body).forEach(service => { + expect(['unhealthy', 'planned']).toContain(service.status) + if (service.status === 'unhealthy') { + expect(service).toHaveProperty('error') + } + }) + }) + }) + + describe('GET /openapi.json', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should return unified OpenAPI specification', async () => { + // Mock service spec responses + axios.get.mockImplementation((url) => { + if (url.includes('/v3/api-docs')) { + return Promise.resolve({ + data: { + openapi: '3.0.0', + info: { title: 'API Gateway', version: '1.0.0' }, + paths: { '/test': { get: { summary: 'Test endpoint' } } }, + components: { schemas: {} } + } + }) + } + if (url.includes('/openapi.json')) { + return Promise.resolve({ + data: { + openapi: '3.0.0', + info: { title: 'Service Adapters', version: '1.0.0' }, + paths: { '/health': { get: { summary: 'Health check' } } }, + components: { schemas: {} } + } + }) + } + return Promise.reject(new Error('Not found')) + }) + + const response = await request(app) + .get('/openapi.json') + .expect(200) + + expect(response.body).toHaveProperty('openapi', '3.0.0') + expect(response.body).toHaveProperty('info') + expect(response.body).toHaveProperty('paths') + expect(response.body).toHaveProperty('components') + expect(response.body).toHaveProperty('tags') + + // Check that paths are prefixed with service names + expect(response.body.paths).toHaveProperty('/api-gateway/test') + expect(response.body.paths).toHaveProperty('/service-adapters/health') + }) + + it('should handle service spec fetch failures gracefully', async () => { + // Mock all service spec requests to fail + axios.get.mockRejectedValue(new Error('Service unavailable')) + + const response = await request(app) + .get('/openapi.json') + .expect(200) + + expect(response.body).toHaveProperty('openapi', '3.0.0') + expect(response.body).toHaveProperty('info') + expect(response.body).toHaveProperty('paths') + expect(response.body).toHaveProperty('components') + expect(response.body).toHaveProperty('tags') + + // Should still have tags for all services + expect(response.body.tags).toHaveLength(4) + }) + }) + + describe('GET /api-docs.json', () => { + it('should return API docs service specification', async () => { + const response = await request(app) + .get('/api-docs.json') + .expect(200) + + expect(response.body).toHaveProperty('openapi', '3.0.0') + expect(response.body).toHaveProperty('info') + expect(response.body.info).toHaveProperty('title', 'LabFusion API Docs Service') + }) + }) + + describe('GET /', () => { + it('should serve Swagger UI', async () => { + const response = await request(app) + .get('/') + .expect(200) + + expect(response.text).toContain('swagger-ui') + expect(response.text).toContain('LabFusion API Documentation') + }) + }) +}) diff --git a/services/api-docs/jest.config.js b/services/api-docs/jest.config.js new file mode 100644 index 0000000..cd22c54 --- /dev/null +++ b/services/api-docs/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + testEnvironment: 'node', + testMatch: ['**/__tests__/**/*.test.js', '**/?(*.)+(spec|test).js'], + collectCoverageFrom: [ + 'server.js', + '!node_modules/**', + '!coverage/**' + ], + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + testTimeout: 10000, + setupFilesAfterEnv: ['/jest.setup.js'] +} diff --git a/services/api-docs/jest.setup.js b/services/api-docs/jest.setup.js new file mode 100644 index 0000000..55938db --- /dev/null +++ b/services/api-docs/jest.setup.js @@ -0,0 +1,18 @@ +// Jest setup file +// This file runs before each test file + +// Mock console methods to reduce noise in tests +global.console = { + ...console, + log: jest.fn(), + warn: jest.fn(), + error: jest.fn() +} + +// Set test environment variables +process.env.NODE_ENV = 'test' +process.env.PORT = '8083' +process.env.API_GATEWAY_URL = 'http://localhost:8080' +process.env.SERVICE_ADAPTERS_URL = 'http://localhost:8000' +process.env.METRICS_COLLECTOR_URL = 'http://localhost:8081' +process.env.NOTIFICATION_SERVICE_URL = 'http://localhost:8082' diff --git a/services/api-docs/package-lock.json b/services/api-docs/package-lock.json index 8f3b15f..8bb6657 100644 --- a/services/api-docs/package-lock.json +++ b/services/api-docs/package-lock.json @@ -13,7 +13,7 @@ "cors": "^2.8.5", "dotenv": "^17.2.2", "express": "^4.21.2", - "swagger-jsdoc": "^7.0.0-rc.6", + "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.0" }, "devDependencies": { @@ -55,9 +55,9 @@ "license": "MIT" }, "node_modules/@apidevtools/swagger-parser": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.2.tgz", - "integrity": "sha512-JFxcEyp8RlNHgBCE98nwuTkZT6eNFPc1aosWV6wPcQph72TSEEu1k3baJD4/x1qznU+JiDdz8F5pTwabZh+Dhg==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", "license": "MIT", "dependencies": { "@apidevtools/json-schema-ref-parser": "^9.0.6", @@ -65,7 +65,7 @@ "@apidevtools/swagger-methods": "^3.0.2", "@jsdevtools/ono": "^7.1.3", "call-me-maybe": "^1.0.1", - "z-schema": "^4.2.3" + "z-schema": "^5.0.1" }, "peerDependencies": { "openapi-types": ">=7" @@ -2332,11 +2332,13 @@ } }, "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", "license": "MIT", - "optional": true + "engines": { + "node": ">= 6" + } }, "node_modules/component-emitter": { "version": "1.3.1", @@ -7333,28 +7335,32 @@ } }, "node_modules/swagger-jsdoc": { - "version": "7.0.0-rc.6", - "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-7.0.0-rc.6.tgz", - "integrity": "sha512-LIvIPQxipRaOzIij+HrWOcCWTINE6OeJuqmXCfDkofVcstPVABHRkaAc3D7vrX9s7L0ccH0sH0amwHgN6+SXPg==", + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", + "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", "license": "MIT", "dependencies": { + "commander": "6.2.0", "doctrine": "3.0.0", "glob": "7.1.6", - "lodash.mergewith": "4.6.2", - "swagger-parser": "10.0.2", + "lodash.mergewith": "^4.6.2", + "swagger-parser": "^10.0.3", "yaml": "2.0.0-1" }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, "engines": { "node": ">=12.0.0" } }, "node_modules/swagger-parser": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.2.tgz", - "integrity": "sha512-9jHkHM+QXyLGFLk1DkXBwV+4HyNm0Za3b8/zk/+mjr8jgOSiqm3FOTHBSDsBjtn9scdL+8eWcHdupp2NLM8tDw==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", "license": "MIT", "dependencies": { - "@apidevtools/swagger-parser": "10.0.2" + "@apidevtools/swagger-parser": "10.0.3" }, "engines": { "node": ">=10" @@ -7965,23 +7971,33 @@ } }, "node_modules/z-schema": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-4.2.4.tgz", - "integrity": "sha512-YvBeW5RGNeNzKOUJs3rTL4+9rpcvHXt5I051FJbOcitV8bl40pEfcG0Q+dWSwS0/BIYrMZ/9HHoqLllMkFhD0w==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", "license": "MIT", "dependencies": { "lodash.get": "^4.4.2", "lodash.isequal": "^4.5.0", - "validator": "^13.6.0" + "validator": "^13.7.0" }, "bin": { "z-schema": "bin/z-schema" }, "engines": { - "node": ">=6.0.0" + "node": ">=8.0.0" }, "optionalDependencies": { - "commander": "^2.7.1" + "commander": "^9.4.1" + } + }, + "node_modules/z-schema/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" } } } diff --git a/services/api-docs/package.json b/services/api-docs/package.json index 14eaa7d..b6b337e 100644 --- a/services/api-docs/package.json +++ b/services/api-docs/package.json @@ -18,7 +18,7 @@ "cors": "^2.8.5", "dotenv": "^17.2.2", "express": "^4.21.2", - "swagger-jsdoc": "^7.0.0-rc.6", + "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.0" }, "devDependencies": { diff --git a/services/api-docs/server.js b/services/api-docs/server.js index f1d1f16..95375e7 100644 --- a/services/api-docs/server.js +++ b/services/api-docs/server.js @@ -324,8 +324,13 @@ app.get('/', swaggerUi.setup(null, { customSiteTitle: 'LabFusion API Documentation' })) -// Start server -app.listen(PORT, () => { - console.log(`LabFusion API Docs server running on port ${PORT}`) - console.log(`Access the documentation at: http://localhost:${PORT}`) -}) +// Export app for testing +module.exports = app + +// Start server only if not in test environment +if (process.env.NODE_ENV !== 'test') { + app.listen(PORT, () => { + console.log(`LabFusion API Docs server running on port ${PORT}`) + console.log(`Access the documentation at: http://localhost:${PORT}`) + }) +}