Files
jump-host/nodejs/test/unit/user_file.test.js
T
wmantly 4879769cc7 Add standalone mode with @simpleworkjs/orm-backed user/host stores
- Add standalone.enabled config flag to switch between LDAP+SSO and
  ORM-backed backends without changing the production code path
- New ORM models: StandaloneUser (uid, passwordHash, sshPublicKeys,
  groups) and StandaloneHost (slug, displayName, kind, metadata)
- user_file.js and hosts_file.js implement the same interfaces as the
  LDAP client and accessibleHosts() respectively
- models/user_ldap.js and utils/access.js become conditional facades
  that delegate based on conf.standalone.enabled at require time
- Zero changes to ssh_server.js core logic, bridge.js, key_inject.js,
  tui_picker.js, or any other consumer
- Fix ssh_server.js: use ?? instead of || for listenPort (0 is falsy)
- Fix ssh_server.js: register session listeners before awaiting
  audit.create() so client exec/shell requests aren't rejected
- Patch StringField.toSequelize() and IntegerField.toSequelize() to
  pass through primaryKey (the ORM's UUIDField already does this)
- 47 tests pass (24 existing + 15 new unit + 3 existing integration
  + 5 new standalone integration)
- Defaults to SQLite; any Sequelize dialect works via conf.orm

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-26 21:34:53 -04:00

114 lines
4.1 KiB
JavaScript

'use strict';
// Unit tests for the ORM-backed user store (models/user_file.js).
// Uses a temp file SQLite database — no external services needed.
process.env.NODE_ENV = 'test';
const { test, before, after } = require('node:test');
const assert = require('node:assert');
const fs = require('fs');
const path = require('path');
const os = require('os');
const bcrypt = require('bcrypt');
const conf = require('@simpleworkjs/conf');
const { init } = require('@simpleworkjs/orm');
const StandaloneUser = require('../../models/standalone_user');
let testPasswordHash;
let tmpDir;
let userFile; // required after ORM init
before(async () => {
// Unique temp DB so this test file doesn't collide with other ORM tests.
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jump-host-test-userfile-'));
const dbPath = path.join(tmpDir, 'test.sqlite');
conf.standalone = { enabled: true };
conf.orm = { dialect: 'sqlite', storage: dbPath, logging: false };
await init({ conf: { orm: conf.orm }, models: [StandaloneUser] });
testPasswordHash = await bcrypt.hash('testpass', 4);
await StandaloneUser.create({
uid: 'alice',
passwordHash: testPasswordHash,
sshPublicKeys: ['ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... alice@laptop'],
groups: ['admin', 'developers'],
});
// Now that the ORM is initialized and conf.standalone is set, require the
// facade. It checks conf.standalone.enabled at require time.
userFile = require('../../models/user_file');
});
after(() => {
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {}
});
test('getUser returns user with synthesized dn and keys', async () => {
const user = await userFile.getUser('alice');
assert.ok(user);
assert.strictEqual(user.uid, 'alice');
assert.strictEqual(user.dn, 'uid=alice,ou=people,dc=standalone,dc=local');
assert.deepStrictEqual(user.sshPublicKeys, ['ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... alice@laptop']);
});
test('getUser returns null for unknown uid', async () => {
const user = await userFile.getUser('nobody');
assert.strictEqual(user, null);
});
test('getGroups returns user groups', async () => {
const groups = await userFile.getGroups('uid=alice,ou=people,dc=standalone,dc=local');
assert.deepStrictEqual(groups, ['admin', 'developers']);
});
test('getGroups returns empty array for unknown dn', async () => {
const groups = await userFile.getGroups('uid=nobody,ou=people,dc=standalone,dc=local');
assert.deepStrictEqual(groups, []);
});
test('getGroups returns empty array for malformed dn', async () => {
const groups = await userFile.getGroups('not-a-dn');
assert.deepStrictEqual(groups, []);
});
test('checkPassword returns true for correct password', async () => {
const ok = await userFile.checkPassword('uid=alice,ou=people,dc=standalone,dc=local', 'testpass');
assert.strictEqual(ok, true);
});
test('checkPassword returns false for wrong password', async () => {
const ok = await userFile.checkPassword('uid=alice,ou=people,dc=standalone,dc=local', 'wrongpass');
assert.strictEqual(ok, false);
});
test('checkPassword returns false for unknown user', async () => {
const ok = await userFile.checkPassword('uid=nobody,ou=people,dc=standalone,dc=local', 'testpass');
assert.strictEqual(ok, false);
});
test('addSshKey appends a new key', async () => {
const newKey = 'ssh-rsa AAAAB3NzaC1yc2E... bob@desktop';
await userFile.addSshKey('uid=alice,ou=people,dc=standalone,dc=local', newKey);
const user = await StandaloneUser.get('alice');
assert.ok(user.sshPublicKeys.includes(newKey));
});
test('addSshKey is idempotent', async () => {
const key = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... alice@laptop';
const userBefore = await StandaloneUser.get('alice');
const countBefore = userBefore.sshPublicKeys.length;
await userFile.addSshKey('uid=alice,ou=people,dc=standalone,dc=local', key);
const userAfter = await StandaloneUser.get('alice');
assert.strictEqual(userAfter.sshPublicKeys.length, countBefore);
});
test('addSshKey is a no-op for unknown user', async () => {
// Should not throw.
await userFile.addSshKey('uid=nobody,ou=people,dc=standalone,dc=local', 'ssh-rsa AAA...');
});