#!/bin/sh
# Functional autopkgtest for node-ldapts.
#
# Starts a throwaway OpenLDAP (slapd) server on a non-privileged port with a
# private database, then exercises the installed ldapts module against it:
# bind, add (insert), search and delete.
set -e

WORKDIR=$(mktemp -d "${AUTOPKGTEST_TMP:-/tmp}/ldapts-slapd.XXXXXX")
PORT=3899
URI="ldap://127.0.0.1:${PORT}"
SUFFIX="dc=example,dc=com"
ADMIN="cn=admin,${SUFFIX}"
PASSWORD="secret"
SLAPD_PID=""

cleanup() {
    [ -n "$SLAPD_PID" ] && kill "$SLAPD_PID" 2>/dev/null || true
    rm -rf "$WORKDIR"
}
trap cleanup EXIT INT TERM

mkdir -p "$WORKDIR/data"

# Minimal legacy-style configuration: enough schema for organization + person.
cat > "$WORKDIR/slapd.conf" <<EOF
include /etc/ldap/schema/core.schema
include /etc/ldap/schema/cosine.schema
include /etc/ldap/schema/inetorgperson.schema
pidfile $WORKDIR/slapd.pid
argsfile $WORKDIR/slapd.args
modulepath /usr/lib/ldap
moduleload back_mdb
database mdb
suffix "$SUFFIX"
rootdn "$ADMIN"
rootpw $PASSWORD
directory $WORKDIR/data
EOF

# Stay in the foreground (-d 0) so $! is the real slapd we can kill on exit.
/usr/sbin/slapd -f "$WORKDIR/slapd.conf" -h "$URI" -d 0 &
SLAPD_PID=$!

# The node script waits for slapd to accept connections before binding.
cat > "$WORKDIR/operations.mjs" <<'NODE'
import { Client } from '/usr/share/nodejs/ldapts/dist/index.mjs';

const url = process.env.LDAP_URI;
const adminDN = process.env.LDAP_ADMIN;
const password = process.env.LDAP_PASSWORD;
const base = process.env.LDAP_BASE;
const userDN = `cn=testuser,${base}`;

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

let failures = 0;
function check(ok, msg) {
  if (ok) {
    console.log(`ok - ${msg}`);
  } else {
    console.error(`not ok - ${msg}`);
    failures += 1;
  }
}

const client = new Client({ url });

// BIND (with a readiness retry loop while slapd is starting up)
let bound = false;
for (let i = 0; i < 60; i += 1) {
  try {
    await client.bind(adminDN, password);
    bound = true;
    break;
  } catch {
    await sleep(500);
  }
}
check(bound, 'bind as admin (rootdn)');
if (!bound) {
  process.exit(1);
}

// INSERT: the base entry, then a user entry
await client.add(base, {
  objectClass: ['top', 'dcObject', 'organization'],
  o: 'Example',
  dc: 'example',
});
await client.add(userDN, {
  objectClass: ['top', 'person', 'organizationalPerson', 'inetOrgPerson'],
  cn: 'testuser',
  sn: 'User',
  mail: 'testuser@example.com',
  userPassword: 'userpass',
});
check(true, 'add base and user entries (insert)');

// SEARCH
const { searchEntries } = await client.search(base, {
  scope: 'sub',
  filter: '(cn=testuser)',
});
check(searchEntries.length === 1, 'search returns exactly one entry');
check(searchEntries[0]?.dn === userDN, 'search returns the expected DN');
const mail = [].concat(searchEntries[0]?.mail ?? []).map(String);
check(mail.includes('testuser@example.com'), 'search returns the mail attribute');

// BIND as the freshly created user (authentication against stored password)
const userClient = new Client({ url });
let userBound = false;
try {
  await userClient.bind(userDN, 'userpass');
  userBound = true;
} catch {
  /* leave userBound false */
}
check(userBound, 'bind as the created user');
await userClient.unbind();

// DELETE
await client.del(userDN);
const after = await client.search(base, {
  scope: 'sub',
  filter: '(cn=testuser)',
});
check(after.searchEntries.length === 0, 'entry is gone after delete');

await client.del(base);
await client.unbind();

if (failures > 0) {
  console.error(`${failures} check(s) failed`);
  process.exit(1);
}
console.log('All ldapts operations (bind, search, insert, delete) succeeded');
NODE

LDAP_URI="$URI" LDAP_ADMIN="$ADMIN" LDAP_PASSWORD="$PASSWORD" LDAP_BASE="$SUFFIX" \
    node "$WORKDIR/operations.mjs"
