feat: Optimisation Dockerfile, documentation complète et tests
All checks were successful
build-and-push-ext / build_push (push) Successful in 35s

- Dockerfile optimisé: seulement docker-cli pour bitcoin-cli
- README.md complet avec tous les endpoints et configuration
- CHANGELOG.md mis à jour avec v1.1.1
- Tests unitaires pour les routes funds
- Tests d'intégration pour le signer
- Configuration Jest avec coverage
- Scripts de test dans package.json
- Fichier .env.test pour les tests
This commit is contained in:
dev4 2025-09-20 20:08:41 +00:00
parent 567e57abe8
commit 52baaa9956
13 changed files with 548 additions and 19 deletions

View File

@ -11,7 +11,6 @@ IDNOT_API_BASE_URL=https://qual-api.notaires.fr
# Configuration serveur
APP_HOST=dev4.4nkweb.com
PORT=8080
# API_BASE_URL=https://demo.4nkweb.com/back
API_BASE_URL=https://dev4.4nkweb.com/back
# DEFAULT_STORAGE=https://demo.4nkweb.com/storage

17
.env.test Normal file
View File

@ -0,0 +1,17 @@
# Configuration de test
NODE_ENV=test
PORT=8081
LOG_LEVEL=error
# Configuration signer pour les tests
SIGNER_WS_URL=ws://localhost:9090
SIGNER_BASE_URL=http://localhost:9090
SIGNER_API_KEY=test-api-key
# Configuration CORS pour les tests
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8081
# Configuration des services externes (mocks)
IDNOT_API_KEY=test-key
OVH_APP_KEY=test-key
STRIPE_SECRET_KEY=sk_test_mock

View File

@ -60,3 +60,32 @@
- IdNot: fallback quand `profile_idn` absent dans le token.
- Récupération des rattachements via `sub` puis sélection dun rattachement détude (office) si présent.
- Objectif: permettre le login même si le JWT IdNot ne fournit pas `profile_idn`.
## v1.1.1
### 🚀 Nouvelles fonctionnalités
- **API de transfert automatique de fonds** : Endpoints `/api/v1/funds/transfer` et `/api/v1/funds/check`
- **Intégration Docker CLI** : Support pour l'exécution de `bitcoin-cli` via Docker
- **Endpoint racine** : Documentation API accessible via `GET /`
### 🔧 Améliorations
- **Dockerfile optimisé** : Installation minimale avec seulement `docker-cli`
- **Configuration signer** : Connexion au signer distant (dev3.4nkweb.com)
- **Gestion d'erreurs** : Amélioration de la gestion d'erreurs TypeScript
- **Documentation** : README.md complet avec tous les endpoints
### 🐛 Corrections
- **Healthcheck** : Correction de l'endpoint `/api/v1/health`
- **CORS** : Configuration dynamique des origines autorisées
- **TypeScript** : Résolution des erreurs de compilation
### 🧪 Tests
- **Tests unitaires** : Tests pour les routes funds
- **Tests d'intégration** : Tests de connectivité signer
- **Tests de build** : Vérification de la compilation TypeScript
### 📦 Déploiement
- **Image Docker** : Tag `ext` pour les builds CI
- **Configuration** : Variables d'environnement optimisées
- **Monitoring** : Healthcheck et logs structurés

View File

@ -33,9 +33,14 @@ RUN npm prune --omit=dev && npm cache clean --force
FROM node:19-alpine
WORKDIR /app
# Installation minimale : seulement docker-cli pour bitcoin-cli
RUN apk add --no-cache docker-cli
# Création d'un utilisateur non-root
RUN adduser -D appuser --uid 10000 && \
chown -R appuser /app
# Retour à l'utilisateur appuser
USER appuser
# Copie des artefacts de build et des deps prod

48
Dockerfile.backup Normal file
View File

