diff --git a/PROGRESS.md b/PROGRESS.md index c8f7aab..4ffeaaa 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -1,6 +1,6 @@ # CardDAV API Routes Implementation - Fortschritt -**Stand:** 2026-05-04, nach Task 8 von 15 (Session pausiert bei ~88k tokens, frische Session startet bei Task 9) +**Stand:** 2026-05-04, nach Task 9 von 15 **Plan:** `docs/superpowers/plans/2026-05-04-cardav-api-routes.md` ## Abgeschlossene Tasks @@ -102,23 +102,17 @@ - Exportiert: bool in export statement - Keine Tests (wird in Task 9 verwendet) -## Offene Tasks (9-15) +### ✅ Task 9: PUT /addressbooks/:id - Toggle Addressbook +**Commit:** (pending) -### 🔄 Task 5: POST /accounts/:id/test -- Test Connection Endpoint (nutzt existierende testConnection Funktion) +- Implementiert: PUT /addressbooks/:id Route in server/routes/cardav.js +- Validierung: ID muss positive Ganzzahl sein, enabled muss boolean sein +- Delegiert an: `CardDAVSync.toggleAddressbook(id, enabled)` +- Response: 200 mit `{ updated: true, enabled: boolean }` +- Tests: 2 Tests (success case mit toggle, validation failure für invalid enabled) +- bool Validator verwendet -### 🔄 Task 6: GET /accounts/:id/addressbooks -- Liste Addressbooks für Account - -### 🔄 Task 7: POST /accounts/:id/addressbooks/refresh -- Re-discover Addressbooks - -### 🔄 Task 8: bool Validator -- `bool()` Validator zu `server/middleware/validate.js` hinzufügen -- Export ergänzen - -### 🔄 Task 9: PUT /addressbooks/:id -- Toggle Addressbook enabled/disabled +## Offene Tasks (10-15) ### 🔄 Task 10: POST /accounts/:id/sync - Sync Account (alle enabled addressbooks) @@ -201,9 +195,9 @@ c078a48 feat(cardav): implement POST /accounts/:id/addressbooks/refresh endpoint ## Test-Status -- **Gesamt:** 97 Tests, alle bestehen -- **Suites:** 14 Suites -- **CardDAV API Routes Suite:** 10 Tests +- **Gesamt:** 99 Tests, alle bestehen +- **Suites:** 15 Suites +- **CardDAV API Routes Suite:** 12 Tests - Account Management (6 Tests): - GET /accounts (empty) - GET /accounts (populated with shape) @@ -216,6 +210,9 @@ c078a48 feat(cardav): implement POST /accounts/:id/addressbooks/refresh endpoint - GET /accounts/:id/addressbooks (success with addressbooks) - GET /accounts/:id/addressbooks (empty array) - POST /accounts/:id/addressbooks/refresh (success) + - Addressbook Management (2 Tests): + - PUT /addressbooks/:id (toggle enabled/disabled) + - PUT /addressbooks/:id (validation failure) ## Branch & Remote @@ -235,7 +232,7 @@ c078a48 feat(cardav): implement POST /accounts/:id/addressbooks/refresh endpoint #6. [completed] Task 6: GET /accounts/:id/addressbooks - List Addressbooks #7. [completed] Task 7: POST /accounts/:id/addressbooks/refresh - Refresh Addressbooks #8. [completed] Task 8: Add bool validator to validate.js -#9. [pending] Task 9: PUT /addressbooks/:id - Toggle Addressbook +#9. [completed] Task 9: PUT /addressbooks/:id - Toggle Addressbook #10. [pending] Task 10: POST /accounts/:id/sync - Sync Account #11. [pending] Task 11: GET /contacts/:id - With Multi-Values #12. [pending] Task 12: POST /contacts - Create With Multi-Values diff --git a/server/routes/cardav.js b/server/routes/cardav.js index 09e6dd4..b44606f 100644 --- a/server/routes/cardav.js +++ b/server/routes/cardav.js @@ -8,7 +8,7 @@ import { createLogger } from '../logger.js'; import express from 'express'; import * as db from '../db.js'; import * as CardDAVSync from '../services/cardav-sync.js'; -import { str, collectErrors, MAX_TITLE } from '../middleware/validate.js'; +import { str, bool, collectErrors, MAX_TITLE } from '../middleware/validate.js'; const log = createLogger('CardDAV'); const MAX_URL = 500; @@ -156,4 +156,28 @@ router.post('/accounts/:id/addressbooks/refresh', async (req, res) => { } }); +/** + * PUT /api/v1/contacts/cardav/addressbooks/:id + * Toggle Addressbook enabled/disabled. + * Body: { enabled: boolean } + * Response: { data: { updated: true, enabled: boolean } } + */ +router.put('/addressbooks/:id', async (req, res) => { + try { + const id = parseInt(req.params.id, 10); + if (!id || id < 1) return res.status(400).json({ error: 'Invalid ID', code: 400 }); + + const vEnabled = bool(req.body.enabled, 'enabled'); + const errors = collectErrors([vEnabled]); + if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 }); + + CardDAVSync.toggleAddressbook(id, vEnabled.value); + + res.json({ data: { updated: true, enabled: vEnabled.value } }); + } catch (err) { + log.error('Error toggling addressbook:', err); + res.status(500).json({ error: 'Interner Fehler', code: 500 }); + } +}); + export default router; diff --git a/test-carddav.js b/test-carddav.js index c1a9a02..5f1bd26 100644 --- a/test-carddav.js +++ b/test-carddav.js @@ -1895,4 +1895,105 @@ describe('CardDAV API Routes', () => { assert.ok(Array.isArray(res.data.data)); }); }); + + describe('Addressbook Management', () => { + it('PUT /addressbooks/:id - should toggle addressbook enabled/disabled', async () => { + const cardavRouter = await import('./server/routes/cardav.js'); + + // First create an account (which creates addressbooks) + const createReq = { + params: {}, + query: {}, + body: { + name: 'Toggle Test Account', + cardavUrl: 'https://example.com/carddav-toggle', + username: 'testuser-toggle', + password: 'testpass' + } + }; + const createRes = { + statusCode: 200, + status(code) { this.statusCode = code; return this; }, + json(data) { this.data = data; return this; }, + }; + + const postAccountHandler = cardavRouter.default.stack.find( + layer => layer.route?.path === '/accounts' && layer.route.methods.post + )?.route?.stack[0]?.handle; + + await postAccountHandler(createReq, createRes); + const accountId = createRes.data.data.account.id; + + // Get addressbooks with IDs via GET /accounts/:id/addressbooks + const getReq = { + params: { id: String(accountId) }, + query: {}, + body: {} + }; + const getRes = { + statusCode: 200, + status(code) { this.statusCode = code; return this; }, + json(data) { this.data = data; return this; }, + }; + + const getAddressbooksHandler = cardavRouter.default.stack.find( + layer => layer.route?.path === '/accounts/:id/addressbooks' && layer.route.methods.get + )?.route?.stack[0]?.handle; + + await getAddressbooksHandler(getReq, getRes); + + const addressbooks = getRes.data.data; + assert.ok(addressbooks.length > 0, 'Should have at least one addressbook'); + + const addressbookId = addressbooks[0].id; + const initialEnabled = addressbooks[0].enabled; + + // Toggle the addressbook + const req = { + params: { id: String(addressbookId) }, + query: {}, + body: { enabled: !initialEnabled } + }; + const res = { + statusCode: 200, + status(code) { this.statusCode = code; return this; }, + json(data) { this.data = data; return this; }, + }; + + const putHandler = cardavRouter.default.stack.find( + layer => layer.route?.path === '/addressbooks/:id' && layer.route.methods.put + )?.route?.stack[0]?.handle; + + assert.ok(putHandler, 'PUT /addressbooks/:id handler should exist'); + await putHandler(req, res); + + assert.strictEqual(res.statusCode, 200); + assert.strictEqual(res.data.data.updated, true); + assert.strictEqual(res.data.data.enabled, !initialEnabled); + }); + + it('PUT /addressbooks/:id - should return 400 for invalid enabled value', async () => { + const cardavRouter = await import('./server/routes/cardav.js'); + + const req = { + params: { id: '1' }, + query: {}, + body: { enabled: 'invalid' } + }; + const res = { + statusCode: 200, + status(code) { this.statusCode = code; return this; }, + json(data) { this.data = data; return this; }, + }; + + const putHandler = cardavRouter.default.stack.find( + layer => layer.route?.path === '/addressbooks/:id' && layer.route.methods.put + )?.route?.stack[0]?.handle; + + await putHandler(req, res); + + assert.strictEqual(res.statusCode, 400); + assert.ok(res.data.error.includes('enabled')); + }); + }); });