test: dockerized test suite (openldap + redis + test-runner)
docker-compose.test.yml spins up the all-in-one OpenLDAP image, a standalone Redis, and a test-runner that seeds the test user and runs jest against them. globalSetup honors REDIS_URL; tests/setup.js initializes the ORM and flushes test Redis keys before the run. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+3
-3
@@ -20,9 +20,9 @@
|
||||
!directory_spec.md
|
||||
!docs/**/*.md
|
||||
|
||||
# Tests
|
||||
nodejs/tests/
|
||||
nodejs/*.test.js
|
||||
# Tests (excluded from production builds; test-runner Dockerfile copies them explicitly)
|
||||
# nodejs/tests/
|
||||
# nodejs/*.test.js
|
||||
|
||||
# Host dependency tree — let the image run a clean `npm ci`. Also avoids
|
||||
# copying platform-wrong native modules (e.g. bcrypt built for the host OS).
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# Test-runner image for SSO Manager.
|
||||
#
|
||||
# Installs all dependencies (including dev) and bundles the app code plus
|
||||
# the seed script. The entrypoint waits for LDAP + Redis, seeds the test
|
||||
# user, then runs whatever command is given (default: npm test).
|
||||
|
||||
FROM node:20-alpine
|
||||
|
||||
# Install OpenLDAP clients (ldapadd, ldapsearch) and bash for the seed script
|
||||
RUN apk add --no-cache openldap-clients bash
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy and install dependencies (including devDependencies for jest/supertest)
|
||||
COPY nodejs/package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Copy the application source
|
||||
COPY nodejs/app.js ./
|
||||
COPY nodejs/bin ./bin
|
||||
COPY nodejs/conf ./conf
|
||||
COPY nodejs/controller ./controller
|
||||
COPY nodejs/middleware ./middleware
|
||||
COPY nodejs/models ./models
|
||||
COPY nodejs/routes ./routes
|
||||
COPY nodejs/services ./services
|
||||
COPY nodejs/utils ./utils
|
||||
COPY nodejs/views ./views
|
||||
COPY nodejs/public ./public
|
||||
COPY nodejs/tests ./tests
|
||||
|
||||
# SQLite database directory (config/inventory.sqlite for Resource model's ORM)
|
||||
RUN mkdir -p /app/config
|
||||
|
||||
# Files expected at the flattened /app path (see Dockerfile.openldap notes)
|
||||
COPY tos.md /tos.md
|
||||
COPY README.md /README.md
|
||||
COPY CHANGELOG.md /CHANGELOG.md
|
||||
COPY DEPLOYMENT.md /DEPLOYMENT.md
|
||||
COPY API.md /API.md
|
||||
COPY directory_spec.md /directory_spec.md
|
||||
COPY docs /docs
|
||||
|
||||
# Seed script and utility
|
||||
COPY test_seed.js ./test_seed.js
|
||||
COPY test/seed-test-user.sh /usr/local/bin/seed-test-user
|
||||
RUN chmod +x /usr/local/bin/seed-test-user
|
||||
|
||||
# Default command: seed the test user, then run the test suite
|
||||
CMD ["sh", "-c", "seed-test-user && npm test"]
|
||||
@@ -0,0 +1,81 @@
|
||||
# Docker Compose for running the SSO Manager test suite.
|
||||
#
|
||||
# Spins up:
|
||||
# ldap — OpenLDAP + Redis (all-in-one image, slapd + redis only, no app)
|
||||
# redis — Standalone Redis for the app's model/token storage
|
||||
# test-runner — Seeds the test user, then runs `npm test`
|
||||
#
|
||||
# Usage:
|
||||
# docker compose -f docker-compose.test.yml up --build
|
||||
# # Or to run a specific test file:
|
||||
# docker compose -f docker-compose.test.yml run --rm test-runner npx jest tests/auth.test.js
|
||||
#
|
||||
# The LDAP service uses the same Dockerfile.openldap image as production but
|
||||
# overrides the command to only start slapd + redis (the entrypoint handles
|
||||
# slapd.conf generation, directory initialization, and Redis startup before
|
||||
# running the given command — "sleep infinity" keeps it alive).
|
||||
#
|
||||
# The test-runner connects to ldap:389 and redis:6379 via Docker networking.
|
||||
# app_* env vars override conf/secrets.js (highest precedence in
|
||||
# @simpleworkjs/conf), so the production secrets.js is never read.
|
||||
|
||||
services:
|
||||
ldap:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.openldap
|
||||
environment:
|
||||
- LDAP_BASE_DN=dc=test,dc=local
|
||||
- LDAP_ADMIN_PASS=secret
|
||||
- ORG_NAME=Test SSO
|
||||
# The entrypoint starts slapd + redis, then runs whatever command is given.
|
||||
# "sleep infinity" keeps the container alive so the test-runner can connect.
|
||||
command: ["sleep", "infinity"]
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "ldapsearch -x -H ldap://localhost:389 -b '' -s base '(objectClass=*)' >/dev/null 2>&1"]
|
||||
interval: 2s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
start_period: 5s
|
||||
volumes:
|
||||
- ldap-data:/var/lib/ldap
|
||||
- ldap-certs:/etc/openldap/certs
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 2s
|
||||
timeout: 3s
|
||||
retries: 15
|
||||
|
||||
test-runner:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.test-runner
|
||||
environment:
|
||||
# Tell the app which environment it's in (loads conf/test.js for Redis prefix)
|
||||
- NODE_ENV=test
|
||||
# LDAP — point at the ldap service container
|
||||
- app_ldap__url=ldap://ldap:389
|
||||
- app_ldap__bindDN=cn=admin,dc=test,dc=local
|
||||
- app_ldap__bindPassword=secret
|
||||
- app_ldap__userBase=ou=people,dc=test,dc=local
|
||||
- app_ldap__groupBase=ou=groups,dc=test,dc=local
|
||||
# Redis — point at the redis service container
|
||||
- app_redis__redisConf__url=redis://redis:6379
|
||||
# Also used by tests/globalSetup.js (direct Redis client, not @simpleworkjs/conf)
|
||||
- REDIS_URL=redis://redis:6379
|
||||
# JWT secret (required by the app, not sensitive in test)
|
||||
- app_oauth__jwtSecret=test-jwt-secret-for-testing-only
|
||||
# App name
|
||||
- app_name=Test SSO
|
||||
depends_on:
|
||||
ldap:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
|
||||
volumes:
|
||||
ldap-data:
|
||||
ldap-certs:
|
||||
@@ -5,7 +5,8 @@
|
||||
const { createClient } = require('redis');
|
||||
|
||||
module.exports = async function() {
|
||||
const client = createClient();
|
||||
const redisUrl = process.env.REDIS_URL || undefined;
|
||||
const client = createClient(redisUrl ? { url: redisUrl } : {});
|
||||
await client.connect();
|
||||
|
||||
const keys = await client.keys('sso_manager_test_*');
|
||||
|
||||
@@ -3,6 +3,19 @@
|
||||
const crypto = require('crypto');
|
||||
const request = require('supertest');
|
||||
const app = require('../app');
|
||||
const { initORM } = require('../models');
|
||||
|
||||
beforeAll(async () => {
|
||||
await initORM();
|
||||
const { Token } = require('../models/token');
|
||||
try {
|
||||
if (Token.orm) {
|
||||
await Token.orm.adapter(Token).Table.redisClient.flushDb();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not flush Redis:', e.message);
|
||||
}
|
||||
});
|
||||
|
||||
const TEST_CREDS = { uid: 'test', password: 'MyTestPassword!2' };
|
||||
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env bash
|
||||
# seed-test-user.sh — Create the test user in LDAP for the test suite.
|
||||
#
|
||||
# The test suite (tests/setup.js) logs in as uid=test / password=MyTestPassword!2.
|
||||
# This script creates that user in the LDAP directory along with its personal
|
||||
# posixGroup, and adds it to the app_sso_admin group so admin-gated tests pass.
|
||||
#
|
||||
# Environment variables (from docker-compose.test.yml):
|
||||
# LDAP_HOST — LDAP server hostname (default: ldap)
|
||||
# LDAP_PORT — LDAP server port (default: 389)
|
||||
# BIND_DN — LDAP admin bind DN (default: cn=admin,dc=test,dc=local)
|
||||
# BIND_PW — LDAP admin password (default: secret)
|
||||
# BASE_DN — LDAP base DN (default: dc=test,dc=local)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
LDAP_HOST="${LDAP_HOST:-ldap}"
|
||||
LDAP_PORT="${LDAP_PORT:-389}"
|
||||
BIND_DN="${BIND_DN:-cn=admin,dc=test,dc=local}"
|
||||
BIND_PW="${BIND_PW:-secret}"
|
||||
BASE_DN="${BASE_DN:-dc=test,dc=local}"
|
||||
|
||||
LDAP_URI="ldap://${LDAP_HOST}:${LDAP_PORT}"
|
||||
USER_UID="test"
|
||||
USER_PASSWORD="MyTestPassword!2"
|
||||
|
||||
info() { echo "[INFO] $*"; }
|
||||
error() { echo "[ERROR] $*" >&2; }
|
||||
|
||||
# ── Wait for LDAP to be reachable ────────────────────────────────────────────
|
||||
info "Waiting for LDAP at ${LDAP_URI}..."
|
||||
for i in $(seq 1 30); do
|
||||
if ldapsearch -x -H "$LDAP_URI" -b '' -s base '(objectClass=*)' >/dev/null 2>&1; then
|
||||
info "LDAP is reachable"
|
||||
break
|
||||
fi
|
||||
if [ "$i" -eq 30 ]; then
|
||||
error "LDAP not reachable after 30 attempts"
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# ── Check if the test user already exists ────────────────────────────────────
|
||||
if ldapsearch -x -H "$LDAP_URI" -D "$BIND_DN" -w "$BIND_PW" \
|
||||
-b "cn=${USER_UID},ou=people,${BASE_DN}" -s base '(objectClass=*)' >/dev/null 2>&1; then
|
||||
info "Test user '${USER_UID}' already exists — skipping seed"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Generate the SSHA512 password hash ───────────────────────────────────────
|
||||
# Inline the hash function to avoid requiring the full model chain (which
|
||||
# tries to connect to Redis/LDAP during module loading and would hang).
|
||||
info "Generating password hash..."
|
||||
PASSWORD_HASH=$(node -e "
|
||||
const crypto = require('crypto');
|
||||
const salt = crypto.randomBytes(8);
|
||||
const hash = crypto.createHash('sha512').update('${USER_PASSWORD}').update(salt).digest();
|
||||
console.log('{SSHA512}' + Buffer.concat([hash, salt]).toString('base64'));
|
||||
")
|
||||
|
||||
info "Password hash generated"
|
||||
|
||||
# ── Create a temporary LDIF file ─────────────────────────────────────────────
|
||||
TMPFILE=$(mktemp /tmp/seed-test-user.XXXXXX)
|
||||
trap 'rm -f "$TMPFILE"' EXIT
|
||||
|
||||
cat > "$TMPFILE" << LDIF
|
||||
# Personal posixGroup for the test user
|
||||
dn: cn=${USER_UID},ou=groups,${BASE_DN}
|
||||
objectClass: posixGroup
|
||||
objectClass: top
|
||||
cn: ${USER_UID}
|
||||
gidNumber: 1500
|
||||
description: Personal group for test user
|
||||
|
||||
# Test user posixAccount
|
||||
dn: cn=${USER_UID},ou=people,${BASE_DN}
|
||||
objectClass: inetOrgPerson
|
||||
objectClass: posixAccount
|
||||
objectClass: top
|
||||
objectClass: theta42Person
|
||||
objectClass: ldapPublicKey
|
||||
objectClass: sudoRole
|
||||
cn: ${USER_UID}
|
||||
sn: Test
|
||||
uid: ${USER_UID}
|
||||
uidNumber: 1500
|
||||
gidNumber: 1500
|
||||
homeDirectory: /home/${USER_UID}
|
||||
loginShell: /bin/bash
|
||||
mail: test@test.local
|
||||
userPassword: ${PASSWORD_HASH}
|
||||
description: Test user for automated test suite
|
||||
sudoHost: ALL
|
||||
sudoCommand: ALL
|
||||
sudoUser: ${USER_UID}
|
||||
LDIF
|
||||
|
||||
# ── Add the entries to LDAP ──────────────────────────────────────────────────
|
||||
info "Creating test user '${USER_UID}' in LDAP..."
|
||||
ldapadd -x -H "$LDAP_URI" -D "$BIND_DN" -w "$BIND_PW" -f "$TMPFILE" 2>/dev/null || true
|
||||
if ldapsearch -x -H "$LDAP_URI" -D "$BIND_DN" -w "$BIND_PW" \
|
||||
-b "cn=${USER_UID},ou=people,${BASE_DN}" -s base '(objectClass=*)' >/dev/null 2>&1; then
|
||||
info "Test user '${USER_UID}' exists or was created"
|
||||
else
|
||||
error "Failed to create test user '${USER_UID}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Add the test user to required SSO groups ─────────────────────────────────
|
||||
info "Adding test user to SSO admin groups..."
|
||||
for group in app_sso_admin app_sso_invite app_sso_oauth_admin; do
|
||||
ldapmodify -x -H "$LDAP_URI" -D "$BIND_DN" -w "$BIND_PW" << EOF 2>/dev/null || true
|
||||
dn: cn=${group},ou=groups,${BASE_DN}
|
||||
changetype: modify
|
||||
add: member
|
||||
member: cn=${USER_UID},ou=people,${BASE_DN}
|
||||
EOF
|
||||
done
|
||||
info "Test user added to SSO admin groups"
|
||||
|
||||
# ── Create additional users needed by tests ─────────────────────────────────
|
||||
# wmantly is referenced by OTP (otp.test.js) and impersonation (impersonate.test.js)
|
||||
# tests as an existing non-admin user.
|
||||
WMANTLY_UID="wmantly"
|
||||
if ! ldapsearch -x -H "$LDAP_URI" -D "$BIND_DN" -w "$BIND_PW" \
|
||||
-b "cn=${WMANTLY_UID},ou=people,${BASE_DN}" -s base '(objectClass=*)' >/dev/null 2>&1; then
|
||||
|
||||
info "Creating additional test user '${WMANTLY_UID}'..."
|
||||
|
||||
# Generate password hash for wmantly
|
||||
WMANTLY_HASH=$(node -e "
|
||||
const crypto = require('crypto');
|
||||
const salt = crypto.randomBytes(8);
|
||||
const hash = crypto.createHash('sha512').update('testpass').update(salt).digest();
|
||||
console.log('{SSHA512}' + Buffer.concat([hash, salt]).toString('base64'));
|
||||
")
|
||||
|
||||
cat > "$TMPFILE" << LDIF
|
||||
dn: cn=${WMANTLY_UID},ou=groups,${BASE_DN}
|
||||
objectClass: posixGroup
|
||||
objectClass: top
|
||||
cn: ${WMANTLY_UID}
|
||||
gidNumber: 1501
|
||||
|
||||
dn: cn=${WMANTLY_UID},ou=people,${BASE_DN}
|
||||
objectClass: inetOrgPerson
|
||||
objectClass: posixAccount
|
||||
objectClass: top
|
||||
objectClass: theta42Person
|
||||
objectClass: ldapPublicKey
|
||||
objectClass: sudoRole
|
||||
cn: ${WMANTLY_UID}
|
||||
sn: Mantly
|
||||
uid: ${WMANTLY_UID}
|
||||
uidNumber: 1501
|
||||
gidNumber: 1501
|
||||
homeDirectory: /home/${WMANTLY_UID}
|
||||
loginShell: /bin/bash
|
||||
mail: wmantly@test.local
|
||||
userPassword: ${WMANTLY_HASH}
|
||||
sudoHost: ALL
|
||||
sudoCommand: ALL
|
||||
sudoUser: ${WMANTLY_UID}
|
||||
LDIF
|
||||
ldapadd -x -H "$LDAP_URI" -D "$BIND_DN" -w "$BIND_PW" -f "$TMPFILE" 2>/dev/null || true
|
||||
if ldapsearch -x -H "$LDAP_URI" -D "$BIND_DN" -w "$BIND_PW" \
|
||||
-b "cn=${WMANTLY_UID},ou=people,${BASE_DN}" -s base '(objectClass=*)' >/dev/null 2>&1; then
|
||||
info "Additional test user '${WMANTLY_UID}' exists or was created"
|
||||
else
|
||||
error "Failed to create additional test user '${WMANTLY_UID}'"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
info "Seed complete — all test users are ready"
|
||||
Reference in New Issue
Block a user