Files
proxy/nodejs/test/unit/host_lookup.test.js
wmantly fcd97b12aa Fix attaching an existing sibling subdomain to a parent wildcard
Host.lookUpWildcardParent() walked all labels of the host down to its own
leaf and only inspected that leaf's "*" child, so it found a wildcard
nested under the host (the base-domain case, e.g. *.cool.mysite.com for
cool.mysite.com) but missed the common case where the wildcard is a
SIBLING of the host's leftmost label (e.g. *.nl.wgnode.com covering an
already-existing sso.nl.wgnode.com). The /wildcard-parent route then
returned nothing and the edit form's "Parent Wildcard" option stayed
greyed out, leaving no way to convert an existing auto-SSL host onto a
wildcard issued afterward.

Track the parent node during the walk and check the sibling "*" slot too.
The never-created-subdomain case is unchanged (plain lookUp()'s wildcard
fallback in the route still handles it). Sync the test mock and add
regression coverage for the sibling case (existing host, never-created
host via the route fallback, and a deeper-wildcard negative case).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 00:13:13 -04:00

436 lines
15 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);
});
// Regression: the common case -- an already-existing single-label subdomain
// (its own auto-SSL/HTTP-01 host) sitting beside a wildcard, e.g.
// sso.nl.wgnode.com under *.nl.wgnode.com. The wildcard is a SIBLING of the
// subdomain's leftmost label, not a child of its node, so the old walk (which
// consumed "sso" and only checked that leaf's "*" child) never found it and
// the edit form's "Parent Wildcard" option stayed permanently greyed out.
test('lookUpWildcardParent finds a sibling wildcard for an existing single-label subdomain', async () => {
await populateTree(Host, ['sso.nl.wgnode.com', '*.nl.wgnode.com']);
const result = Host.lookUpWildcardParent('sso.nl.wgnode.com');
assert.ok(result, 'Should find the sibling wildcard');
assert.strictEqual(result.host, '*.nl.wgnode.com');
});
// A subdomain with no leaf of its own (never created) is deliberately NOT
// this method's job -- the walk stops before reaching the sibling "*" slot.
// The route resolves that case via plain lookUp()'s wildcard fallback first
// (covered in the route-fallback describe block below).
test('lookUpWildcardParent returns undefined for a subdomain with no leaf of its own', async () => {
await populateTree(Host, ['*.nl.wgnode.com']);
assert.strictEqual(Host.lookUpWildcardParent('api.nl.wgnode.com'), undefined);
});
test('lookUpWildcardParent does not treat a deeper wildcard as covering a shallower host', async () => {
// *.deep.nl.wgnode.com must NOT be offered as a parent for sso.nl.wgnode.com
// (a single-level wildcard covers only its own direct children).
await populateTree(Host, ['sso.nl.wgnode.com', '*.deep.nl.wgnode.com']);
assert.strictEqual(Host.lookUpWildcardParent('sso.nl.wgnode.com'), undefined);
});
});
/**
* Tests for the exact fallback combination used by
* routes/host.js's GET /wildcard-parent/:item (and, via hostMatchWildcard(),
* the host create/edit form's "Parent Wildcard" option) -- lookUp() first
* (handles a brand-new subdomain that has no leaf of its own yet), falling
* back to lookUpWildcardParent() only when lookUp() didn't resolve to a
* wildcard (handles an ALREADY-EXISTING host, which lookUp() would resolve
* to its own record). Regression coverage for the edit-form bug where the
* "Parent Wildcard" option stayed permanently greyed out for an existing
* host, because the route only ever tried lookUp().
*/
describe('Host wildcard-parent route fallback (lookUp then lookUpWildcardParent)', () => {
let Host;
before(async () => {
Host = createMockHostClassWithWildcardParentFix();
});
function findWildcardParent(host){
let match = Host.lookUp(host);
if(!match || !match.is_wildcard) match = Host.lookUpWildcardParent(host);
return (match && match.is_wildcard) ? match : null;
}
test('finds the wildcard for a brand-new subdomain that was never created', async () => {
await populateTree(Host, ['*.cool.mysite.com']);
const result = findWildcardParent('newthing.cool.mysite.com');
assert.ok(result);
assert.strictEqual(result.host, '*.cool.mysite.com');
});
test('finds the wildcard for the wildcard\'s own base domain, whether or not it is already a plain host', async () => {
await populateTree(Host, ['*.cool.mysite.com']);
assert.strictEqual(findWildcardParent('cool.mysite.com').host, '*.cool.mysite.com');
await populateTree(Host, ['*.cool.mysite.com', 'cool.mysite.com']);
assert.strictEqual(findWildcardParent('cool.mysite.com').host, '*.cool.mysite.com');
});
test('returns null when the host has no wildcard sibling at all', async () => {
await populateTree(Host, ['cool.mysite.com']);
assert.strictEqual(findWildcardParent('cool.mysite.com'), null);
});
// The user's scenario: sso.nl.wgnode.com already exists as its own host, and
// a *.nl.wgnode.com wildcard is added afterward. lookUp() resolves to sso's
// own (non-wildcard) leaf, so the fallback to lookUpWildcardParent() is what
// surfaces the sibling wildcard and lets the edit form offer conversion.
test('finds the sibling wildcard for an already-existing single-label subdomain', async () => {
await populateTree(Host, ['sso.nl.wgnode.com', '*.nl.wgnode.com']);
const result = findWildcardParent('sso.nl.wgnode.com');
assert.ok(result);
assert.strictEqual(result.host, '*.nl.wgnode.com');
});
test('finds the sibling wildcard for a never-created single-label subdomain', async () => {
await populateTree(Host, ['*.nl.wgnode.com']);
const result = findWildcardParent('api.nl.wgnode.com');
assert.ok(result);
assert.strictEqual(result.host, '*.nl.wgnode.com');
});
});
/**
* 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;
let parent = undefined;
for(let fragment of host.split('.').reverse()){
if(!place[fragment]){ place = undefined; break; }
parent = place;
place = place[fragment];
}
// Case 1: wildcard is a child of host's own node (base domain).
if(place && place['*'] && place['*']['#record']) return place['*']['#record'];
// Case 2: wildcard is a sibling of host's leftmost label
// (single-label subdomain, e.g. sso.nl.wgnode.com -> *.nl.wgnode.com).
if(parent && parent['*'] && parent['*']['#record']) return parent['*']['#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){
// is_wildcard mirrors the real Host model's own field (set
// whenever a host is DNS-01 wildcard-issued, i.e. starts with
// "*."), needed by tests that check it the same way the real
// /wildcard-parent/:item route does.
pointer[fragment]['#record'] = {host, is_wildcard: host.startsWith('*.')};
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;
}