feat(cardav): implement POST /accounts endpoint
Add account creation route with validation. Delegates to CardDAVSync.addAccount() which creates account and discovers addressbooks. Returns 201 with account + addressbooks array. Add _mockTestConnection() helper for testing CardDAV routes without real server connections. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,8 +7,10 @@
|
|||||||
import { createLogger } from '../logger.js';
|
import { createLogger } from '../logger.js';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
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';
|
||||||
|
|
||||||
const log = createLogger('CardDAV');
|
const log = createLogger('CardDAV');
|
||||||
|
const MAX_URL = 500;
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,4 +28,33 @@ router.get('/accounts', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/v1/contacts/cardav/accounts
|
||||||
|
* Neuen CardDAV Account erstellen und Addressbooks discovern.
|
||||||
|
* Body: { name, cardavUrl, username, password }
|
||||||
|
* Response: { data: { account, addressbooks } }
|
||||||
|
*/
|
||||||
|
router.post('/accounts', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const vName = str(req.body.name, 'Name', { max: MAX_TITLE });
|
||||||
|
const vUrl = str(req.body.cardavUrl, 'CardDAV URL', { max: MAX_URL });
|
||||||
|
const vUsername = str(req.body.username, 'Username', { max: MAX_TITLE });
|
||||||
|
const vPassword = str(req.body.password, 'Password', { max: MAX_TITLE });
|
||||||
|
const errors = collectErrors([vName, vUrl, vUsername, vPassword]);
|
||||||
|
if (errors.length) return res.status(400).json({ error: errors.join(' '), code: 400 });
|
||||||
|
|
||||||
|
const result = await CardDAVSync.addAccount(
|
||||||
|
vName.value,
|
||||||
|
vUrl.value,
|
||||||
|
vUsername.value,
|
||||||
|
vPassword.value
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(201).json({ data: result });
|
||||||
|
} catch (err) {
|
||||||
|
log.error('Error adding CardDAV account:', err);
|
||||||
|
res.status(500).json({ error: 'Interner Fehler', code: 500 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -212,6 +212,11 @@ function parseBirthday(value) {
|
|||||||
* @returns {Promise<Object>} { ok: true, addressbooks: [...] }
|
* @returns {Promise<Object>} { ok: true, addressbooks: [...] }
|
||||||
*/
|
*/
|
||||||
async function testConnection(cardavUrl, username, password) {
|
async function testConnection(cardavUrl, username, password) {
|
||||||
|
// Use mock if set (for testing)
|
||||||
|
if (_testConnectionMock) {
|
||||||
|
return _testConnectionMock(cardavUrl, username, password);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { createDAVClient } = await import('tsdav');
|
const { createDAVClient } = await import('tsdav');
|
||||||
const client = await createDAVClient({
|
const client = await createDAVClient({
|
||||||
@@ -288,7 +293,16 @@ async function addAccount(name, cardavUrl, username, password) {
|
|||||||
|
|
||||||
log.info(`Added CardDAV account "${name}" with ${addressbooks.length} addressbooks.`);
|
log.info(`Added CardDAV account "${name}" with ${addressbooks.length} addressbooks.`);
|
||||||
|
|
||||||
return { accountId, addressbooks: addressbookData };
|
const account = {
|
||||||
|
id: accountId,
|
||||||
|
name,
|
||||||
|
cardavUrl,
|
||||||
|
username,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
lastSync: null
|
||||||
|
};
|
||||||
|
|
||||||
|
return { account, addressbooks: addressbookData };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('Failed to add account:', err.message);
|
log.error('Failed to add account:', err.message);
|
||||||
throw err;
|
throw err;
|
||||||
@@ -869,4 +883,19 @@ export {
|
|||||||
|
|
||||||
// Helpers (exported for testing)
|
// Helpers (exported for testing)
|
||||||
parseVCard,
|
parseVCard,
|
||||||
|
_mockTestConnection,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// Test Mocking Support
|
||||||
|
// --------------------------------------------------------
|
||||||
|
|
||||||
|
let _testConnectionMock = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ONLY FOR TESTING: Mock testConnection for unit tests
|
||||||
|
* @param {Function|null} mockFn - Mock function or null to reset
|
||||||
|
*/
|
||||||
|
function _mockTestConnection(mockFn) {
|
||||||
|
_testConnectionMock = mockFn;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1513,12 +1513,26 @@ describe('CardDAV API Routes', () => {
|
|||||||
// Override db.get() to use our test database
|
// Override db.get() to use our test database
|
||||||
const dbModule = await import('./server/db.js');
|
const dbModule = await import('./server/db.js');
|
||||||
dbModule._setTestDatabase(apiTestDb);
|
dbModule._setTestDatabase(apiTestDb);
|
||||||
|
|
||||||
|
// Mock testConnection for API route tests
|
||||||
|
const cardavSync = await import('./server/services/cardav-sync.js');
|
||||||
|
cardavSync._mockTestConnection(async () => ({
|
||||||
|
ok: true,
|
||||||
|
addressbooks: [
|
||||||
|
{ url: 'https://example.com/carddav/addressbook1', displayName: 'Contacts' },
|
||||||
|
{ url: 'https://example.com/carddav/addressbook2', displayName: 'Work' }
|
||||||
|
]
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
after(async () => {
|
after(async () => {
|
||||||
// Restore original database
|
// Restore original database
|
||||||
const dbModule = await import('./server/db.js');
|
const dbModule = await import('./server/db.js');
|
||||||
dbModule._resetTestDatabase();
|
dbModule._resetTestDatabase();
|
||||||
|
|
||||||
|
// Reset testConnection mock
|
||||||
|
const cardavSync = await import('./server/services/cardav-sync.js');
|
||||||
|
cardavSync._mockTestConnection(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Account Management', () => {
|
describe('Account Management', () => {
|
||||||
@@ -1576,5 +1590,66 @@ describe('CardDAV API Routes', () => {
|
|||||||
assert.strictEqual(account.createdAt, '2026-05-01T10:00:00Z');
|
assert.strictEqual(account.createdAt, '2026-05-01T10:00:00Z');
|
||||||
assert.ok(!account.password, 'Password should not be exposed');
|
assert.ok(!account.password, 'Password should not be exposed');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('POST /accounts - should create account and discover addressbooks', async () => {
|
||||||
|
const cardavRouter = await import('./server/routes/cardav.js');
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
params: {},
|
||||||
|
query: {},
|
||||||
|
body: {
|
||||||
|
name: 'Test Account',
|
||||||
|
cardavUrl: 'https://example.com/carddav',
|
||||||
|
username: 'testuser',
|
||||||
|
password: 'testpass'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const res = {
|
||||||
|
statusCode: 200,
|
||||||
|
status(code) { this.statusCode = code; return this; },
|
||||||
|
json(data) { this.data = data; return this; },
|
||||||
|
};
|
||||||
|
|
||||||
|
const postHandler = cardavRouter.default.stack.find(
|
||||||
|
layer => layer.route?.path === '/accounts' && layer.route.methods.post
|
||||||
|
)?.route?.stack[0]?.handle;
|
||||||
|
|
||||||
|
assert.ok(postHandler, 'POST /accounts handler should exist');
|
||||||
|
await postHandler(req, res);
|
||||||
|
|
||||||
|
assert.strictEqual(res.statusCode, 201);
|
||||||
|
assert.ok(res.data.data.account);
|
||||||
|
assert.ok(res.data.data.account.id);
|
||||||
|
assert.strictEqual(res.data.data.account.name, 'Test Account');
|
||||||
|
assert.ok(Array.isArray(res.data.data.addressbooks));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('POST /accounts - should return 400 for missing name', async () => {
|
||||||
|
const cardavRouter = await import('./server/routes/cardav.js');
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
params: {},
|
||||||
|
query: {},
|
||||||
|
body: {
|
||||||
|
cardavUrl: 'https://example.com/carddav',
|
||||||
|
username: 'testuser',
|
||||||
|
password: 'testpass'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const res = {
|
||||||
|
statusCode: 200,
|
||||||
|
status(code) { this.statusCode = code; return this; },
|
||||||
|
json(data) { this.data = data; return this; },
|
||||||
|
};
|
||||||
|
|
||||||
|
const postHandler = cardavRouter.default.stack.find(
|
||||||
|
layer => layer.route?.path === '/accounts' && layer.route.methods.post
|
||||||
|
)?.route?.stack[0]?.handle;
|
||||||
|
|
||||||
|
await postHandler(req, res);
|
||||||
|
|
||||||
|
assert.strictEqual(res.statusCode, 400);
|
||||||
|
assert.ok(res.data.error.includes('Name'));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user