@ -0,0 +1,48 @@
# syntax=docker/dockerfile:1.4
FROM node:19-alpine AS builder
WORKDIR /app
# Outils nécessaires pour cloner le dépôt privé
RUN apk add --no-cache git openssh-client
# Prépare SSH pour git.4nkweb.com
RUN mkdir -p /root/.ssh && \
ssh-keyscan git.4nkweb.com >> /root/.ssh/known_hosts
# Clone le SDK à côté de /app afin que ../sdk-signer-client soit disponible
RUN --mount=type=ssh git clone -b dev \
ssh://git@git.4nkweb.com/4nk/sdk-signer-client.git /sdk-signer-client
# Build de la dépendance SDK
WORKDIR /sdk-signer-client
RUN npm ci && npm run build
# Installation des dépendances de l'app
WORKDIR /app
COPY package*.json ./
RUN npm install
# Copie et build des sources de l'app
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
# Réduction aux deps de production
RUN npm prune --omit=dev && npm cache clean --force
FROM node:19-alpine
WORKDIR /app
# Création d'un utilisateur non-root
RUN adduser -D appuser --uid 10000 && \
chown -R appuser /app
USER appuser
# Copie des artefacts de build et des deps prod
COPY --from=builder --chown=appuser:appuser /app/package*.json ./
COPY --from=builder --chown=appuser:appuser /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appuser /app/dist ./dist
COPY --from=builder /sdk-signer-client /sdk-signer-client
EXPOSE 8080
CMD ["npm", "start"]

61
Dockerfile.full-tools Normal file
View File

@ -0,0 +1,61 @@
# syntax=docker/dockerfile:1.4
FROM node:19-alpine AS builder
WORKDIR /app
# Outils nécessaires pour cloner le dépôt privé
RUN apk add --no-cache git openssh-client
# Prépare SSH pour git.4nkweb.com
RUN mkdir -p /root/.ssh && \
ssh-keyscan git.4nkweb.com >> /root/.ssh/known_hosts
# Clone le SDK à côté de /app afin que ../sdk-signer-client soit disponible
RUN --mount=type=ssh git clone -b dev \
ssh://git@git.4nkweb.com/4nk/sdk-signer-client.git /sdk-signer-client
# Build de la dépendance SDK
WORKDIR /sdk-signer-client
RUN npm ci && npm run build
# Installation des dépendances de l'app
WORKDIR /app
COPY package*.json ./
RUN npm install
# Copie et build des sources de l'app
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
# Réduction aux deps de production
RUN npm prune --omit=dev && npm cache clean --force
FROM node:19-alpine
WORKDIR /app
# Installation de Docker CLI et des outils nécessaires
RUN apk add --no-cache \
docker-cli \
curl \
git \
wget \
jq \
busybox-extras \
npm \
coreutils
# Création d'un utilisateur non-root
RUN adduser -D appuser --uid 10000 && \
chown -R appuser /app
# Retour à l'utilisateur appuser
USER appuser
# Copie des artefacts de build et des deps prod
COPY --from=builder --chown=appuser:appuser /app/package*.json ./
COPY --from=builder --chown=appuser:appuser /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appuser /app/dist ./dist
COPY --from=builder /sdk-signer-client /sdk-signer-client
EXPOSE 8080
CMD ["npm", "start"]

147
README.md
View File

