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
Some checks failed
API Docs (Node.js Express) / test (16) (push) Failing after 5m42s
API Docs (Node.js Express) / test (20) (push) Has been cancelled
API Docs (Node.js Express) / build (push) Has been cancelled
API Docs (Node.js Express) / security (push) Has been cancelled
API Docs (Node.js Express) / test (18) (push) Has been cancelled
LabFusion CI/CD Pipeline / api-gateway (push) Has been cancelled
LabFusion CI/CD Pipeline / service-adapters (push) Has been cancelled
LabFusion CI/CD Pipeline / api-docs (push) Has been cancelled
LabFusion CI/CD Pipeline / frontend (push) Has been cancelled
LabFusion CI/CD Pipeline / integration-tests (push) Has been cancelled
LabFusion CI/CD Pipeline / security-scan (push) Has been cancelled
Docker Build and Push / build-and-push (push) Has been cancelled
Docker Build and Push / security-scan (push) Has been cancelled
Docker Build and Push / deploy-staging (push) Has been cancelled
Docker Build and Push / deploy-production (push) Has been cancelled
Frontend (React) / test (16) (push) Has been cancelled
Frontend (React) / test (18) (push) Has been cancelled
Frontend (React) / test (20) (push) Has been cancelled
Frontend (React) / build (push) Has been cancelled
Frontend (React) / lighthouse (push) Has been cancelled
Frontend (React) / security (push) Has been cancelled
Integration Tests / performance-tests (push) Has been cancelled
Service Adapters (Python FastAPI) / security (push) Has been cancelled
Service Adapters (Python FastAPI) / test (3.1) (push) Has been cancelled
Service Adapters (Python FastAPI) / test (3.11) (push) Has been cancelled
Service Adapters (Python FastAPI) / test (3.12) (push) Has been cancelled
Service Adapters (Python FastAPI) / test (3.9) (push) Has been cancelled
Service Adapters (Python FastAPI) / build (push) Has been cancelled
Integration Tests / integration-tests (push) Has been cancelled

This commit is contained in:
glenn schrooyen
2025-09-12 22:42:47 +02:00
parent 8d1755fd52
commit 8c87c84208
11 changed files with 392 additions and 32 deletions

View File

@@ -82,7 +82,7 @@ jobs:
isort --check-only . isort --check-only .
- name: Run linting - name: Run linting
run: flake8 . run: flake8 . --count --max-complexity=10 --max-line-length=150
- name: Run tests - name: Run tests
run: | run: |

View File

@@ -58,7 +58,7 @@ jobs:
- name: Run linting - name: Run linting
run: | run: |
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 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 - name: Run type checking
run: mypy . --ignore-missing-imports run: mypy . --ignore-missing-imports

View File

@@ -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] 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 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] 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 ## Resources
- [Project Specifications](specs.md) - [Project Specifications](specs.md)

58
frontend/src/App.test.js Normal file
View File

@@ -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(<App />)
expect(screen.getByText(/LabFusion/i)).toBeInTheDocument()
})
it('renders the main dashboard', () => {
render(<App />)
// Check for common dashboard elements
expect(screen.getByText(/Dashboard/i)).toBeInTheDocument()
})
it('shows service status when online', () => {
render(<App />)
// Should show service status information
expect(screen.getByText(/Service Status/i)).toBeInTheDocument()
})
})

View File

@@ -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)
})
})
})

View File

@@ -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')
})
})
})

View File

@@ -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: ['<rootDir>/jest.setup.js']
}

View File

@@ -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'

View File

