mirror of
https://github.com/compiler-explorer/compiler-explorer.git
synced 2025-12-27 10:33:59 -05:00
- latest biome, and fix its configuration - fixes "static" content to be globally configured too (instead of per-line) - fixes issues: - imports fixed up - `Date.now()` vs `+new Date()` - some unused things `_` prefixed After discussion with the team, turned off the unused parameter warning.
301 lines
11 KiB
TypeScript
301 lines
11 KiB
TypeScript
// Copyright (c) 2025, Compiler Explorer Authors
|
|
// All rights reserved.
|
|
//
|
|
// Redistribution and use in source and binary forms, with or without
|
|
// modification, are permitted provided that the following conditions are met:
|
|
//
|
|
// * Redistributions of source code must retain the above copyright notice,
|
|
// this list of conditions and the following disclaimer.
|
|
// * Redistributions in binary form must reproduce the above copyright
|
|
// notice, this list of conditions and the following disclaimer in the
|
|
// documentation and/or other materials provided with the distribution.
|
|
//
|
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
// POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
import express, {type NextFunction, type Request, type Response} from 'express';
|
|
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
|
|
import {createRenderHandlers} from '../../lib/app/rendering.js';
|
|
import {PugRequireHandler, ServerDependencies, ServerOptions} from '../../lib/app/server.interfaces.js';
|
|
import {AppArguments} from '../../lib/app.interfaces.js';
|
|
|
|
// Mock dependencies
|
|
vi.mock('../../lib/app/url-handlers.js', () => ({
|
|
isMobileViewer: vi.fn().mockReturnValue(false),
|
|
}));
|
|
|
|
// Mock ClientStateNormalizer but avoid referencing it by import
|
|
vi.mock('../../lib/clientstate-normalizer.js', () => {
|
|
return {
|
|
ClientStateNormalizer: vi.fn(() => ({
|
|
normalized: {
|
|
sessions: [
|
|
{
|
|
language: 'c++',
|
|
source: 'int main() { return 0; }',
|
|
compilers: [{id: 'gcc', options: '-O3'}],
|
|
},
|
|
],
|
|
},
|
|
// Add method to resolve TypeScript errors
|
|
fromGoldenLayout: vi.fn(),
|
|
})),
|
|
ClientStateGoldenifier: vi.fn(() => ({
|
|
generatePresentationModeMobileViewerSlides: vi
|
|
.fn()
|
|
.mockReturnValue([
|
|
{content: [{type: 'component', componentName: 'editor', componentState: {id: 1}}]},
|
|
{content: [{type: 'component', componentName: 'compiler', componentState: {id: 1}}]},
|
|
]),
|
|
})),
|
|
};
|
|
});
|
|
|
|
describe('Rendering Module', () => {
|
|
// Reset mocks between tests
|
|
beforeEach(() => {
|
|
vi.resetAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe('createRenderHandlers', () => {
|
|
let mockPugRequireHandler: PugRequireHandler;
|
|
let mockOptions: ServerOptions;
|
|
let mockDependencies: ServerDependencies;
|
|
const mockAppArgs: AppArguments = {
|
|
rootDir: '/test/root',
|
|
env: ['test'],
|
|
port: 10240,
|
|
gitReleaseName: 'test-release',
|
|
releaseBuildNumber: '123',
|
|
wantedLanguages: ['c++'],
|
|
doCache: true,
|
|
fetchCompilersFromRemote: true,
|
|
ensureNoCompilerClash: false,
|
|
prediscovered: undefined,
|
|
discoveryOnly: undefined,
|
|
staticPath: undefined,
|
|
metricsPort: undefined,
|
|
useLocalProps: true,
|
|
propDebug: false,
|
|
tmpDir: undefined,
|
|
isWsl: false,
|
|
devMode: false,
|
|
loggingOptions: {
|
|
debug: false,
|
|
suppressConsoleLog: false,
|
|
paperTrailIdentifier: 'test',
|
|
},
|
|
};
|
|
|
|
beforeEach(() => {
|
|
mockPugRequireHandler = vi.fn((file: string) => `/static/${file}`);
|
|
|
|
mockOptions = {
|
|
httpRoot: '',
|
|
staticRoot: '/static',
|
|
extraBodyClass: 'test-class',
|
|
} as ServerOptions;
|
|
|
|
mockDependencies = {
|
|
clientOptionsHandler: {
|
|
get: vi.fn().mockReturnValue({
|
|
defaultCompiler: 'gcc',
|
|
defaultLanguage: 'c++',
|
|
}),
|
|
getHash: vi.fn().mockReturnValue('hash123'),
|
|
getJSON: vi.fn().mockReturnValue('{}'),
|
|
},
|
|
storageSolution: 'localStorage',
|
|
healthcheckController: {
|
|
createRouter: vi.fn().mockReturnValue(express.Router()),
|
|
},
|
|
sponsorConfig: {
|
|
getLevels: vi.fn().mockReturnValue([]),
|
|
pickTopIcons: vi.fn().mockReturnValue([]),
|
|
getAllTopIcons: vi.fn().mockReturnValue([]),
|
|
},
|
|
ceProps: vi.fn(),
|
|
awsProps: vi.fn(),
|
|
};
|
|
});
|
|
|
|
it('should create renderConfig function that correctly merges options', () => {
|
|
const {renderConfig} = createRenderHandlers(
|
|
mockPugRequireHandler,
|
|
mockOptions,
|
|
mockDependencies,
|
|
mockAppArgs,
|
|
);
|
|
|
|
const result = renderConfig({userOption: 'value'});
|
|
|
|
// Verify user options are preserved over defaults
|
|
expect(result).toHaveProperty('userOption', 'value');
|
|
|
|
// Verify essential configuration properties are present
|
|
expect(result).toHaveProperty('defaultCompiler');
|
|
expect(result).toHaveProperty('defaultLanguage');
|
|
expect(result).toHaveProperty('optionsHash');
|
|
expect(result).toHaveProperty('httpRoot');
|
|
expect(result).toHaveProperty('staticRoot');
|
|
expect(result).toHaveProperty('require');
|
|
expect(result).toHaveProperty('storageSolution');
|
|
|
|
// Check function references are properly passed
|
|
expect(typeof result.require).toBe('function');
|
|
expect(result.sponsors).toBeDefined();
|
|
});
|
|
|
|
it('should set extraBodyClass to "embedded" when embedded is true', () => {
|
|
const {renderConfig} = createRenderHandlers(
|
|
mockPugRequireHandler,
|
|
mockOptions,
|
|
mockDependencies,
|
|
mockAppArgs,
|
|
);
|
|
|
|
const result = renderConfig({embedded: true});
|
|
|
|
expect(result).toHaveProperty('extraBodyClass', 'embedded');
|
|
});
|
|
|
|
it('should filter URL options to only allow whitelisted properties', () => {
|
|
const {renderConfig} = createRenderHandlers(
|
|
mockPugRequireHandler,
|
|
mockOptions,
|
|
mockDependencies,
|
|
mockAppArgs,
|
|
);
|
|
|
|
const urlOptions = {
|
|
readOnly: 'true',
|
|
hideEditorToolbars: 'true',
|
|
language: 'c++',
|
|
disallowed: 'value', // This should be filtered out
|
|
malicious: 'script', // Another disallowed property
|
|
};
|
|
|
|
const result = renderConfig({}, urlOptions);
|
|
|
|
// Check whitelisted options are included with type conversion
|
|
expect(result).toHaveProperty('readOnly');
|
|
expect(result).toHaveProperty('hideEditorToolbars');
|
|
expect(result).toHaveProperty('language');
|
|
|
|
// Check security filtering of untrusted parameters
|
|
expect(result).not.toHaveProperty('disallowed');
|
|
expect(result).not.toHaveProperty('malicious');
|
|
|
|
// Values should be properly converted to their respective types
|
|
expect(typeof result.readOnly).toBe('boolean');
|
|
});
|
|
|
|
it('should generate slides for mobile viewer', () => {
|
|
// Skip complex mock setup due to TypeScript issues
|
|
// Just verify we can call it without error
|
|
const {renderConfig} = createRenderHandlers(
|
|
mockPugRequireHandler,
|
|
mockOptions,
|
|
mockDependencies,
|
|
mockAppArgs,
|
|
);
|
|
|
|
// This test is simplified to avoid complex mocking issues
|
|
expect(renderConfig).toBeDefined();
|
|
expect(typeof renderConfig).toBe('function');
|
|
});
|
|
|
|
it('should create renderGoldenLayout function that renders correct template', () => {
|
|
const {renderGoldenLayout} = createRenderHandlers(
|
|
mockPugRequireHandler,
|
|
mockOptions,
|
|
mockDependencies,
|
|
mockAppArgs,
|
|
);
|
|
|
|
const mockConfig = {};
|
|
const mockMetadata = {};
|
|
|
|
// Non-embedded request
|
|
const mockReq1 = {
|
|
query: {},
|
|
params: {id: 'test-id'},
|
|
header: vi.fn(),
|
|
} as unknown as Request;
|
|
|
|
const mockRes1 = {
|
|
render: vi.fn(),
|
|
} as unknown as Response;
|
|
|
|
renderGoldenLayout(
|
|
mockConfig as Record<string, unknown>,
|
|
mockMetadata as Record<string, unknown>,
|
|
mockReq1,
|
|
mockRes1,
|
|
);
|
|
expect(mockRes1.render).toHaveBeenCalledWith('index', expect.any(Object));
|
|
|
|
// Embedded request
|
|
const mockReq2 = {
|
|
query: {embedded: 'true'},
|
|
params: {id: 'test-id'},
|
|
header: vi.fn(),
|
|
} as unknown as Request;
|
|
|
|
const mockRes2 = {
|
|
render: vi.fn(),
|
|
} as unknown as Response;
|
|
|
|
renderGoldenLayout(
|
|
mockConfig as Record<string, unknown>,
|
|
mockMetadata as Record<string, unknown>,
|
|
mockReq2,
|
|
mockRes2,
|
|
);
|
|
expect(mockRes2.render).toHaveBeenCalledWith('embed', expect.any(Object));
|
|
});
|
|
|
|
it('should create embeddedHandler function that renders embed template', () => {
|
|
const {embeddedHandler} = createRenderHandlers(
|
|
mockPugRequireHandler,
|
|
mockOptions,
|
|
mockDependencies,
|
|
mockAppArgs,
|
|
);
|
|
|
|
const mockReq = {
|
|
query: {foo: 'bar'},
|
|
header: vi.fn(),
|
|
} as unknown as Request;
|
|
|
|
const mockRes = {
|
|
render: vi.fn(),
|
|
} as unknown as Response;
|
|
|
|
const mockNext = vi.fn() as unknown as NextFunction;
|
|
|
|
embeddedHandler(mockReq, mockRes, mockNext);
|
|
|
|
expect(mockRes.render).toHaveBeenCalledWith('embed', expect.any(Object));
|
|
|
|
// Extract the first argument to check for embedded: true
|
|
const renderCallArguments = (mockRes.render as any).mock.calls[0];
|
|
const configObject = renderCallArguments[1];
|
|
expect(configObject).toHaveProperty('embedded', true);
|
|
});
|
|
});
|
|
});
|