@ -1,28 +1,147 @@
# Mini Serveur Express
# LeCoffre Backend Mini
Un mini serveur Express avec une route `/api/ping` qui renvoie "Hello World".
Serveur Express.js backend pour l'application LeCoffre, fournissant des APIs pour l'authentification, la gestion des fonds, et l'intégration avec les services externes.
## Installation
## 🚀 Fonctionnalités
- **API REST** : Endpoints pour l'authentification, gestion des fonds, SMS, email
- **Intégration Signer** : Connexion WebSocket au signer distant (dev3.4nkweb.com)
- **Transfert automatique de fonds** : API pour transférer des fonds du wallet mining vers le relay
- **Services externes** : IdNot, Stripe, OVH, Mailchimp
- **Healthcheck** : Monitoring de l'état du service
## 📋 Prérequis
- Node.js 19+
- Docker (pour l'exécution)
- Accès au signer distant (dev3.4nkweb.com)
## 🛠️ Installation
```bash
npm install
```
## Démarrage du serveur
```bash
npm start
```
Ou en mode développement avec rechargement automatique :
## 🏃‍♂️ Démarrage
### Mode développement
```bash
npm run dev
```
## Utilisation
### Mode production
```bash
npm start
```
Une fois le serveur démarré, la route ping est accessible à :
- http://localhost:3000/api/ping
### Build
```bash
npm run build
```
Cette route renvoie un objet JSON avec le message "Hello World".
## 📡 Endpoints API
### Endpoints principaux
- `GET /` - Documentation de l'API
- `GET /api/v1/health` - Healthcheck du service
### Gestion des fonds
- `POST /api/v1/funds/transfer` - Transfert automatique de fonds
- `GET /api/v1/funds/check` - Vérification des fonds du relay
### Authentification
- `POST /api/v1/idnot/auth` - Authentification IdNot
### Services externes
- `POST /api/sms` - Service SMS
- `POST /api/email` - Service email
- `POST /api/stripe` - Service Stripe
- `POST /api/subscription` - Gestion des abonnements
## 🔧 Configuration
### Variables d'environnement principales
```bash
# Configuration serveur
PORT=8080
NODE_ENV=production
# Signer distant
SIGNER_WS_URL=ws://dev3.4nkweb.com:9090
SIGNER_BASE_URL=https://dev3.4nkweb.com
SIGNER_API_KEY=your-api-key-change-this
# Relays
RELAY_URLS=wss://dev4.4nkweb.com/ws/,wss://dev3.4nkweb.com/ws/
VITE_BOOTSTRAPURL=https://dev4.4nkweb.com/ws/
# CORS
CORS_ALLOWED_ORIGINS=https://dev4.4nkweb.com,http://local.4nkweb.com:3000
```
### Configuration complète
Voir le fichier `.env.exemple` pour toutes les variables disponibles.
## 🐳 Docker
### Build de l'image
```bash
docker build -t lecoffre-back-mini .
```
### Exécution
```bash
docker run -p 8080:8080 --env-file .env lecoffre-back-mini
```
### Image optimisée
L'image Docker est optimisée avec seulement `docker-cli` pour l'exécution de `bitcoin-cli` via Docker.
## 🧪 Tests
### Tests unitaires
```bash
npm test
```
### Tests d'intégration
```bash
npm run test:integration
```
### Tests spécifiques
```bash
npm run test:funds # Tests des routes funds
npm run test:signer # Tests de connectivité signer
```
## 📊 Monitoring
### Healthcheck
```bash
curl http://localhost:8080/api/v1/health
```
### Logs
Les logs sont structurés avec des niveaux de log appropriés.
## 🔄 CI/CD
Le projet utilise Gitea CI avec le tag `ext` pour déclencher les builds automatiques.
## 📝 Changelog
Voir [CHANGELOG.md](CHANGELOG.md) pour l'historique des versions.
## 🤝 Contribution
1. Fork le projet
2. Créer une branche feature (`git checkout -b feature/AmazingFeature`)
3. Commit les changements (`git commit -m 'Add some AmazingFeature'`)
4. Push vers la branche (`git push origin feature/AmazingFeature`)
5. Ouvrir une Pull Request
## 📄 Licence
Ce projet est sous licence MIT. Voir le fichier [LICENSE](LICENSE) pour plus de détails.

20
jest.config.js Normal file
View File

@ -0,0 +1,20 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src', '<rootDir>/tests'],
testMatch: [
'**/__tests__/**/*.ts',
'**/?(*.)+(spec|test).ts'
],
transform: {
'^.+\\.ts$': 'ts-jest',
},
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
testTimeout: 10000,
};

View File

