feat(cardav): implement PUT /addressbooks/:id endpoint

Adds route to toggle addressbook enabled/disabled state with bool validation.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Ulas Kalayci
2026-05-04 17:47:26 +02:00
parent 749e6ac79b
commit 9ec7fda6b0
3 changed files with 143 additions and 21 deletions
+17 -20
View File
@@ -1,6 +1,6 @@
# CardDAV API Routes Implementation - Fortschritt # 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` **Plan:** `docs/superpowers/plans/2026-05-04-cardav-api-routes.md`
## Abgeschlossene Tasks ## Abgeschlossene Tasks
@@ -102,23 +102,17 @@
- Exportiert: bool in export statement - Exportiert: bool in export statement
- Keine Tests (wird in Task 9 verwendet) - 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 - Implementiert: PUT /addressbooks/:id Route in server/routes/cardav.js
- Test Connection Endpoint (nutzt existierende testConnection Funktion) - 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 ## Offene Tasks (10-15)
- 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
### 🔄 Task 10: POST /accounts/:id/sync ### 🔄 Task 10: POST /accounts/:id/sync
- Sync Account (alle enabled addressbooks) - Sync Account (alle enabled addressbooks)
@@ -201,9 +195,9 @@ c078a48 feat(cardav): implement POST /accounts/:id/addressbooks/refresh endpoint
## Test-Status ## Test-Status
- **Gesamt:** 97 Tests, alle bestehen - **Gesamt:** 99 Tests, alle bestehen
- **Suites:** 14 Suites - **Suites:** 15 Suites
- **CardDAV API Routes Suite:** 10 Tests - **CardDAV API Routes Suite:** 12 Tests
- Account Management (6 Tests): - Account Management (6 Tests):
- GET /accounts (empty) - GET /accounts (empty)
- GET /accounts (populated with shape) - 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 (success with addressbooks)
- GET /accounts/:id/addressbooks (empty array) - GET /accounts/:id/addressbooks (empty array)
- POST /accounts/:id/addressbooks/refresh (success) - POST /accounts/:id/addressbooks/refresh (success)
- Addressbook Management (2 Tests):
- PUT /addressbooks/:id (toggle enabled/disabled)
- PUT /addressbooks/:id (validation failure)
## Branch & Remote ## 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 #6. [completed] Task 6: GET /accounts/:id/addressbooks - List Addressbooks
#7. [completed] Task 7: POST /accounts/:id/addressbooks/refresh - Refresh Addressbooks #7. [completed] Task 7: POST /accounts/:id/addressbooks/refresh - Refresh Addressbooks
#8. [completed] Task 8: Add bool validator to validate.js #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 #10. [pending] Task 10: POST /accounts/:id/sync - Sync Account
#11. [pending] Task 11: GET /contacts/:id - With Multi-Values #11. [pending] Task 11: GET /contacts/:id - With Multi-Values
#12. [pending] Task 12: POST /contacts - Create With Multi-Values #12. [pending] Task 12: POST /contacts - Create With Multi-Values
+25 -1
View File
@@ -8,7 +8,7 @@ import { createLogger } from '../logger.js';
import express from 'express'; import express from 'express';
import * as db from '../db.js'; import * as db from '../db.js';
import * as CardDAVSync from '../services/cardav-sync.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 log = createLogger('CardDAV');
const MAX_URL = 500; 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; export default router;
+101
View File
@@ -1895,4 +1895,105 @@ describe('CardDAV API Routes', () => {
assert.ok(Array.isArray(res.data.data)); 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'));
});
});
}); });