joplock/tests/adminService.test.js

164 lines
5.9 KiB
JavaScript

'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const http = require('node:http');
const { createAdminService, isStrongPassword } = require('../app/adminService');
test('isStrongPassword rejects short password', () => {
assert.equal(isStrongPassword('Short1!'), false);
});
test('isStrongPassword rejects password with only one category', () => {
assert.equal(isStrongPassword('aaaaaaaaaaaaa'), false);
});
test('isStrongPassword rejects password with two categories', () => {
assert.equal(isStrongPassword('aaaaAAAAAAAAA'), false);
});
test('isStrongPassword accepts password with 3+ categories and length>=12', () => {
assert.equal(isStrongPassword('aaAAA123456!'), true);
});
test('isStrongPassword accepts lowercase+digits+special', () => {
assert.equal(isStrongPassword('aabbcc123!!!'), true);
});
test('isStrongPassword rejects null/undefined', () => {
assert.equal(isStrongPassword(null), false);
assert.equal(isStrongPassword(undefined), false);
assert.equal(isStrongPassword(''), false);
});
test('isStrongPassword requires at least 12 chars', () => {
assert.equal(isStrongPassword('aA1!aA1!aA1'), false); // 11 chars
assert.equal(isStrongPassword('aA1!aA1!aA1!'), true); // 12 chars
});
test('createUser clears must_set_password with integer 0', async () => {
const requests = [];
const server = http.createServer((req, res) => {
let body = '';
req.on('data', chunk => { body += chunk; });
req.on('end', () => {
requests.push({ method: req.method, url: req.url, body: body ? JSON.parse(body) : null });
if (req.method === 'POST' && req.url === '/api/sessions') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ id: 'admin-token' }));
return;
}
if (req.method === 'POST' && req.url === '/api/users') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ id: 'user-1' }));
return;
}
if (req.method === 'PATCH' && req.url === '/api/users/user-1') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ id: 'user-1' }));
return;
}
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'not found' }));
});
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
const port = server.address().port;
const database = { query: async () => ({ rows: [] }) };
const service = createAdminService({
database,
joplinServerOrigin: `http://127.0.0.1:${port}`,
joplinServerPublicUrl: `http://127.0.0.1:${port}`,
adminEmail: 'admin@example.com',
adminPassword: 'AdminPass123!',
});
try {
await service.createUser('new@example.com', 'New User', 'UserPass123!');
} finally {
await new Promise(resolve => server.close(resolve));
}
const patchReq = requests.find(r => r.method === 'PATCH' && r.url === '/api/users/user-1');
assert.ok(patchReq);
assert.equal(patchReq.body.must_set_password, 0);
});
test('createUser fails when password update fails', async () => {
const server = http.createServer((req, res) => {
if (req.method === 'POST' && req.url === '/api/sessions') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ id: 'admin-token' }));
return;
}
if (req.method === 'POST' && req.url === '/api/users') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ id: 'user-1' }));
return;
}
if (req.method === 'PATCH' && req.url === '/api/users/user-1') {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'bad password update' }));
return;
}
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'not found' }));
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
const port = server.address().port;
const database = { query: async () => ({ rows: [] }) };
const service = createAdminService({
database,
joplinServerOrigin: `http://127.0.0.1:${port}`,
joplinServerPublicUrl: `http://127.0.0.1:${port}`,
adminEmail: 'admin@example.com',
adminPassword: 'AdminPass123!',
});
try {
await assert.rejects(() => service.createUser('new@example.com', 'New User', 'UserPass123!'), /bad password update/);
} finally {
await new Promise(resolve => server.close(resolve));
}
});
test('ensureAdminUser retries until users table is ready', async () => {
let attempts = 0;
const database = {
query: async (sql) => {
if (sql.includes('SELECT id')) {
attempts++;
if (attempts < 3) throw new Error('relation "users" does not exist');
return { rows: [] }; // table ready, no existing user
}
if (sql.includes('INSERT INTO users')) return { rows: [] };
return { rows: [] };
},
};
const service = createAdminService({
database,
joplinServerOrigin: 'http://127.0.0.1:19999',
joplinServerPublicUrl: 'http://127.0.0.1:19999',
adminEmail: 'admin@example.com',
adminPassword: 'AdminPass123!',
});
await service.ensureAdminUser({ retryMs: 10, timeoutMs: 5000 });
assert.equal(attempts, 3, 'should have retried until table was ready');
});
test('ensureAdminUser gives up after timeout', async () => {
const messages = [];
const origStderr = process.stderr.write.bind(process.stderr);
process.stderr.write = (msg) => { messages.push(msg); return true; };
try {
const database = { query: async () => { throw new Error('relation "users" does not exist'); } };
const service = createAdminService({
database,
joplinServerOrigin: 'http://127.0.0.1:19999',
joplinServerPublicUrl: 'http://127.0.0.1:19999',
adminEmail: 'admin@example.com',
adminPassword: 'AdminPass123!',
});
await service.ensureAdminUser({ retryMs: 20, timeoutMs: 50 });
assert.ok(messages.some(m => m.includes('WARNING') && m.includes('admin bootstrap')), 'should log warning after timeout');
} finally {
process.stderr.write = origStderr;
}
});