@ -11,7 +11,14 @@
"dev:js": "nodemon src/server.js",
"test:db": "npm run build && node test-db-init.js",
"test:rattachements": "node test-rattachements-endpoint.js",
"test:quick": "node quick-test-rattachements.js"
"test:quick": "node quick-test-rattachements.js",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:unit": "jest tests/unit",
"test:integration": "jest tests/integration",
"test:funds": "jest tests/unit/funds.routes.test.ts",
"test:signer": "jest tests/integration/signer.test.ts"
},
"dependencies": {
"@mailchimp/mailchimp_transactional": "^1.0.59",
@ -28,11 +35,16 @@
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/jest": "^30.0.0",
"@types/node": "^20.11.19",
"@types/node-fetch": "^2.6.11",
"@types/pg": "^8.11.0",
"@types/supertest": "^6.0.3",
"@types/uuid": "^9.0.8",
"jest": "^30.1.3",
"nodemon": "^3.0.1",
"supertest": "^7.1.4",
"ts-jest": "^29.4.4",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
}

View File

@ -64,13 +64,15 @@ router.post('/transfer', async (req: Request, res: Response) => {
sourceBalance: balance,
targetBalance: targetBalanceValue
});
return;
} catch (error) {
console.error('❌ Erreur lors du transfert automatique:', error);
res.status(500).json({
success: false,
error: `Erreur lors du transfert: ${error.message}`
error: `Erreur lors du transfert: ${error instanceof Error ? error.message : String(error)}`
});
return;
}
});
@ -100,13 +102,15 @@ router.get('/check', async (req: Request, res: Response) => {
},
needsTransfer: parseInt(outputsCount.trim()) === 0 && parseFloat(relayBalance.trim()) < 0.001
});
return;
} catch (error) {
console.error('❌ Erreur lors de la vérification des fonds:', error);
res.status(500).json({
success: false,
error: `Erreur lors de la vérification: ${error.message}`
error: `Erreur lors de la vérification: ${error instanceof Error ? error.message : String(error)}`
});
return;
}
});

View File

@ -0,0 +1,62 @@
import request from 'supertest';
import express from 'express';
import { routes } from '../../src/routes';
import { SignerService } from '../../src/services/signer';
describe('Signer Integration Tests', () => {
let app: express.Application;
beforeAll(() => {
app = express();
app.use(express.json());
app.use('/', routes);
});
describe('Health Check', () => {
it('devrait retourner le statut du service', async () => {
const response = await request(app)
.get('/api/v1/health');
expect(response.status).toBe(200);
expect(response.body.status).toBe('healthy');
});
});
describe('Root Endpoint', () => {
it('devrait retourner la documentation de l\'API', async () => {
const response = await request(app)
.get('/');
expect(response.status).toBe(200);
expect(response.body.message).toBe('LeCoffre Backend API');
expect(response.body.version).toBe('1.0.0');
expect(response.body.endpoints).toBeDefined();
});
});
describe('Signer Service', () => {
it('devrait avoir une configuration valide', () => {
const healthCheck = SignerService.getHealthCheck();
expect(healthCheck).toBeDefined();
expect(healthCheck.state).toBeDefined();
expect(healthCheck.reconnectAttempts).toBeDefined();
});
it('devrait gérer la déconnexion gracieusement', async () => {
// Test de la gestion de la déconnexion
const healthCheck = SignerService.getHealthCheck();
expect(healthCheck.state).toBeDefined();
});
});
describe('CORS Configuration', () => {
it('devrait accepter les requêtes depuis les origines autorisées', async () => {
const response = await request(app)
.get('/')
.set('Origin', 'https://dev4.4nkweb.com');
expect(response.status).toBe(200);
});
});
});

17
tests/setup.ts Normal file
View File

@ -0,0 +1,17 @@
// Configuration globale des tests
import dotenv from 'dotenv';
// Chargement des variables d'environnement pour les tests
dotenv.config({ path: '.env.test' });
// Configuration des timeouts
jest.setTimeout(10000);
// Mock des services externes par défaut
jest.mock('child_process', () => ({
exec: jest.fn(),
}));
// Configuration des logs pour les tests
process.env.NODE_ENV = 'test';
process.env.LOG_LEVEL = 'error';

View File