@@ -13,7 +13,7 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^17.2.2", "dotenv": "^17.2.2",
"express": "^4.21.2", "express": "^4.21.2",
"swagger-jsdoc": "^7.0.0-rc.6", "swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.0" "swagger-ui-express": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
@@ -55,9 +55,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@apidevtools/swagger-parser": { "node_modules/@apidevtools/swagger-parser": {
"version": "10.0.2", "version": "10.0.3",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.2.tgz", "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz",
"integrity": "sha512-JFxcEyp8RlNHgBCE98nwuTkZT6eNFPc1aosWV6wPcQph72TSEEu1k3baJD4/x1qznU+JiDdz8F5pTwabZh+Dhg==", "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@apidevtools/json-schema-ref-parser": "^9.0.6", "@apidevtools/json-schema-ref-parser": "^9.0.6",
@@ -65,7 +65,7 @@
"@apidevtools/swagger-methods": "^3.0.2", "@apidevtools/swagger-methods": "^3.0.2",
"@jsdevtools/ono": "^7.1.3", "@jsdevtools/ono": "^7.1.3",
"call-me-maybe": "^1.0.1", "call-me-maybe": "^1.0.1",
"z-schema": "^4.2.3" "z-schema": "^5.0.1"
}, },
"peerDependencies": { "peerDependencies": {
"openapi-types": ">=7" "openapi-types": ">=7"
@@ -2332,11 +2332,13 @@
} }
}, },
"node_modules/commander": { "node_modules/commander": {
"version": "2.20.3", "version": "6.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==",
"license": "MIT", "license": "MIT",
"optional": true "engines": {
"node": ">= 6"
}
}, },
"node_modules/component-emitter": { "node_modules/component-emitter": {
"version": "1.3.1", "version": "1.3.1",
@@ -7333,28 +7335,32 @@
} }
}, },
"node_modules/swagger-jsdoc": { "node_modules/swagger-jsdoc": {
"version": "7.0.0-rc.6", "version": "6.2.8",
"resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-7.0.0-rc.6.tgz", "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz",
"integrity": "sha512-LIvIPQxipRaOzIij+HrWOcCWTINE6OeJuqmXCfDkofVcstPVABHRkaAc3D7vrX9s7L0ccH0sH0amwHgN6+SXPg==", "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"commander": "6.2.0",
"doctrine": "3.0.0", "doctrine": "3.0.0",
"glob": "7.1.6", "glob": "7.1.6",
"lodash.mergewith": "4.6.2", "lodash.mergewith": "^4.6.2",
"swagger-parser": "10.0.2", "swagger-parser": "^10.0.3",
"yaml": "2.0.0-1" "yaml": "2.0.0-1"
}, },
"bin": {
"swagger-jsdoc": "bin/swagger-jsdoc.js"
},
"engines": { "engines": {
"node": ">=12.0.0" "node": ">=12.0.0"
} }
}, },
"node_modules/swagger-parser": { "node_modules/swagger-parser": {
"version": "10.0.2", "version": "10.0.3",
"resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.2.tgz", "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz",
"integrity": "sha512-9jHkHM+QXyLGFLk1DkXBwV+4HyNm0Za3b8/zk/+mjr8jgOSiqm3FOTHBSDsBjtn9scdL+8eWcHdupp2NLM8tDw==", "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@apidevtools/swagger-parser": "10.0.2" "@apidevtools/swagger-parser": "10.0.3"
}, },
"engines": { "engines": {
"node": ">=10" "node": ">=10"
@@ -7965,23 +7971,33 @@
} }
}, },
"node_modules/z-schema": { "node_modules/z-schema": {
"version": "4.2.4", "version": "5.0.5",
"resolved": "https://registry.npmjs.org/z-schema/-/z-schema-4.2.4.tgz", "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz",
"integrity": "sha512-YvBeW5RGNeNzKOUJs3rTL4+9rpcvHXt5I051FJbOcitV8bl40pEfcG0Q+dWSwS0/BIYrMZ/9HHoqLllMkFhD0w==", "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"validator": "^13.6.0" "validator": "^13.7.0"
}, },
"bin": { "bin": {
"z-schema": "bin/z-schema" "z-schema": "bin/z-schema"
}, },
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=8.0.0"
}, },
"optionalDependencies": { "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"
} }
} }
} }

View File

@@ -18,7 +18,7 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^17.2.2", "dotenv": "^17.2.2",
"express": "^4.21.2", "express": "^4.21.2",
"swagger-jsdoc": "^7.0.0-rc.6", "swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.0" "swagger-ui-express": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -324,8 +324,13 @@ app.get('/', swaggerUi.setup(null, {
customSiteTitle: 'LabFusion API Documentation' customSiteTitle: 'LabFusion API Documentation'
})) }))
// Start server // Export app for testing
app.listen(PORT, () => { module.exports = app
console.log(`LabFusion API Docs server running on port ${PORT}`)
console.log(`Access the documentation at: http://localhost:${PORT}`) // 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}`)
})
}