← back to Ventura Claw Leads

tests/auth.test.js

86 lines

// Auth lib unit tests — bcrypt round-trip + claim token lifecycle.
// Uses the live local PG (PGHOST=/tmp), seeds a temp business, cleans up.

require('dotenv').config({ path: require('path').resolve(__dirname, '..', '.env') });
process.env.SESSION_SECRET = process.env.SESSION_SECRET || 'test-secret';
process.env.NODE_ENV = 'test';

const test = require('node:test');
const assert = require('node:assert/strict');
const auth = require('../lib/auth');
const db = require('../lib/db');

let testBusinessId;

test.before(async () => {
  const r = await db.one(
    `INSERT INTO businesses (slug, business_name, vertical, status, source)
     VALUES ('__test_auth__', 'Auth Test Co', 'creative', 'active', 'test')
     ON CONFLICT (slug) DO UPDATE SET business_name = EXCLUDED.business_name
     RETURNING id`
  );
  testBusinessId = r.id;
});

test.after(async () => {
  await db.query(`DELETE FROM businesses WHERE slug = '__test_auth__'`);
  await db.pool.end();
});

test('bcrypt password round-trip', async () => {
  const hash = await auth.hashPassword('correct-horse-battery-staple');
  assert.ok(hash.startsWith('$2'), 'bcrypt prefix present');
  assert.equal(await auth.verifyPassword('correct-horse-battery-staple', hash), true);
  assert.equal(await auth.verifyPassword('wrong-password', hash), false);
  assert.equal(await auth.verifyPassword('correct-horse-battery-staple', null), false);
});

test('email shape validator', () => {
  assert.equal(auth.isEmailShape('a@b.co'), true);
  assert.equal(auth.isEmailShape('Mixed.Case+Tag@Sub.Example.com'), true);
  assert.equal(auth.isEmailShape('no-at-sign'), false);
  assert.equal(auth.isEmailShape(''), false);
  assert.equal(auth.isEmailShape('a@b'), false);
});

test('email normalization', () => {
  assert.equal(auth.normalizeEmail('  Foo@BAR.com '), 'foo@bar.com');
  assert.equal(auth.normalizeEmail(null), '');
});

test('claim token: mint, consume, mark consumed, double-consume blocked', async () => {
  const { token, expiresAt } = await auth.issueClaimToken({
    businessId: testBusinessId,
    email: 'tok-test@example.com',
    ipHash: 'abc'
  });
  assert.equal(token.length, 32);
  assert.ok(new Date(expiresAt) > new Date(), 'expiresAt is in the future');

  const r1 = await auth.consumeClaimToken(token);
  assert.equal(r1.ok, true);
  assert.equal(r1.claim.business_id, testBusinessId);

  await auth.markClaimConsumed(token);
  const r2 = await auth.consumeClaimToken(token);
  assert.equal(r2.ok, false);
  assert.equal(r2.reason, 'already_used');

  // Cleanup
  await db.query(`DELETE FROM claim_tokens WHERE token = $1`, [token]);
});

test('claim token: invalid token returns ok:false', async () => {
  const r = await auth.consumeClaimToken('not-a-real-token-32-chars-abcde');
  assert.equal(r.ok, false);
  assert.equal(r.reason, 'invalid');
});

test('createUser idempotent on email conflict', async () => {
  const a = await auth.createUser({ email: 'create-test@example.com', businessId: testBusinessId, displayName: 'A' });
  assert.ok(a.id);
  const b = await auth.createUser({ email: 'create-test@example.com', businessId: testBusinessId, displayName: 'B' });
  assert.equal(b.id, a.id, 'second createUser returns same row');
  await db.query(`DELETE FROM business_users WHERE email = 'create-test@example.com'`);
});