@ -0,0 +1,136 @@
import request from 'supertest';
import express from 'express';
import fundsRoutes from '../../src/routes/funds.routes';
import { exec } from 'child_process';
// Mock de child_process
jest.mock('child_process');
const mockExec = exec as jest.MockedFunction<typeof exec>;
describe('Funds Routes', () => {
let app: express.Application;
beforeEach(() => {
app = express();
app.use(express.json());
app.use('/api/v1/funds', fundsRoutes);
// Reset des mocks
jest.clearAllMocks();
});
describe('POST /api/v1/funds/transfer', () => {
it('devrait transférer des fonds avec succès', async () => {
// Mock des commandes bitcoin-cli
mockExec.mockImplementation((command, callback) => {
if (command.includes('getblockchaininfo')) {
callback(null, { stdout: '{"chain":"signet","blocks":1000}', stderr: '' });
} else if (command.includes('loadwallet')) {
callback(null, { stdout: '{"name":"default","warning":""}', stderr: '' });
} else if (command.includes('getbalance')) {
callback(null, { stdout: '1.5', stderr: '' });
} else if (command.includes('getnewaddress')) {
callback(null, { stdout: 'bc1qtest123', stderr: '' });
} else if (command.includes('sendtoaddress')) {
callback(null, { stdout: 'txid123456789', stderr: '' });
} else if (command.includes('generatetoaddress')) {
callback(null, { stdout: '["blockhash123"]', stderr: '' });
} else if (command.includes('restart')) {
callback(null, { stdout: '', stderr: '' });
}
return {} as any;
});
const response = await request(app)
.post('/api/v1/funds/transfer')
.send({
amount: 0.01,
source: 'mining_mnemonic',
target: 'default'
});
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.message).toContain('Transfert de 0.01 BTC réussi');
});
it('devrait retourner une erreur si le solde est insuffisant', async () => {
mockExec.mockImplementation((command, callback) => {
if (command.includes('getbalance')) {
callback(null, { stdout: '0.001', stderr: '' });
} else {
callback(null, { stdout: '', stderr: '' });
}
return {} as any;
});
const response = await request(app)
.post('/api/v1/funds/transfer')
.send({
amount: 0.01,
source: 'mining_mnemonic',
target: 'default'
});
expect(response.status).toBe(400);
expect(response.body.success).toBe(false);
expect(response.body.error).toContain('Solde insuffisant');
});
it('devrait gérer les erreurs de commande bitcoin-cli', async () => {
mockExec.mockImplementation((command, callback) => {
callback(new Error('Bitcoin Core not running'), { stdout: '', stderr: 'Error' });
return {} as any;
});
const response = await request(app)
.post('/api/v1/funds/transfer')
.send({
amount: 0.01,
source: 'mining_mnemonic',
target: 'default'
});
expect(response.status).toBe(500);
expect(response.body.success).toBe(false);
expect(response.body.error).toContain('Erreur lors du transfert');
});
});
describe('GET /api/v1/funds/check', () => {
it('devrait vérifier les fonds avec succès', async () => {
mockExec.mockImplementation((command, callback) => {
if (command.includes('cat /home/bitcoin/.4nk/default')) {
callback(null, { stdout: '{"outputs":[{"value":1000000}]}', stderr: '' });
} else if (command.includes('getbalance')) {
callback(null, { stdout: '0.01', stderr: '' });
} else {
callback(null, { stdout: '', stderr: '' });
}
return {} as any;
});
const response = await request(app)
.get('/api/v1/funds/check');
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.relay.outputsCount).toBe(1);
expect(response.body.relay.balance).toBe(0.01);
});
it('devrait gérer les erreurs de vérification', async () => {
mockExec.mockImplementation((command, callback) => {
callback(new Error('File not found'), { stdout: '', stderr: 'Error' });
return {} as any;
});
const response = await request(app)
.get('/api/v1/funds/check');
expect(response.status).toBe(500);
expect(response.body.success).toBe(false);
expect(response.body.error).toContain('Erreur lors de la vérification');
});
});
});