LDAP: a sync interval in milliseconds no longer stops WeKan.

"[uncaughtException] WeKan is stopping: TypeError: string.toLowerCase is
not a function at later.parse.text ... at Object.schedule
(wekan-ldap/server/sync.js)", in a snap restart loop. The docs give
LDAP_BACKGROUND_SYNC_INTERVAL in milliseconds, settings_get() turns a
numeric value into a Number, and the schedule handed it straight to
later's text parser, which threw from a timer. Text later could not read
did not throw, but made a schedule that never ran - as a quoted
'every 1 mins' from a compose file did, the second report in #2354.

packages/wekan-ldap/server/syncSchedule.js reads the setting and never
throws: milliseconds (a Number or digits) become that interval, later.js
text is read as before, one pair of surrounding quotes is removed, unset
keeps the original default (recur().on(0).minute()), and anything
unreadable logs why and uses the default. The docs, the snap description
and the compose files say both forms work, instead of "leave this unset
so it does not crash".

Tests: tests/ldapSyncSchedule.test.cjs reproduces the crash with the real
later.js from the build, schedules 3600000 ms an hour apart and 10000 ms
ten seconds apart, reads quoted text, keeps the default, never throws on
any value a setting can hold, and fails if any server file hands a setting
to parser.text directly.

Thanks to B2RN and xet7 !

Fixes #2354.
This commit is contained in:
Lauri Ojansivu committed 2026-10-15 03:17:49 +03:00
1 parent fae5886319
commit 7dfd942ff6
13 files changed
+258 -57

No files matched your search

