'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; }