Files
proxy/nodejs/test/unit/host_lookup.test.js
T
wmantly 17b903e228 Fix two wildcard-cert gaps: attaching an existing host, and the wildcard's own base domain
- Host.prototype.update() had no challengeType handling (only create() did),
  so selecting "Parent Wildcard" on an existing host's edit form silently
  did nothing. Added the same wildcard-parent lookup to update(), using a
  new Host.lookUpWildcardParent() -- the existing lookUp() can't be reused
  here since an already-created host resolves to its own leaf rather than
  falling through to a sibling wildcard.

- A wildcard's issued cert covers both the base domain and *.base domain
  (altNames), but the lookup tree stores the wildcard one level below its
  base -- looking up the bare base domain landed on an empty parent node
  and found nothing. buildLookUpObj() now also stamps that parent node,
  order-independent (a real host explicitly created at that exact name
  always still wins).

Verified both fixes against a real Redis-backed Host model (not just the
mocked lookup-tree tests) -- see PR description.

Bumps to v1.1.8.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KDEx8ghuZR61pqPXc6da9C
2026-07-17 18:46:09 -04:00

333 lines
10 KiB
JavaScript

'use strict';
const {describe, test, before} = require('node:test');
const assert = require('node:assert');
/**
* Tests for Host lookup algorithm
*
* The Host.lookUp method implements a complex tree-based lookup system
* that supports exact matches, single wildcards (*), and double wildcards (**).
*
* Pattern matching priority (highest to lowest):
* 1. Exact match (example.com)
* 2. Single wildcard (*.example.com matches any.example.com)
* 3. Double wildcard (**.example.com matches any.sub.domain.example.com)
*
* These tests validate the lookup algorithm without requiring a Redis connection.
*/
describe('Host Lookup Algorithm', () => {
let Host;
before(async () => {
// Mock the Host class and build a test lookup tree
Host = createMockHostClass();
await populateTestData(Host);
});
test('should match exact host', () => {
const result = Host.lookUp('payments.718it.biz');
assert.ok(result, 'Should find a match');
assert.strictEqual(result.host, 'payments.718it.biz');
});
test('should return undefined for non-existent host', () => {
const result = Host.lookUp('sd.blah.test.vm42.com');
assert.strictEqual(result, undefined);
});
test('should match double wildcard at any depth', () => {
const result = Host.lookUp('payments.test.com');
assert.ok(result, 'Should find a match');
assert.strictEqual(result.host, 'payments.**');
});
test('should match double wildcard with multiple subdomains', () => {
const result = Host.lookUp('test.sample.other.exmaple.com');
assert.ok(result, 'Should find a match');
assert.strictEqual(result.host, '**.exmaple.com');
});
test('should prefer exact match over wildcard', () => {
const result = Host.lookUp('stan.test.vm42.com');
assert.ok(result, 'Should find a match');
assert.strictEqual(result.host, 'stan.test.vm42.com');
});
test('should match at root level', () => {
const result = Host.lookUp('test.vm42.com');
assert.ok(result, 'Should find a match');
assert.strictEqual(result.host, 'test.vm42.com');
});
test('should match single wildcard', () => {
const result = Host.lookUp('blah.test.vm42.com');
assert.ok(result, 'Should find a match');
assert.strictEqual(result.host, '*.test.vm42.com');
});
test('should match double wildcard for top-level domain queries', () => {
const result = Host.lookUp('payments.example.com');
assert.ok(result, 'Should find a match');
assert.strictEqual(result.host, 'payments.**');
});
test('should match single wildcard in middle of domain', () => {
const result = Host.lookUp('info.wma.users.718it.biz');
assert.ok(result, 'Should find a match');
assert.strictEqual(result.host, 'info.*.users.718it.biz');
});
test('should return undefined when single wildcard does not match', () => {
const result = Host.lookUp('infof.users.718it.biz');
assert.strictEqual(result, undefined);
});
test('should return undefined for non-existent TLD', () => {
const result = Host.lookUp('blah.biz');
assert.strictEqual(result, undefined);
});
test('should match multiple single wildcards', () => {
const result = Host.lookUp('test.1.2.718it.net');
assert.ok(result, 'Should find a match');
assert.strictEqual(result.host, 'test.*.*.718it.net');
});
test('should match exact subdomain', () => {
const result = Host.lookUp('test1.exmaple.com');
assert.ok(result, 'Should find a match');
assert.strictEqual(result.host, 'test1.exmaple.com');
});
test('should match single wildcard when exact not found', () => {
const result = Host.lookUp('other.exmaple.com');
assert.ok(result, 'Should find a match');
assert.strictEqual(result.host, '*.exmaple.com');
});
test('should match double wildcard with subdomain prefix', () => {
const result = Host.lookUp('info.payments.example.com');
assert.ok(result, 'Should find a match');
assert.strictEqual(result.host, 'info.**');
});
test('should match bare domain', () => {
const result = Host.lookUp('718it.biz');
assert.ok(result, 'Should find a match');
assert.strictEqual(result.host, '718it.biz');
});
test('should handle single-part domain', () => {
const result = Host.lookUp('localhost');
assert.strictEqual(result, undefined);
});
test('should handle empty string', () => {
const result = Host.lookUp('');
assert.strictEqual(result, undefined);
});
test('should be case-sensitive', () => {
const result = Host.lookUp('PAYMENTS.718it.biz');
assert.strictEqual(result, undefined);
});
});
/**
* Tests for the wildcard's-own-base-domain fix: a single-level wildcard's
* issued cert also covers its own base domain (altNames: [domain, *.domain],
* see utils/letsencrypt.js), but that base domain sits one tree level ABOVE
* the wildcard's own leaf. buildLookUpObj() now also stamps that parent
* node's #record, and lookUpWildcardParent() finds it even when the base
* domain is ALSO separately registered as its own plain host (the "attach an
* existing host to a parent wildcard" case, unlike lookUp() which would just
* resolve to that host's own record).
*/
describe('Host wildcard base-domain lookup', () => {
let Host;
before(async () => {
Host = createMockHostClassWithWildcardParentFix();
});
test('lookUp finds the wildcard record for its own bare base domain when no plain host exists', async () => {
await populateTree(Host, ['*.cool.mysite.com']);
const result = Host.lookUp('cool.mysite.com');
assert.ok(result, 'Should find a match');
assert.strictEqual(result.host, '*.cool.mysite.com');
});
test('lookUp still prefers an explicitly-created plain host over the wildcard, regardless of population order', async () => {
await populateTree(Host, ['*.cool.mysite.com', 'cool.mysite.com']);
assert.strictEqual(Host.lookUp('cool.mysite.com').host, 'cool.mysite.com');
await populateTree(Host, ['cool.mysite.com', '*.cool.mysite.com']);
assert.strictEqual(Host.lookUp('cool.mysite.com').host, 'cool.mysite.com');
});
test('lookUpWildcardParent finds the wildcard even when the base domain already has its own plain host', async () => {
await populateTree(Host, ['*.cool.mysite.com', 'cool.mysite.com']);
const result = Host.lookUpWildcardParent('cool.mysite.com');
assert.ok(result, 'Should find the sibling wildcard');
assert.strictEqual(result.host, '*.cool.mysite.com');
});
test('lookUpWildcardParent returns undefined when there is no wildcard sibling', async () => {
await populateTree(Host, ['cool.mysite.com']);
assert.strictEqual(Host.lookUpWildcardParent('cool.mysite.com'), undefined);
});
test('lookUpWildcardParent returns undefined for an unrelated host', async () => {
await populateTree(Host, ['*.cool.mysite.com']);
assert.strictEqual(Host.lookUpWildcardParent('other.example.com'), undefined);
});
});
/**
* Same mock shape as createMockHostClass() above, plus the parent-record
* stamp in the tree-population loop and the lookUpWildcardParent() method --
* both copied from the real implementation in models/host.js.
*/
function createMockHostClassWithWildcardParentFix() {
return class MockHost {
static lookUpObj = {};
static lookUp(host) {
let place = this.lookUpObj;
let last_resort = {};
let parent = undefined;
for(let fragment of host.split('.').reverse()){
parent = place;
if(place['**']) last_resort = place['**'];
if({...last_resort, ...place}[fragment]){
place = {...last_resort, ...place}[fragment];
}else if(place['*']){
place = place['*']
}else if(last_resort){
place = last_resort;
}
}
if(place && place['#record']) return place['#record'];
if(parent && parent['*'] && parent['*']['#record']) return parent['*']['#record'];
}
static lookUpWildcardParent(host) {
let place = this.lookUpObj;
for(let fragment of host.split('.').reverse()){
if(!place[fragment]) return undefined;
place = place[fragment];
}
if(place['*'] && place['*']['#record']) return place['*']['#record'];
}
};
}
async function populateTree(Host, hosts) {
Host.lookUpObj = {};
for(let host of hosts){
let fragments = host.split('.');
let pointer = Host.lookUpObj;
while(fragments.length){
let fragment = fragments.pop();
if(!pointer[fragment]){
pointer[fragment] = {};
}
if(fragments.length === 0){
pointer[fragment]['#record'] = {host};
if(fragment === '*' && !pointer['#record']){
pointer['#record'] = pointer[fragment]['#record'];
}
}
pointer = pointer[fragment];
}
}
}
/**
* Creates a mock Host class with just the lookUp functionality
* This allows us to test the algorithm without Redis dependencies
*/
function createMockHostClass() {
return class MockHost {
static lookUpObj = {};
static __lookUpIsReady = false;
static lookUp(host) {
// This is the exact implementation from models/host.js lines 324-357
let place = this.lookUpObj;
let last_resort = {};
for(let fragment of host.split('.').reverse()){
if(place['**']) last_resort = place['**'];
if({...last_resort, ...place}[fragment]){
place = {...last_resort, ...place}[fragment];
}else if(place['*']){
place = place['*']
}else if(last_resort){
place = last_resort;
}
}
if(place && place['#record']) return place['#record'];
}
};
}
/**
* Populates the mock Host class with test data
* Builds the lookup tree structure based on test cases from models/host.js
*/
async function populateTestData(Host) {
// Test data based on the commented test cases in models/host.js
const testHosts = [
'payments.718it.biz',
'payments.**',
'**.exmaple.com',
'stan.test.vm42.com',
'test.vm42.com',
'*.test.vm42.com',
'info.*.users.718it.biz',
'test.*.*.718it.net',
'test1.exmaple.com',
'*.exmaple.com',
'info.**',
'718it.biz',
];
Host.lookUpObj = {};
for(let host of testHosts){
let fragments = host.split('.');
let pointer = Host.lookUpObj;
while(fragments.length){
let fragment = fragments.pop();
if(!pointer[fragment]){
pointer[fragment] = {};
}
if(fragments.length === 0){
pointer[fragment]['#record'] = {host};
}
pointer = pointer[fragment];
}
}
Host.__lookUpIsReady = true;
}