+4 -4
View File
@@ -684,12 +684,12 @@ services:
# If the sync of the users should be done in the background
#- LDAP_BACKGROUND_SYNC=false
#
# LDAP_BACKGROUND_SYNC_INTERVAL : At which interval does the background task sync in milliseconds
# The format must be as specified in:
# LDAP_BACKGROUND_SYNC_INTERVAL : How often the background task syncs: milliseconds (3600000 = hourly) or text
# Text is later.js text, quotes allowed, as specified in:
# https://bunkat.github.io/later/parsers.html#text
#- LDAP_BACKGROUND_SYNC_INTERVAL=every 1 hours
# At which interval does the background task sync in milliseconds.
# Leave this unset, so it uses default, and does not crash.
# Unset or empty uses the default (hourly). A value it cannot read is logged
# and the default is used; it no longer stops WeKan (#2354).
# https://github.com/wekan/wekan/issues/2354#issuecomment-515305722
- LDAP_BACKGROUND_SYNC_INTERVAL=''
#
+4 -4
View File
@@ -686,12 +686,12 @@ services:
# If the sync of the users should be done in the background
#- LDAP_BACKGROUND_SYNC=false
#
# LDAP_BACKGROUND_SYNC_INTERVAL : At which interval does the background task sync in milliseconds
# The format must be as specified in:
# LDAP_BACKGROUND_SYNC_INTERVAL : How often the background task syncs: milliseconds (3600000 = hourly) or text
# Text is later.js text, quotes allowed, as specified in:
# https://bunkat.github.io/later/parsers.html#text
#- LDAP_BACKGROUND_SYNC_INTERVAL=every 1 hours
# At which interval does the background task sync in milliseconds.
# Leave this unset, so it uses default, and does not crash.
# Unset or empty uses the default (hourly). A value it cannot read is logged
# and the default is used; it no longer stops WeKan (#2354).
# https://github.com/wekan/wekan/issues/2354#issuecomment-515305722
- LDAP_BACKGROUND_SYNC_INTERVAL=''
#
+4 -4
View File
@@ -673,12 +673,12 @@ services:
# If the sync of the users should be done in the background
#- LDAP_BACKGROUND_SYNC=false
#
# LDAP_BACKGROUND_SYNC_INTERVAL : At which interval does the background task sync in milliseconds
# The format must be as specified in:
# LDAP_BACKGROUND_SYNC_INTERVAL : How often the background task syncs: milliseconds (3600000 = hourly) or text
# Text is later.js text, quotes allowed, as specified in:
# https://bunkat.github.io/later/parsers.html#text
#- LDAP_BACKGROUND_SYNC_INTERVAL=every 1 hours
# At which interval does the background task sync in milliseconds.
# Leave this unset, so it uses default, and does not crash.
# Unset or empty uses the default (hourly). A value it cannot read is logged
# and the default is used; it no longer stops WeKan (#2354).
# https://github.com/wekan/wekan/issues/2354#issuecomment-515305722
- LDAP_BACKGROUND_SYNC_INTERVAL=''
#
+4 -4
View File
@@ -690,12 +690,12 @@ services:
# If the sync of the users should be done in the background
#- LDAP_BACKGROUND_SYNC=false
#
# LDAP_BACKGROUND_SYNC_INTERVAL : At which interval does the background task sync in milliseconds
# The format must be as specified in:
# LDAP_BACKGROUND_SYNC_INTERVAL : How often the background task syncs: milliseconds (3600000 = hourly) or text
# Text is later.js text, quotes allowed, as specified in:
# https://bunkat.github.io/later/parsers.html#text
#- LDAP_BACKGROUND_SYNC_INTERVAL=every 1 hours
# At which interval does the background task sync in milliseconds.
# Leave this unset, so it uses default, and does not crash.
# Unset or empty uses the default (hourly). A value it cannot read is logged
# and the default is used; it no longer stops WeKan (#2354).
# https://github.com/wekan/wekan/issues/2354#issuecomment-515305722
- LDAP_BACKGROUND_SYNC_INTERVAL=''
#
+4 -4
View File
@@ -593,12 +593,12 @@ services:
# If the sync of the users should be done in the background
#- LDAP_BACKGROUND_SYNC=false
#
# LDAP_BACKGROUND_SYNC_INTERVAL : At which interval does the background task sync in milliseconds
# The format must be as specified in:
# LDAP_BACKGROUND_SYNC_INTERVAL : How often the background task syncs: milliseconds (3600000 = hourly) or text
# Text is later.js text, quotes allowed, as specified in:
# https://bunkat.github.io/later/parsers.html#text
#- LDAP_BACKGROUND_SYNC_INTERVAL=every 1 hours
# At which interval does the background task sync in milliseconds.
# Leave this unset, so it uses default, and does not crash.
# Unset or empty uses the default (hourly). A value it cannot read is logged
# and the default is used; it no longer stops WeKan (#2354).
# https://github.com/wekan/wekan/issues/2354#issuecomment-515305722
- LDAP_BACKGROUND_SYNC_INTERVAL=''
#
+4 -4
View File
@@ -827,12 +827,12 @@ services:
# If the sync of the users should be done in the background
#- LDAP_BACKGROUND_SYNC=false
#
# LDAP_BACKGROUND_SYNC_INTERVAL : At which interval does the background task sync in milliseconds
# The format must be as specified in:
# LDAP_BACKGROUND_SYNC_INTERVAL : How often the background task syncs: milliseconds (3600000 = hourly) or text
# Text is later.js text, quotes allowed, as specified in:
# https://bunkat.github.io/later/parsers.html#text
#- LDAP_BACKGROUND_SYNC_INTERVAL=every 1 hours
# At which interval does the background task sync in milliseconds.
# Leave this unset, so it uses default, and does not crash.
# Unset or empty uses the default (hourly). A value it cannot read is logged
# and the default is used; it no longer stops WeKan (#2354).
# https://github.com/wekan/wekan/issues/2354#issuecomment-515305722
- LDAP_BACKGROUND_SYNC_INTERVAL=''
#
+4 -4
View File
@@ -650,12 +650,12 @@ services:
# If the sync of the users should be done in the background
#- LDAP_BACKGROUND_SYNC=false
#
# LDAP_BACKGROUND_SYNC_INTERVAL : At which interval does the background task sync in milliseconds
# The format must be as specified in:
# LDAP_BACKGROUND_SYNC_INTERVAL : How often the background task syncs: milliseconds (3600000 = hourly) or text
# Text is later.js text, quotes allowed, as specified in:
# https://bunkat.github.io/later/parsers.html#text
#- LDAP_BACKGROUND_SYNC_INTERVAL=every 1 hours
# At which interval does the background task sync in milliseconds.
# Leave this unset, so it uses default, and does not crash.
# Unset or empty uses the default (hourly). A value it cannot read is logged
# and the default is used; it no longer stops WeKan (#2354).
# https://github.com/wekan/wekan/issues/2354#issuecomment-515305722
- LDAP_BACKGROUND_SYNC_INTERVAL=''
#
@@ -511,16 +511,16 @@ services:
#- LDAP_BACKGROUND_SYNC=false
#
# At which interval does the background task sync.
# The format must be as specified in:
# Text is later.js text, quotes allowed, as specified in:
# https://bunkat.github.io/later/parsers.html#text
#- LDAP_BACKGROUND_SYNC_INTERVAL='every 1 hour'
#
# LDAP_BACKGROUND_SYNC_INTERVAL : At which interval does the background task sync in milliseconds
# The format must be as specified in:
# LDAP_BACKGROUND_SYNC_INTERVAL : How often the background task syncs: milliseconds (3600000 = hourly) or text
# Text is later.js text, quotes allowed, as specified in:
# https://bunkat.github.io/later/parsers.html#text
#- LDAP_BACKGROUND_SYNC_INTERVAL=every 1 hours
# At which interval does the background task sync in milliseconds.
# Leave this unset, so it uses default, and does not crash.
# Unset or empty uses the default (hourly). A value it cannot read is logged
# and the default is used; it no longer stops WeKan (#2354).
# https://github.com/wekan/wekan/issues/2354#issuecomment-515305722
- LDAP_BACKGROUND_SYNC_INTERVAL=''
#
+16 -16
View File
@@ -88,12 +88,12 @@ sudo snap set wekan ldap-authentication-userdn='CN=LDAP-User,OU=Service Accounts
sudo snap set wekan ldap-authentication-password='<password>'
sudo snap set wekan ldap-log-enabled='true'
sudo snap set wekan ldap-background-sync='true'
# LDAP_BACKGROUND_SYNC_INTERVAL : At which interval does the background task sync in milliseconds
# The format must be as specified in:
# LDAP_BACKGROUND_SYNC_INTERVAL : How often the background task syncs: milliseconds (3600000 = hourly) or text
# Text is later.js text, quotes allowed, as specified in:
# https://bunkat.github.io/later/parsers.html#text
#sudo snap set wekan ldap-background-sync-interval='every 1 hours'
# At which interval does the background task sync in milliseconds.
# If not in use, Leave this unset, so it uses default, and does not crash.
# Milliseconds (3600000 = hourly) or later.js text such as every 1 hours.
# If not in use, leave this unset: it uses the default (hourly).
# https://github.com/wekan/wekan/issues/2354#issuecomment-515305722
#sudo snap set wekan ldap-background-sync-interval=''
sudo snap set wekan ldap-background-sync-interval='every 1 hours'
@@ -144,12 +144,12 @@ snap set wekan ldap-basedn='dc=example,dc=com'
snap set wekan ldap-background-sync='true'
snap set wekan ldap-background-sync-keep-existant-users-updated='true'
snap set wekan ldap-background-sync-import-new-users='true'
# LDAP_BACKGROUND_SYNC_INTERVAL : At which interval does the background task sync in milliseconds
# The format must be as specified in:
# LDAP_BACKGROUND_SYNC_INTERVAL : How often the background task syncs: milliseconds (3600000 = hourly) or text
# Text is later.js text, quotes allowed, as specified in:
# https://bunkat.github.io/later/parsers.html#text
#sudo snap set wekan ldap-background-sync-interval='every 1 hours'
# At which interval does the background task sync in milliseconds.
# If not in use, Leave this unset, so it uses default, and does not crash.
# Milliseconds (3600000 = hourly) or later.js text such as every 1 hours.
# If not in use, leave this unset: it uses the default (hourly).
# https://github.com/wekan/wekan/issues/2354#issuecomment-515305722
#sudo snap set wekan ldap-background-sync-interval=''
snap set wekan ldap-background-sync-interval='every 1 hours'
@@ -173,12 +173,12 @@ sudo snap set wekan ldap-authentication-password='********'
sudo snap set wekan ldap-authentication-userdn='cn=admin,dc=*******,dc=lan'
sudo snap set wekan ldap-background-sync='true'
sudo snap set wekan ldap-background-sync-import-new-users='true'
# LDAP_BACKGROUND_SYNC_INTERVAL : At which interval does the background task sync in milliseconds
# The format must be as specified in:
# LDAP_BACKGROUND_SYNC_INTERVAL : How often the background task syncs: milliseconds (3600000 = hourly) or text
# Text is later.js text, quotes allowed, as specified in:
# https://bunkat.github.io/later/parsers.html#text
#sudo snap set wekan ldap-background-sync-interval='every 1 hours'
# At which interval does the background task sync in milliseconds.
# If not in use, Leave this unset, so it uses default, and does not crash.
# Milliseconds (3600000 = hourly) or later.js text such as every 1 hours.
# If not in use, leave this unset: it uses the default (hourly).
# https://github.com/wekan/wekan/issues/2354#issuecomment-515305722
#sudo snap set wekan ldap-background-sync-interval=''
sudo snap set wekan ldap-background-sync-interval='every 1 hours'
@@ -423,12 +423,12 @@ services:
# LDAP_BACKGROUND_SYNC : If the sync of the users should be done in the background
# example : LDAP_BACKGROUND_SYNC=true
- LDAP_BACKGROUND_SYNC=false
# LDAP_BACKGROUND_SYNC_INTERVAL : At which interval does the background task sync in milliseconds
# The format must be as specified in:
# LDAP_BACKGROUND_SYNC_INTERVAL : How often the background task syncs: milliseconds (3600000 = hourly) or text
# Text is later.js text, quotes allowed, as specified in:
# https://bunkat.github.io/later/parsers.html#text
#- LDAP_BACKGROUND_SYNC_INTERVAL=every 1 hours
# At which interval does the background task sync in milliseconds.
# Leave this unset, so it uses default, and does not crash.
# Unset or empty uses the default (hourly). A value it cannot read is logged
# and the default is used; it no longer stops WeKan (#2354).
# https://github.com/wekan/wekan/issues/2354#issuecomment-515305722
- LDAP_BACKGROUND_SYNC_INTERVAL=''
# LDAP_BACKGROUND_SYNC_KEEP_EXISTANT_USERS_UPDATED :
+4 -7
View File
@@ -6,6 +6,7 @@ import { parseGroupAllowlist, filterGroupsByAllowlist } from './groupAllowlist';
import { isAdminByGroups } from './adminGroups';
import { runWithLdapDisconnect } from './connectionGuard';
import { log_debug, log_info, log_warn, log_error } from './logger';
import { ldapSyncSchedule } from './syncSchedule';
import { getLdapPhotoBuffer } from './ldapPhoto';
import { ldapPresenceUpdate } from './presenceSync';
import { isKnownLdapGroup } from './entryKind';
@@ -688,13 +689,9 @@ const addCronJob = debounce(function addCronJobDebounced() {
log_info('Enabling LDAP Background Sync');
sc.add({
name: jobName,
schedule: function(parser) {
if (LDAP.settings_get('LDAP_BACKGROUND_SYNC_INTERVAL')) {
return parser.text(LDAP.settings_get('LDAP_BACKGROUND_SYNC_INTERVAL'));
}
else {
return parser.recur().on(0).minute();
}},
// Milliseconds (as documented, which settings_get() turns into a Number)
// or later.js text; never throws (syncSchedule.js).
schedule: parser => ldapSyncSchedule(parser, LDAP.settings_get('LDAP_BACKGROUND_SYNC_INTERVAL'), log_warn),
job: async function() {
await runLdapSync();
},
@@ -0,0 +1,79 @@
'use strict';
// LDAP_BACKGROUND_SYNC_INTERVAL, read into a SyncedCron schedule.
//
// The docs call it the interval "in milliseconds", and the snap/Docker
// examples also show later.js text ("every 1 hours"). Both were handed to
// later's parser.text(), and settings_get() JSON-parses the environment
// string, so a millisecond value arrived as a Number: later called
// toLowerCase() on it, the throw escaped a timer as an uncaughtException,
// and WeKan stopped - a snap restart loop for anybody who followed the docs.
// Text that later could not read did not throw, but gave a schedule that
// never fired, so background sync silently stopped.
//
// ldapSyncSchedule() accepts both and never throws:
//
// a number, or a string of digits milliseconds, as documented
// other text later.js text, e.g. "every 1 hours"
// either one in quotes ('…', "…") the same, quotes removed (#2354)
// unset / empty the default, recur().on(0).minute()
// (later: at minute 0, so hourly)
// anything unreadable the default, with a warning saying why
//
// `parser` is later's parser (SyncedCron passes it); `warn` logs. Pure apart
// from those two: tests/ldapSyncSchedule.test.cjs.
const MINUTE = 60 * 1000;
function defaultSchedule(parser) {
return parser.recur().on(0).minute();
}
function fromMilliseconds(parser, ms) {
// later counts in whole seconds; the shortest useful sync is one second,
// and a whole number of minutes is written as minutes.
if (ms % MINUTE === 0) return parser.recur().every(ms / MINUTE).minute();
return parser.recur().every(Math.max(1, Math.round(ms / 1000))).second();
}
// #2354: `LDAP_BACKGROUND_SYNC_INTERVAL='every 1 mins'` in a compose file or
// an env file keeps its quotes, and later cannot read a quoted text.
function unquote(value) {
if (typeof value !== 'string') return value;
const trimmed = value.trim();
const match = /^(['"])(.*)\1$/.exec(trimmed);
return match ? match[2].trim() : trimmed;
}
function ldapSyncSchedule(parser, rawValue, warn = () => {}) {
const value = unquote(rawValue);
if (value === undefined || value === null || value === '' || value === false) {
return defaultSchedule(parser);
}
if (typeof value === 'number' || (typeof value === 'string' && /^\s*\d+\s*$/.test(value))) {
const ms = Number(value);
if (Number.isFinite(ms) && ms > 0) return fromMilliseconds(parser, ms);
warn(`LDAP_BACKGROUND_SYNC_INTERVAL ${JSON.stringify(value)} is not a positive number of milliseconds; using the default schedule`);
return defaultSchedule(parser);
}
if (typeof value !== 'string') {
warn(`LDAP_BACKGROUND_SYNC_INTERVAL must be milliseconds or text such as "every 1 hours", not ${typeof value}; using the default schedule`);
return defaultSchedule(parser);
}
let parsed;
try {
parsed = parser.text(value);
} catch (error) {
parsed = null;
}
// later.parse.text() reports the position of the first word it could not
// read in `error` (-1 when it read everything).
if (!parsed || !Array.isArray(parsed.schedules) || !parsed.schedules.length
|| (typeof parsed.error === 'number' && parsed.error !== -1)) {
warn(`LDAP_BACKGROUND_SYNC_INTERVAL ${JSON.stringify(value)} is not later.js text such as "every 1 hours" or milliseconds; using the default schedule`);
return defaultSchedule(parser);
}
return parsed;
}
module.exports = { ldapSyncSchedule };
+1 -1
View File
@@ -510,7 +510,7 @@ DESCRIPTION_LDAP_BACKGROUND_SYNC="If the sync of the users should be done in the
DEFAULT_LDAP_BACKGROUND_SYNC="false"
KEY_LDAP_BACKGROUND_SYNC="ldap-background-sync"
DESCRIPTION_LDAP_BACKGROUND_SYNC_INTERVAL="At which interval does the background task sync"
DESCRIPTION_LDAP_BACKGROUND_SYNC_INTERVAL="How often the background task syncs: milliseconds (3600000 = hourly) or later.js text such as every 1 hours"
DEFAULT_LDAP_BACKGROUND_SYNC_INTERVAL=""
KEY_LDAP_BACKGROUND_SYNC_INTERVAL="ldap-background-sync-interval"
+125
View File
@@ -0,0 +1,125 @@
'use strict';
// LDAP_BACKGROUND_SYNC_INTERVAL (packages/wekan-ldap/server/syncSchedule.js).
// A snap stopped in a restart loop: "[uncaughtException] WeKan is stopping:
// TypeError: string.toLowerCase is not a function at later.parse.text ...
// at Object.schedule (wekan-ldap/server/sync.js)". The docs give the interval
// in milliseconds, settings_get() turned that into a Number, and later's text
// parser called toLowerCase() on it from a timer.
// Run: node tests/ldapSyncSchedule.test.cjs
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { ldapSyncSchedule } = require('../packages/wekan-ldap/server/syncSchedule');
const ROOT = path.join(__dirname, '..');
let passed = 0;
const test = (name, fn) => { fn(); passed += 1; console.log(`ok - ${name}`); };
// The real later.js, as SyncedCron hands it to schedule(), when a build has it.
const LATER = path.join(ROOT, '.build/bundle/programs/server/npm/node_modules/meteor/quave_synced-cron/node_modules/@breejs/later');
const later = fs.existsSync(LATER) ? require(LATER) : null;
// A recording stand-in with later's shape, so the decisions are checked
// without a build too.
function fakeParser() {
const calls = [];
const chain = (...prefix) => new Proxy({}, {
get: (_, name) => (...args) => {
const step = [...prefix, `${String(name)}(${args.join(',')})`];
calls.push(step.join('.'));
return chain(...step);
},
});
return {
calls,
recur: () => chain('recur()'),
text: value => {
if (typeof value !== 'string') throw new TypeError('string.toLowerCase is not a function');
return /^every \d+ (hours|minutes|mins|seconds)$/.test(value) ? { schedules: [{ h: [0] }], exceptions: [], error: -1 } : { schedules: [{}], exceptions: [], error: 0 };
},
};
}
const warnings = [];
const warn = message => warnings.push(message);
test('milliseconds, as documented, become an interval - from a Number or a string', () => {
for (const value of [3600000, '3600000', ' 600000 ']) {
const parser = fakeParser();
assert.doesNotThrow(() => ldapSyncSchedule(parser, value, warn));
assert.ok(parser.calls.some(c => /every\((60|10)\)\.minute\(\)$/.test(c)), String(value));
}
const seconds = fakeParser();
ldapSyncSchedule(seconds, 90000, warn);
assert.ok(seconds.calls.some(c => /every\(90\)\.second\(\)$/.test(c)), 'not a whole minute: seconds');
});
test('later.js text still works, also in the quotes a compose file keeps (#2354)', () => {
for (const value of ['every 1 hours', "'every 1 hours'", '"every 1 hours"', " 'every 1 hours' "]) {
assert.deepEqual(ldapSyncSchedule(fakeParser(), value, warn).error, -1, value);
}
// ...and quoted milliseconds, and quoted nothing.
const ms = fakeParser();
ldapSyncSchedule(ms, "'600000'", warn);
assert.ok(ms.calls.some(c => /every\(10\)\.minute\(\)$/.test(c)));
const empty = fakeParser();
ldapSyncSchedule(empty, "''", warn);
assert.deepEqual(empty.calls.slice(-1), ['recur().on(0).minute()']);
});
test('unset keeps the original default, recur().on(0).minute()', () => {
for (const value of [undefined, null, '', false]) {
const parser = fakeParser();
ldapSyncSchedule(parser, value, warn);
assert.deepEqual(parser.calls.slice(-1), ['recur().on(0).minute()'], String(value));
}
});
test('negative: nothing a setting can hold throws, and unreadable values are reported', () => {
warnings.length = 0;
for (const value of [0, -5, 'every banana', true, { a: 1 }, ['x'], NaN]) {
const parser = fakeParser();
assert.doesNotThrow(() => ldapSyncSchedule(parser, value, warn), String(value));
assert.deepEqual(parser.calls.slice(-1), ['recur().on(0).minute()'], `${String(value)}: the default`);
}
assert.equal(warnings.length, 7, 'each one says why');
assert.ok(warnings.every(w => /LDAP_BACKGROUND_SYNC_INTERVAL/.test(w) && /default schedule/.test(w)));
});
test('with the real later.js: the crash is reproduced, and the fix schedules', function () {
if (!later) { console.log(' (skipped: no .build bundle with later.js)'); return; }
assert.throws(() => later.parse.text(3600000), /toLowerCase is not a function/, 'the reported crash');
const hourly = ldapSyncSchedule(later.parse, 3600000, warn);
const next = later.schedule(hourly).next(2, new Date('2026-10-14T13:44:37Z'));
assert.equal(next[1] - next[0], 3600000, 'one hour apart');
const text = ldapSyncSchedule(later.parse, 'every 1 hours', warn);
assert.equal(text.error, -1);
// #2354: the quoted form later itself cannot read is read now.
assert.notEqual(later.parse.text("'every 1 mins'").error, -1, 'later alone fails on the quotes');
assert.equal(ldapSyncSchedule(later.parse, "'every 1 mins'", warn).error, -1);
// The documented example: 10000 ms.
const tenSeconds = later.schedule(ldapSyncSchedule(later.parse, 10000, warn)).next(2, new Date('2026-10-14T13:44:37Z'));
assert.equal(tenSeconds[1] - tenSeconds[0], 10000);
const fallback = ldapSyncSchedule(later.parse, 'every banana', warn);
assert.ok(later.schedule(fallback).next(1), 'an unreadable value still schedules');
});
test('sync.js schedules through it, and nowhere hands a setting to parser.text directly', () => {
const sync = fs.readFileSync(path.join(ROOT, 'packages/wekan-ldap/server/sync.js'), 'utf8');
assert.match(sync, /schedule: parser => ldapSyncSchedule\(parser, LDAP\.settings_get\('LDAP_BACKGROUND_SYNC_INTERVAL'\), log_warn\)/);
// Negative, across every server file: no raw setting reaches later's parser.
const files = [];
const walk = dir => {
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
if (['node_modules', '.build', '_build', '.tools', '.meteor', '.git'].includes(e.name)) continue;
const p = path.join(dir, e.name);
if (e.isDirectory()) walk(p); else if (/\.(js|mjs|cjs)$/.test(e.name)) files.push(p);
}
};
for (const dir of ['server', 'packages', 'models']) walk(path.join(ROOT, dir));
const offenders = files.filter(f => /parser\.text\(\s*(?:LDAP\.)?settings_get|parser\.text\(\s*process\.env/.test(fs.readFileSync(f, 'utf8')));
assert.deepEqual(offenders.map(f => path.relative(ROOT, f)), []);
});
console.log(`# ${passed} passed`);