Files
wownerogue/test/rendererCdnPolicy.test.js
jwinterm fad2a695bd Wownerogue
A browser-based roguelike synchronized with crypto block timing. A run is a race
against the next block: reach the exit before the block lands, or the house wins.
The same codebase runs the solo dungeon, the social Tavern, and operator-selected
PvP and race rulesets, with separate free and paid leaderboards.

Self-hostable under the MIT License. See README.md to run it, docs/ARCHITECTURE.md
for the design, and docs/DEPLOY.md to operate an instance.
2026-07-27 08:23:01 -04:00

205 lines
7.1 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const vm = require('vm');
const crypto = require('crypto');
function loadRenderModes(runtime, preloadedThree = null) {
const window = {
console,
WOWNGEON_RUNTIME: runtime,
localStorage: {
getItem: () => null,
setItem: () => {},
},
RK: {
TileRenderer: function TileRenderer() {},
THREE: preloadedThree,
},
};
window.window = window;
const source = fs.readFileSync(
path.join(__dirname, '../html/js/render/renderModes.js'),
'utf8',
);
vm.runInNewContext(source, { window, console });
return window.RK;
}
function fakeElement(tagName) {
const classes = new Set();
const element = {
tagName,
children: [],
className: '',
textContent: '',
title: '',
attributes: {},
appendChild(child) { this.children.push(child); return child; },
setAttribute(name, value) { this.attributes[name] = String(value); },
classList: {
add(name) { classes.add(name); },
remove(name) { classes.delete(name); },
contains(name) { return classes.has(name); },
},
};
Object.defineProperty(element, 'innerHTML', {
set() { element.children = []; },
});
return element;
}
function loadPickerWithRuntimeDisabled() {
const picker = fakeElement('div');
const head = fakeElement('head');
const document = {
head,
createElement: fakeElement,
getElementById(id) {
if (id === 'renderPicker') return picker;
if (id === 'rp-style') return head.children.find(child => child.id === 'rp-style') || null;
return null;
},
addEventListener() {},
};
const window = {
console,
document,
WOWNGEON_RUNTIME: { rendererCdnEnabled: false },
localStorage: { getItem: () => null, setItem: () => {} },
RK: { TileRenderer: function TileRenderer() {} },
};
window.window = window;
const context = { window, document, console };
vm.runInNewContext(
fs.readFileSync(path.join(__dirname, '../html/js/render/renderModes.js'), 'utf8'),
context,
);
window.RK.entitlements.premium = true;
const focusGameplay = jest.fn();
window.RK.SPGame = {
mode: () => 'tiles',
setMode: () => true,
focusGameplay,
};
vm.runInNewContext(
fs.readFileSync(path.join(__dirname, '../html/js/ui/spRenderPicker.js'), 'utf8'),
context,
);
window.RK.rebuildRenderPicker();
return { picker, RK: window.RK, focusGameplay };
}
describe('optional renderer CDN policy', () => {
test('external renderer loading defaults closed', () => {
const RK = loadRenderModes(undefined);
expect(RK.externalRenderersEnabled()).toBe(false);
expect(RK.isModeRuntimeAvailable('3d')).toBe(false);
expect(RK.modeAvailability('3d')).toMatchObject({
usable: false,
runtimeAvailable: false,
reason: 'runtime_unavailable',
});
expect(RK.canUseMode('3d')).toBe(false);
const callback = jest.fn();
RK.ensureThree(callback);
expect(callback).toHaveBeenCalledWith(false);
});
test('a locally preloaded renderer remains usable with CDN loading disabled', () => {
const localThree = {
THREE: { REVISION: 'local-test' },
GLTFLoader: function GLTFLoader() {},
};
const RK = loadRenderModes({ rendererCdnEnabled: false }, localThree);
RK.entitlements.premium = true;
expect(RK.canUseMode('3d')).toBe(true);
const callback = jest.fn();
RK.ensureThree(callback);
expect(callback).toHaveBeenCalledWith(true);
});
test('the pinned same-origin renderer is available without enabling CDN execution', () => {
const RK = loadRenderModes({
rendererLocalEnabled: true,
rendererLocalVersion: '0.160.0',
rendererCdnEnabled: false,
});
RK.entitlements.premium = true;
expect(RK.externalRenderersEnabled()).toBe(false);
expect(RK.localRenderersEnabled()).toBe(true);
expect(RK.isModeRuntimeAvailable('3d')).toBe(true);
expect(RK.canUseMode('3d')).toBe(true);
expect(RK.threeModuleSources()).toEqual({
library: '/vendor/three/0.160.0/three.module.min.js',
loader: '/vendor/three/0.160.0/addons/loaders/GLTFLoader.js',
skeleton: '/vendor/three/0.160.0/addons/utils/SkeletonUtils.js',
});
});
test('homepage picker labels policy-disabled 3D as unavailable, not purchasable', () => {
const { picker } = loadPickerWithRuntimeDisabled();
const modeButtons = picker.children[0].children;
const threeButton = modeButtons.find(button => button.textContent.startsWith('3D'));
expect(threeButton.textContent).toContain('⛔');
expect(threeButton.textContent).not.toContain('🔒');
expect(threeButton.title).toMatch(/unavailable on this server/i);
expect(threeButton.title).not.toMatch(/unlock with credits/i);
expect(threeButton.attributes['aria-disabled']).toBe('true');
});
test('homepage renderer selection immediately returns keyboard focus to gameplay', () => {
const { picker, focusGameplay } = loadPickerWithRuntimeDisabled();
const tiledButton = picker.children[0].children.find(button => button.textContent === 'Tiled');
tiledButton.onclick();
expect(focusGameplay).toHaveBeenCalledTimes(1);
});
test('server CSP trusts jsDelivr only when the renderer CDN flag is enabled', () => {
const serverSource = fs.readFileSync(
path.join(__dirname, '../src/index.js'),
'utf8',
);
expect(serverSource).toContain('RENDERER_CDN_ENABLED');
expect(serverSource).toMatch(/if \(rendererCdnEnabled\) \{[\s\S]*scriptSources\.push\('https:\/\/cdn\.jsdelivr\.net'\)/);
expect(serverSource).not.toContain("script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net");
});
test('browser pages use the pinned same-origin jQuery 3.7.1 asset', () => {
const asset = fs.readFileSync(path.join(__dirname, '../html/js/lib/jquery-3.7.1.min.js'));
const homepage = fs.readFileSync(path.join(__dirname, '../html/index.html'), 'utf8');
const debugPage = fs.readFileSync(path.join(__dirname, '../html/debug.html'), 'utf8');
expect(asset.toString('utf8', 0, 100)).toContain('jQuery v3.7.1');
expect(crypto.createHash('sha256').update(asset).digest('hex'))
.toBe('3289f31657004339ed678f16c4e626987e57303f2107577ed4973f6c9e105260');
expect(homepage).toContain('js/lib/jquery-3.7.1.min.js');
expect(debugPage).toContain('js/lib/jquery-3.7.1.min.js');
expect(homepage + debugPage).not.toContain('jquery-3.4.1');
});
test('server and renderer pages publish the exact same-origin Three.js build', () => {
const packageJson = require('../src/package.json');
const serverSource = fs.readFileSync(path.join(__dirname, '../src/index.js'), 'utf8');
const pages = ['index.html', 'tavern.html', 'match.html']
.map(name => fs.readFileSync(path.join(__dirname, '../html', name), 'utf8'))
.join('\n');
expect(packageJson.dependencies.three).toBe('0.160.0');
expect(serverSource).toContain("const rendererLocalVersion = '0.160.0'");
expect(serverSource).toContain("'examples/jsm'");
expect(serverSource).toContain("'node_modules/three'");
expect(pages.split('"three":"/vendor/three/0.160.0/three.module.min.js"'))
.toHaveLength(4);
});
});