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}`)
+ })
+}