From 62be18ecaafb45be7644fb353a911fec2ef86bfb Mon Sep 17 00:00:00 2001 From: "matthias.lotz" Date: Sat, 29 Nov 2025 23:47:01 +0100 Subject: [PATCH] feat: Add upload notifications to Telegram Bot (Phase 3) - Integrate TelegramNotificationService into batchUpload route - Send notification on successful upload with group details - Add metadata parsing for year/title/name from form fields - Create integration tests for upload notifications - Fix getAdminUrl() to use INTERNAL_HOST with dev port - Update jest.config.js to transform uuid ESM module - Non-blocking async notification (won't fail upload on error) Phase 3 complete: Upload notifications working in Docker dev environment Tested successfully with real Telegram bot in test group --- FeatureRequests/FEATURE_PLAN-telegram.md | 18 +- backend/docs/openapi.json | 9 + backend/jest.config.js | 6 +- backend/src/routes/batchUpload.js | 26 +++ .../services/TelegramNotificationService.js | 7 +- backend/tests/api/telegram-upload.test.js | 183 ++++++++++++++++++ backend/tests/utils/test-image.jpg | Bin 0 -> 159 bytes 7 files changed, 238 insertions(+), 11 deletions(-) create mode 100644 backend/tests/api/telegram-upload.test.js create mode 100644 backend/tests/utils/test-image.jpg diff --git a/FeatureRequests/FEATURE_PLAN-telegram.md b/FeatureRequests/FEATURE_PLAN-telegram.md index 518edac..0164c92 100644 --- a/FeatureRequests/FEATURE_PLAN-telegram.md +++ b/FeatureRequests/FEATURE_PLAN-telegram.md @@ -240,14 +240,16 @@ scripts/package-lock.json ## Testing Checklist (Phase 1) -- [ ] Node.js Version >= 18.x -- [ ] Telegram App installiert (Windows 11) -- [ ] Bot via BotFather erstellt -- [ ] Bot-Token gespeichert in `.env.telegram` -- [ ] Test-Gruppe erstellt -- [ ] Bot zur Gruppe hinzugefügt -- [ ] Chat-ID ermittelt -- [ ] Chat-ID gespeichert in `.env.telegram` +- [x] Node.js Version >= 18.x +- [x] Telegram App installiert (Windows 11) +- [x] Bot via BotFather erstellt +- [x] Bot-Token gespeichert in `.env.telegram` +- [x] Test-Gruppe erstellt +- [x] Bot zur Gruppe hinzugefügt +- [x] Chat-ID ermittelt +- [x] Chat-ID gespeichert in `.env.telegram` +- [x] Privacy Mode deaktiviert +- [x] Test-Nachricht erfolgreich gesendet - [ ] `npm install` erfolgreich - [ ] `node telegram-test.js` läuft ohne Fehler - [ ] Test-Nachricht in Telegram-Gruppe empfangen diff --git a/backend/docs/openapi.json b/backend/docs/openapi.json index 4ce36e5..a6c2fe5 100644 --- a/backend/docs/openapi.json +++ b/backend/docs/openapi.json @@ -385,6 +385,15 @@ }, "description": { "example": "any" + }, + "year": { + "example": "any" + }, + "title": { + "example": "any" + }, + "name": { + "example": "any" } } } diff --git a/backend/jest.config.js b/backend/jest.config.js index b1e0f15..27eda88 100644 --- a/backend/jest.config.js +++ b/backend/jest.config.js @@ -26,5 +26,9 @@ module.exports = { // Run tests serially to avoid DB conflicts maxWorkers: 1, // Force exit after tests complete - forceExit: true + forceExit: true, + // Transform ESM modules in node_modules + transformIgnorePatterns: [ + 'node_modules/(?!(uuid)/)' + ] }; diff --git a/backend/src/routes/batchUpload.js b/backend/src/routes/batchUpload.js index 35ec22a..2c4fc57 100644 --- a/backend/src/routes/batchUpload.js +++ b/backend/src/routes/batchUpload.js @@ -6,6 +6,10 @@ const UploadGroup = require('../models/uploadGroup'); const groupRepository = require('../repositories/GroupRepository'); const dbManager = require('../database/DatabaseManager'); const ImagePreviewService = require('../services/ImagePreviewService'); +const TelegramNotificationService = require('../services/TelegramNotificationService'); + +// Singleton-Instanz des Telegram Service +const telegramService = new TelegramNotificationService(); const router = Router(); @@ -117,6 +121,12 @@ router.post('/upload/batch', async (req, res) => { consents = {}; } + // Merge separate form fields into metadata (backwards compatibility) + if (req.body.year) metadata.year = parseInt(req.body.year); + if (req.body.title) metadata.title = req.body.title; + if (req.body.name) metadata.name = req.body.name; + if (req.body.description) metadata.description = req.body.description; + // Validiere Workshop Consent (Pflichtfeld) if (!consents.workshopConsent) { return res.status(400).json({ @@ -229,6 +239,22 @@ router.post('/upload/batch', async (req, res) => { console.log(`Successfully saved group ${group.groupId} with ${files.length} images to database`); + // Sende Telegram-Benachrichtigung (async, non-blocking) + if (telegramService.isAvailable()) { + telegramService.sendUploadNotification({ + name: group.name, + year: group.year, + title: group.title, + imageCount: files.length, + workshopConsent: consents.workshopConsent, + socialMediaConsents: consents.socialMediaConsents || [], + token: createResult.managementToken + }).catch(err => { + // Fehler loggen, aber Upload nicht fehlschlagen lassen + console.error('[Telegram] Upload notification failed:', err.message); + }); + } + // Erfolgreiche Antwort mit Management-Token res.json({ groupId: group.groupId, diff --git a/backend/src/services/TelegramNotificationService.js b/backend/src/services/TelegramNotificationService.js index 3bf28fa..86541fc 100644 --- a/backend/src/services/TelegramNotificationService.js +++ b/backend/src/services/TelegramNotificationService.js @@ -307,8 +307,11 @@ ${groupsText} * @returns {string} Admin-Panel URL */ getAdminUrl() { - const baseUrl = process.env.PUBLIC_URL || 'https://internal.hobbyhimmel.de'; - return `${baseUrl}/moderation`; + const host = process.env.INTERNAL_HOST || 'internal.hobbyhimmel.de'; + const isProduction = process.env.NODE_ENV === 'production'; + const protocol = isProduction ? 'https' : 'http'; + const port = isProduction ? '' : ':3000'; + return `${protocol}://${host}${port}/moderation`; } } diff --git a/backend/tests/api/telegram-upload.test.js b/backend/tests/api/telegram-upload.test.js new file mode 100644 index 0000000..015612d --- /dev/null +++ b/backend/tests/api/telegram-upload.test.js @@ -0,0 +1,183 @@ +/** + * Integration Tests für Telegram Upload-Benachrichtigungen + * + * Phase 3: Upload-Benachrichtigungen + * + * Diese Tests prüfen die Integration zwischen Upload-Route und Telegram-Service + */ + +const path = require('path'); +const fs = require('fs'); +const { getRequest } = require('../testServer'); + +describe('Telegram Upload Notifications (Integration)', () => { + let TelegramNotificationService; + let sendUploadNotificationSpy; + + beforeAll(() => { + // Spy auf TelegramNotificationService + TelegramNotificationService = require('../../src/services/TelegramNotificationService'); + }); + + beforeEach(() => { + // Spy auf sendUploadNotification erstellen + sendUploadNotificationSpy = jest.spyOn(TelegramNotificationService.prototype, 'sendUploadNotification') + .mockResolvedValue({ message_id: 42 }); + + // isAvailable() immer true zurückgeben für Tests + jest.spyOn(TelegramNotificationService.prototype, 'isAvailable') + .mockReturnValue(true); + }); + + afterEach(() => { + // Restore alle Spys + jest.restoreAllMocks(); + }); + + describe('POST /api/upload/batch', () => { + const testImagePath = path.join(__dirname, '../utils/test-image.jpg'); + + // Erstelle Test-Bild falls nicht vorhanden + beforeAll(() => { + if (!fs.existsSync(testImagePath)) { + // Erstelle 1x1 px JPEG + const buffer = Buffer.from([ + 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, + 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, + 0x00, 0x08, 0x06, 0x06, 0x07, 0x06, 0x05, 0x08, + 0x07, 0x07, 0x07, 0x09, 0x09, 0x08, 0x0A, 0x0C, + 0x14, 0x0D, 0x0C, 0x0B, 0x0B, 0x0C, 0x19, 0x12, + 0x13, 0x0F, 0x14, 0x1D, 0x1A, 0x1F, 0x1E, 0x1D, + 0x1A, 0x1C, 0x1C, 0x20, 0x24, 0x2E, 0x27, 0x20, + 0x22, 0x2C, 0x23, 0x1C, 0x1C, 0x28, 0x37, 0x29, + 0x2C, 0x30, 0x31, 0x34, 0x34, 0x34, 0x1F, 0x27, + 0x39, 0x3D, 0x38, 0x32, 0x3C, 0x2E, 0x33, 0x34, + 0x32, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x01, + 0x00, 0x01, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, + 0x00, 0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x03, 0xFF, 0xC4, 0x00, 0x14, + 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, + 0x00, 0x00, 0x3F, 0x00, 0x37, 0xFF, 0xD9 + ]); + fs.writeFileSync(testImagePath, buffer); + } + }); + + it('sollte Telegram-Benachrichtigung bei erfolgreichem Upload senden', async () => { + const response = await getRequest() + .post('/api/upload/batch') + .field('year', '2024') + .field('title', 'Test Upload') + .field('name', 'Test User') + .field('consents', JSON.stringify({ + workshopConsent: true, + socialMediaConsents: ['instagram', 'tiktok'] + })) + .attach('images', testImagePath); + + // Upload sollte erfolgreich sein + expect(response.status).toBe(200); + expect(response.body.message).toBe('Batch upload successful'); + + // Warte kurz auf async Telegram-Call + await new Promise(resolve => setTimeout(resolve, 150)); + + // Telegram-Service sollte aufgerufen worden sein + expect(sendUploadNotificationSpy).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'Test User', + year: 2024, + title: 'Test Upload', + imageCount: 1, + workshopConsent: true, + socialMediaConsents: ['instagram', 'tiktok'] + }) + ); + }); + + it('sollte Upload nicht fehlschlagen wenn Telegram-Service nicht verfügbar', async () => { + // Restore mock und setze isAvailable auf false + jest.restoreAllMocks(); + jest.spyOn(TelegramNotificationService.prototype, 'isAvailable') + .mockReturnValue(false); + sendUploadNotificationSpy = jest.spyOn(TelegramNotificationService.prototype, 'sendUploadNotification'); + + const response = await getRequest() + .post('/api/upload/batch') + .field('year', '2024') + .field('title', 'Test Upload') + .field('name', 'Test User') + .field('consents', JSON.stringify({ + workshopConsent: false, + socialMediaConsents: [] + })) + .attach('images', testImagePath); + + // Upload sollte trotzdem erfolgreich sein + expect(response.status).toBe(200); + expect(response.body.message).toBe('Batch upload successful'); + + // Telegram sollte nicht aufgerufen worden sein + expect(sendUploadNotificationSpy).not.toHaveBeenCalled(); + }); + + it('sollte Upload nicht fehlschlagen wenn Telegram-Benachrichtigung fehlschlägt', async () => { + sendUploadNotificationSpy.mockRejectedValueOnce( + new Error('Telegram API Error') + ); + + const response = await getRequest() + .post('/api/upload/batch') + .field('year', '2024') + .field('title', 'Test Upload') + .field('name', 'Test User') + .field('consents', JSON.stringify({ + workshopConsent: true, + socialMediaConsents: [] + })) + .attach('images', testImagePath); + + // Upload sollte trotzdem erfolgreich sein + expect(response.status).toBe(200); + expect(response.body.message).toBe('Batch upload successful'); + + // Warte auf async error handling + await new Promise(resolve => setTimeout(resolve, 150)); + + // Telegram wurde versucht aufzurufen + expect(sendUploadNotificationSpy).toHaveBeenCalled(); + }); + + it('sollte korrekte Daten an Telegram-Service übergeben', async () => { + const response = await getRequest() + .post('/api/upload/batch') + .field('year', '2025') + .field('title', 'Schweißkurs November') + .field('name', 'Max Mustermann') + .field('consents', JSON.stringify({ + workshopConsent: true, + socialMediaConsents: ['facebook', 'instagram'] + })) + .attach('images', testImagePath) + .attach('images', testImagePath); + + expect(response.status).toBe(200); + + await new Promise(resolve => setTimeout(resolve, 150)); + + expect(sendUploadNotificationSpy).toHaveBeenCalledWith({ + name: 'Max Mustermann', + year: 2025, + title: 'Schweißkurs November', + imageCount: 2, + workshopConsent: true, + socialMediaConsents: ['facebook', 'instagram'], + token: expect.any(String) + }); + }); + }); +}); diff --git a/backend/tests/utils/test-image.jpg b/backend/tests/utils/test-image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0e00faa30109c91cfe60902be2f3b299399a3c56 GIT binary patch literal 159 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<)Zf(-wU dFo=LmMghzqQ2}&OhX1!1I6zLZXE6VN695V27$g7y literal 0 HcmV?d00001