mirror of
https://github.com/game-ci/unity-builder.git
synced 2026-06-01 06:16:14 -07:00
test: add integration wiring and input parsing tests for enterprise features
Add three test files covering the two highest-priority gaps in PR #777: 1. src/index-enterprise-features.test.ts (21 tests) - Integration wiring tests for index.ts that verify conditional gating of all enterprise services (GitHooks, LocalCache, ChildWorkspace, SubmoduleProfile, LfsAgent). Tests that disabled features (default) are never invoked, enabled features call the correct service methods, and the order of operations is correct (restore before build, save after build). Also tests non-local provider strategy skips all enterprise features. 2. src/model/enterprise-inputs.test.ts (103 tests) - Input/BuildParameters wiring tests for all 20 new enterprise properties. Covers defaults, explicit values, and boolean string parsing edge cases (the #1 source of bugs: 'false' as truthy, 'TRUE' case sensitivity, '1', 'yes'). Verifies BuildParameters.create() correctly maps all Input getters. 3. src/model/orchestrator/services/submodule/submodule-profile-service.test.ts (5 new tests) - Command construction safety tests for execute(), documenting how paths, branches, and tokens are passed into git commands and verifying the expected command strings. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
606
src/index-enterprise-features.test.ts
Normal file
606
src/index-enterprise-features.test.ts
Normal file
@@ -0,0 +1,606 @@
|
||||
/**
|
||||
* Integration wiring tests for enterprise features in index.ts
|
||||
*
|
||||
* These tests verify the conditional gating logic in runMain():
|
||||
* - Each enterprise feature is only invoked when its gate condition is met
|
||||
* - Services are NOT called when their feature is disabled (the default)
|
||||
* - The order of operations is correct (restore before build, save after build)
|
||||
*/
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import { BuildParameters } from './model';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Service mocks — must be declared before importing index.ts (jest hoists them)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const mockChildWorkspaceService = {
|
||||
buildConfig: jest.fn().mockReturnValue({ enabled: true, workspaceName: 'Test' }),
|
||||
initializeWorkspace: jest.fn().mockReturnValue(false),
|
||||
getWorkspaceSize: jest.fn().mockReturnValue('0 B'),
|
||||
saveWorkspace: jest.fn(),
|
||||
};
|
||||
|
||||
const mockSubmoduleProfileService = {
|
||||
createInitPlan: jest.fn().mockResolvedValue([]),
|
||||
execute: jest.fn().mockResolvedValue(''),
|
||||
};
|
||||
|
||||
const mockLfsAgentService = {
|
||||
configure: jest.fn().mockResolvedValue(''),
|
||||
};
|
||||
|
||||
const mockLocalCacheService = {
|
||||
resolveCacheRoot: jest.fn().mockReturnValue('/cache'),
|
||||
generateCacheKey: jest.fn().mockReturnValue('key-1'),
|
||||
restoreLfsCache: jest.fn().mockResolvedValue(true),
|
||||
restoreLibraryCache: jest.fn().mockResolvedValue(true),
|
||||
saveLibraryCache: jest.fn().mockResolvedValue(''),
|
||||
saveLfsCache: jest.fn().mockResolvedValue(''),
|
||||
};
|
||||
|
||||
const mockGitHooksService = {
|
||||
installHooks: jest.fn().mockResolvedValue(''),
|
||||
configureSkipList: jest.fn().mockReturnValue({ LEFTHOOK_EXCLUDE: 'pre-commit' }),
|
||||
};
|
||||
|
||||
// Mock the dynamic import() targets — jest.mock with factory functions.
|
||||
// The services are imported dynamically via `await import(...)` in index.ts,
|
||||
// so we mock the module path and return the mock objects as named exports.
|
||||
jest.mock('./model/orchestrator/services/cache/child-workspace-service', () => ({
|
||||
ChildWorkspaceService: mockChildWorkspaceService,
|
||||
}));
|
||||
|
||||
jest.mock('./model/orchestrator/services/submodule/submodule-profile-service', () => ({
|
||||
SubmoduleProfileService: mockSubmoduleProfileService,
|
||||
}));
|
||||
|
||||
jest.mock('./model/orchestrator/services/lfs/lfs-agent-service', () => ({
|
||||
LfsAgentService: mockLfsAgentService,
|
||||
}));
|
||||
|
||||
jest.mock('./model/orchestrator/services/cache/local-cache-service', () => ({
|
||||
LocalCacheService: mockLocalCacheService,
|
||||
}));
|
||||
|
||||
jest.mock('./model/orchestrator/services/hooks/git-hooks-service', () => ({
|
||||
GitHooksService: mockGitHooksService,
|
||||
}));
|
||||
|
||||
// Mock all non-enterprise dependencies to isolate the wiring logic
|
||||
jest.mock('@actions/core');
|
||||
jest.mock('./model', () => ({
|
||||
Action: {
|
||||
checkCompatibility: jest.fn(),
|
||||
workspace: '/workspace',
|
||||
actionFolder: '/action',
|
||||
},
|
||||
BuildParameters: {
|
||||
create: jest.fn(),
|
||||
},
|
||||
Cache: {
|
||||
verify: jest.fn(),
|
||||
},
|
||||
Orchestrator: {
|
||||
run: jest.fn().mockResolvedValue(''),
|
||||
},
|
||||
Docker: {
|
||||
run: jest.fn().mockResolvedValue(0),
|
||||
},
|
||||
ImageTag: jest.fn().mockImplementation(() => ({
|
||||
toString: () => 'mock-image:latest',
|
||||
})),
|
||||
Output: {
|
||||
setBuildVersion: jest.fn().mockResolvedValue(''),
|
||||
setAndroidVersionCode: jest.fn().mockResolvedValue(''),
|
||||
setEngineExitCode: jest.fn().mockResolvedValue(''),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('./model/cli/cli', () => ({
|
||||
Cli: {
|
||||
InitCliMode: jest.fn().mockReturnValue(false),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('./model/mac-builder', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
run: jest.fn().mockResolvedValue(0),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('./model/platform-setup', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
setup: jest.fn().mockResolvedValue(''),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockedBuildParametersCreate = BuildParameters.create as jest.Mock;
|
||||
|
||||
interface EnterpriseBuildParametersOverrides {
|
||||
providerStrategy?: string;
|
||||
childWorkspacesEnabled?: boolean;
|
||||
childWorkspaceName?: string;
|
||||
childWorkspaceCacheRoot?: string;
|
||||
childWorkspacePreserveGit?: boolean;
|
||||
childWorkspaceSeparateLibrary?: boolean;
|
||||
submoduleProfilePath?: string;
|
||||
submoduleVariantPath?: string;
|
||||
submoduleToken?: string;
|
||||
gitPrivateToken?: string;
|
||||
lfsTransferAgent?: string;
|
||||
lfsTransferAgentArgs?: string;
|
||||
lfsStoragePaths?: string;
|
||||
localCacheEnabled?: boolean;
|
||||
localCacheRoot?: string;
|
||||
localCacheLibrary?: boolean;
|
||||
localCacheLfs?: boolean;
|
||||
gitHooksEnabled?: boolean;
|
||||
gitHooksSkipList?: string;
|
||||
gitHooksRunBeforeBuild?: string;
|
||||
}
|
||||
|
||||
function createMockBuildParameters(overrides: EnterpriseBuildParametersOverrides = {}) {
|
||||
return {
|
||||
// Required base properties
|
||||
providerStrategy: 'local',
|
||||
targetPlatform: 'StandaloneLinux64',
|
||||
editorVersion: '2021.3.1f1',
|
||||
buildVersion: '1.0.0',
|
||||
androidVersionCode: '1',
|
||||
projectPath: '.',
|
||||
branch: 'main',
|
||||
runnerTempPath: '/tmp',
|
||||
|
||||
// Enterprise features - all disabled by default
|
||||
childWorkspacesEnabled: false,
|
||||
childWorkspaceName: '',
|
||||
childWorkspaceCacheRoot: '',
|
||||
childWorkspacePreserveGit: true,
|
||||
childWorkspaceSeparateLibrary: true,
|
||||
submoduleProfilePath: '',
|
||||
submoduleVariantPath: '',
|
||||
submoduleToken: '',
|
||||
gitPrivateToken: '',
|
||||
lfsTransferAgent: '',
|
||||
lfsTransferAgentArgs: '',
|
||||
lfsStoragePaths: '',
|
||||
localCacheEnabled: false,
|
||||
localCacheRoot: '',
|
||||
localCacheLibrary: true,
|
||||
localCacheLfs: false,
|
||||
gitHooksEnabled: false,
|
||||
gitHooksSkipList: '',
|
||||
gitHooksRunBeforeBuild: '',
|
||||
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The entry point (runMain) is invoked by importing index.ts.
|
||||
* Since it calls `runMain()` at module scope, we need to re-import it
|
||||
* for each test. jest.isolateModules() handles this.
|
||||
*/
|
||||
async function runIndex(overrides: EnterpriseBuildParametersOverrides = {}): Promise<void> {
|
||||
mockedBuildParametersCreate.mockResolvedValue(createMockBuildParameters(overrides));
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
jest.isolateModules(() => {
|
||||
require('./index');
|
||||
|
||||
// runMain() is async; give it a tick to complete
|
||||
// We use setImmediate to ensure all microtasks from the dynamic imports resolve
|
||||
});
|
||||
|
||||
// Allow all promises and microtasks to settle
|
||||
setTimeout(resolve, 100);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('index.ts enterprise feature wiring', () => {
|
||||
const originalPlatform = process.platform;
|
||||
const originalEnvironment = { ...process.env };
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
process.env.GITHUB_WORKSPACE = '/workspace';
|
||||
|
||||
// Force linux platform so Docker.run is used (not MacBuilder)
|
||||
Object.defineProperty(process, 'platform', { value: 'linux' });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
||||
process.env = { ...originalEnvironment };
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// GitHooksService gating
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
describe('GitHooksService gating', () => {
|
||||
it('should NOT call GitHooksService when gitHooksEnabled is false (default)', async () => {
|
||||
await runIndex({ gitHooksEnabled: false });
|
||||
|
||||
expect(mockGitHooksService.installHooks).not.toHaveBeenCalled();
|
||||
expect(mockGitHooksService.configureSkipList).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call installHooks when gitHooksEnabled is true', async () => {
|
||||
await runIndex({ gitHooksEnabled: true });
|
||||
|
||||
expect(mockGitHooksService.installHooks).toHaveBeenCalledWith('/workspace');
|
||||
});
|
||||
|
||||
it('should call configureSkipList when gitHooksEnabled and gitHooksSkipList is set', async () => {
|
||||
await runIndex({
|
||||
gitHooksEnabled: true,
|
||||
gitHooksSkipList: 'pre-commit,pre-push',
|
||||
});
|
||||
|
||||
expect(mockGitHooksService.configureSkipList).toHaveBeenCalledWith(['pre-commit', 'pre-push']);
|
||||
});
|
||||
|
||||
it('should NOT call configureSkipList when gitHooksSkipList is empty', async () => {
|
||||
await runIndex({
|
||||
gitHooksEnabled: true,
|
||||
gitHooksSkipList: '',
|
||||
});
|
||||
|
||||
expect(mockGitHooksService.installHooks).toHaveBeenCalled();
|
||||
expect(mockGitHooksService.configureSkipList).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// LocalCacheService gating
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
describe('LocalCacheService gating', () => {
|
||||
it('should NOT call LocalCacheService when localCacheEnabled is false (default)', async () => {
|
||||
await runIndex({ localCacheEnabled: false });
|
||||
|
||||
expect(mockLocalCacheService.resolveCacheRoot).not.toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.generateCacheKey).not.toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.restoreLibraryCache).not.toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.restoreLfsCache).not.toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.saveLibraryCache).not.toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.saveLfsCache).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call restore and save operations when localCacheEnabled is true', async () => {
|
||||
await runIndex({
|
||||
localCacheEnabled: true,
|
||||
localCacheLibrary: true,
|
||||
localCacheLfs: true,
|
||||
});
|
||||
|
||||
expect(mockLocalCacheService.resolveCacheRoot).toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.generateCacheKey).toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.restoreLibraryCache).toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.restoreLfsCache).toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.saveLibraryCache).toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.saveLfsCache).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should only cache Library when localCacheLibrary is true and localCacheLfs is false', async () => {
|
||||
await runIndex({
|
||||
localCacheEnabled: true,
|
||||
localCacheLibrary: true,
|
||||
localCacheLfs: false,
|
||||
});
|
||||
|
||||
expect(mockLocalCacheService.restoreLibraryCache).toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.restoreLfsCache).not.toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.saveLibraryCache).toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.saveLfsCache).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should only cache LFS when localCacheLfs is true and localCacheLibrary is false', async () => {
|
||||
await runIndex({
|
||||
localCacheEnabled: true,
|
||||
localCacheLibrary: false,
|
||||
localCacheLfs: true,
|
||||
});
|
||||
|
||||
expect(mockLocalCacheService.restoreLibraryCache).not.toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.restoreLfsCache).toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.saveLibraryCache).not.toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.saveLfsCache).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// ChildWorkspaceService gating
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
describe('ChildWorkspaceService gating', () => {
|
||||
it('should NOT call ChildWorkspaceService when childWorkspacesEnabled is false (default)', async () => {
|
||||
await runIndex({ childWorkspacesEnabled: false });
|
||||
|
||||
expect(mockChildWorkspaceService.buildConfig).not.toHaveBeenCalled();
|
||||
expect(mockChildWorkspaceService.initializeWorkspace).not.toHaveBeenCalled();
|
||||
expect(mockChildWorkspaceService.saveWorkspace).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should NOT call ChildWorkspaceService when childWorkspacesEnabled is true but childWorkspaceName is empty', async () => {
|
||||
await runIndex({
|
||||
childWorkspacesEnabled: true,
|
||||
childWorkspaceName: '',
|
||||
});
|
||||
|
||||
expect(mockChildWorkspaceService.buildConfig).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call buildConfig, initializeWorkspace, and saveWorkspace when enabled with a name', async () => {
|
||||
mockChildWorkspaceService.buildConfig.mockReturnValue({ enabled: true, workspaceName: 'TurnOfWar' });
|
||||
|
||||
await runIndex({
|
||||
childWorkspacesEnabled: true,
|
||||
childWorkspaceName: 'TurnOfWar',
|
||||
childWorkspaceCacheRoot: '/cache/workspaces',
|
||||
});
|
||||
|
||||
expect(mockChildWorkspaceService.buildConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
childWorkspacesEnabled: true,
|
||||
childWorkspaceName: 'TurnOfWar',
|
||||
}),
|
||||
);
|
||||
expect(mockChildWorkspaceService.initializeWorkspace).toHaveBeenCalled();
|
||||
expect(mockChildWorkspaceService.getWorkspaceSize).toHaveBeenCalled();
|
||||
expect(mockChildWorkspaceService.saveWorkspace).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// SubmoduleProfileService gating
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
describe('SubmoduleProfileService gating', () => {
|
||||
it('should NOT call SubmoduleProfileService when submoduleProfilePath is empty (default)', async () => {
|
||||
await runIndex({ submoduleProfilePath: '' });
|
||||
|
||||
expect(mockSubmoduleProfileService.createInitPlan).not.toHaveBeenCalled();
|
||||
expect(mockSubmoduleProfileService.execute).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call createInitPlan and execute when submoduleProfilePath is set', async () => {
|
||||
await runIndex({
|
||||
submoduleProfilePath: '/path/to/profile.yml',
|
||||
submoduleVariantPath: '',
|
||||
submoduleToken: 'my-token',
|
||||
});
|
||||
|
||||
expect(mockSubmoduleProfileService.createInitPlan).toHaveBeenCalledWith('/path/to/profile.yml', '', '/workspace');
|
||||
expect(mockSubmoduleProfileService.execute).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should pass variant path when provided', async () => {
|
||||
await runIndex({
|
||||
submoduleProfilePath: '/path/to/profile.yml',
|
||||
submoduleVariantPath: '/path/to/variant.yml',
|
||||
});
|
||||
|
||||
expect(mockSubmoduleProfileService.createInitPlan).toHaveBeenCalledWith(
|
||||
'/path/to/profile.yml',
|
||||
'/path/to/variant.yml',
|
||||
'/workspace',
|
||||
);
|
||||
});
|
||||
|
||||
it('should use submoduleToken for auth, falling back to gitPrivateToken', async () => {
|
||||
await runIndex({
|
||||
submoduleProfilePath: '/path/to/profile.yml',
|
||||
submoduleToken: '',
|
||||
gitPrivateToken: 'fallback-token',
|
||||
});
|
||||
|
||||
expect(mockSubmoduleProfileService.execute).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
'/workspace',
|
||||
'fallback-token',
|
||||
);
|
||||
});
|
||||
|
||||
it('should prefer submoduleToken over gitPrivateToken', async () => {
|
||||
await runIndex({
|
||||
submoduleProfilePath: '/path/to/profile.yml',
|
||||
submoduleToken: 'specific-token',
|
||||
gitPrivateToken: 'fallback-token',
|
||||
});
|
||||
|
||||
expect(mockSubmoduleProfileService.execute).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
'/workspace',
|
||||
'specific-token',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// LfsAgentService gating
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
describe('LfsAgentService gating', () => {
|
||||
it('should NOT call LfsAgentService when lfsTransferAgent is empty (default)', async () => {
|
||||
await runIndex({ lfsTransferAgent: '' });
|
||||
|
||||
expect(mockLfsAgentService.configure).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call configure when lfsTransferAgent is set', async () => {
|
||||
await runIndex({
|
||||
lfsTransferAgent: '/tools/elastic-git-storage',
|
||||
lfsTransferAgentArgs: '--verbose',
|
||||
lfsStoragePaths: '/path/a;/path/b',
|
||||
});
|
||||
|
||||
expect(mockLfsAgentService.configure).toHaveBeenCalledWith(
|
||||
'/tools/elastic-git-storage',
|
||||
'--verbose',
|
||||
['/path/a', '/path/b'],
|
||||
'/workspace',
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass empty array when lfsStoragePaths is empty', async () => {
|
||||
await runIndex({
|
||||
lfsTransferAgent: '/tools/agent',
|
||||
lfsStoragePaths: '',
|
||||
});
|
||||
|
||||
expect(mockLfsAgentService.configure).toHaveBeenCalledWith('/tools/agent', '', [], '/workspace');
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Order of operations (restore before build, save after build)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
describe('order of operations', () => {
|
||||
it('should execute restore operations before build and save operations after build', async () => {
|
||||
const callOrder: string[] = [];
|
||||
|
||||
// Track call order for each relevant operation
|
||||
mockChildWorkspaceService.buildConfig.mockReturnValue({ enabled: true, workspaceName: 'Test' });
|
||||
mockChildWorkspaceService.initializeWorkspace.mockImplementation(() => {
|
||||
callOrder.push('child-workspace-restore');
|
||||
|
||||
return false;
|
||||
});
|
||||
mockChildWorkspaceService.getWorkspaceSize.mockImplementation(() => {
|
||||
callOrder.push('child-workspace-size');
|
||||
|
||||
return '0 B';
|
||||
});
|
||||
mockSubmoduleProfileService.createInitPlan.mockImplementation(async () => {
|
||||
callOrder.push('submodule-profile-plan');
|
||||
|
||||
return [];
|
||||
});
|
||||
mockSubmoduleProfileService.execute.mockImplementation(async () => {
|
||||
callOrder.push('submodule-profile-execute');
|
||||
});
|
||||
mockLfsAgentService.configure.mockImplementation(async () => {
|
||||
callOrder.push('lfs-agent-configure');
|
||||
});
|
||||
mockLocalCacheService.resolveCacheRoot.mockImplementation(() => {
|
||||
callOrder.push('local-cache-resolve');
|
||||
|
||||
return '/cache';
|
||||
});
|
||||
mockLocalCacheService.generateCacheKey.mockImplementation(() => {
|
||||
callOrder.push('local-cache-keygen');
|
||||
|
||||
return 'key-1';
|
||||
});
|
||||
mockLocalCacheService.restoreLfsCache.mockImplementation(async () => {
|
||||
callOrder.push('local-cache-restore-lfs');
|
||||
|
||||
return true;
|
||||
});
|
||||
mockLocalCacheService.restoreLibraryCache.mockImplementation(async () => {
|
||||
callOrder.push('local-cache-restore-library');
|
||||
|
||||
return true;
|
||||
});
|
||||
mockGitHooksService.installHooks.mockImplementation(async () => {
|
||||
callOrder.push('git-hooks-install');
|
||||
});
|
||||
mockLocalCacheService.saveLibraryCache.mockImplementation(async () => {
|
||||
callOrder.push('local-cache-save-library');
|
||||
});
|
||||
mockLocalCacheService.saveLfsCache.mockImplementation(async () => {
|
||||
callOrder.push('local-cache-save-lfs');
|
||||
});
|
||||
mockChildWorkspaceService.saveWorkspace.mockImplementation(() => {
|
||||
callOrder.push('child-workspace-save');
|
||||
});
|
||||
|
||||
await runIndex({
|
||||
childWorkspacesEnabled: true,
|
||||
childWorkspaceName: 'TurnOfWar',
|
||||
submoduleProfilePath: '/profile.yml',
|
||||
lfsTransferAgent: '/tools/agent',
|
||||
localCacheEnabled: true,
|
||||
localCacheLfs: true,
|
||||
localCacheLibrary: true,
|
||||
gitHooksEnabled: true,
|
||||
});
|
||||
|
||||
// Verify restore operations happen before save operations.
|
||||
// The expected order from index.ts is:
|
||||
// 1. Child workspace restore
|
||||
// 2. Submodule profile init
|
||||
// 3. LFS agent configure
|
||||
// 4. Local cache restore (LFS then Library)
|
||||
// 5. Git hooks install
|
||||
// 6. [BUILD happens here - Docker.run or MacBuilder.run]
|
||||
// 7. Local cache save (Library then LFS)
|
||||
// 8. Child workspace save
|
||||
|
||||
const restoreOps = [
|
||||
'child-workspace-restore',
|
||||
'submodule-profile-plan',
|
||||
'submodule-profile-execute',
|
||||
'lfs-agent-configure',
|
||||
'local-cache-restore-lfs',
|
||||
'local-cache-restore-library',
|
||||
'git-hooks-install',
|
||||
];
|
||||
|
||||
const saveOps = ['local-cache-save-library', 'local-cache-save-lfs', 'child-workspace-save'];
|
||||
|
||||
// All restore ops should appear before all save ops
|
||||
for (const restoreOp of restoreOps) {
|
||||
if (!callOrder.includes(restoreOp)) continue; // Skip if the operation wasn't called
|
||||
for (const saveOp of saveOps) {
|
||||
if (!callOrder.includes(saveOp)) continue;
|
||||
expect(callOrder.indexOf(restoreOp)).toBeLessThan(callOrder.indexOf(saveOp));
|
||||
}
|
||||
}
|
||||
|
||||
// Child workspace save should be last
|
||||
if (callOrder.includes('child-workspace-save') && callOrder.includes('local-cache-save-lfs')) {
|
||||
expect(callOrder.indexOf('local-cache-save-lfs')).toBeLessThan(callOrder.indexOf('child-workspace-save'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Non-local provider strategy
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
describe('non-local provider strategy', () => {
|
||||
it('should skip all enterprise features when providerStrategy is not local', async () => {
|
||||
await runIndex({
|
||||
providerStrategy: 'aws',
|
||||
childWorkspacesEnabled: true,
|
||||
childWorkspaceName: 'Test',
|
||||
submoduleProfilePath: '/profile.yml',
|
||||
lfsTransferAgent: '/tools/agent',
|
||||
localCacheEnabled: true,
|
||||
gitHooksEnabled: true,
|
||||
});
|
||||
|
||||
// None of the enterprise services should be called because
|
||||
// they are inside the `if (providerStrategy === 'local')` block
|
||||
expect(mockChildWorkspaceService.buildConfig).not.toHaveBeenCalled();
|
||||
expect(mockSubmoduleProfileService.createInitPlan).not.toHaveBeenCalled();
|
||||
expect(mockLfsAgentService.configure).not.toHaveBeenCalled();
|
||||
expect(mockLocalCacheService.resolveCacheRoot).not.toHaveBeenCalled();
|
||||
expect(mockGitHooksService.installHooks).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
509
src/model/enterprise-inputs.test.ts
Normal file
509
src/model/enterprise-inputs.test.ts
Normal file
@@ -0,0 +1,509 @@
|
||||
/**
|
||||
* Tests for enterprise input properties and their wiring into BuildParameters.
|
||||
*
|
||||
* Covers all 20 new input properties added for enterprise features:
|
||||
* - Boolean inputs: localCacheEnabled, childWorkspacesEnabled, gitHooksEnabled,
|
||||
* localCacheLibrary, localCacheLfs, childWorkspacePreserveGit, childWorkspaceSeparateLibrary
|
||||
* - String inputs: submoduleProfilePath, submoduleVariantPath, submoduleToken,
|
||||
* localCacheRoot, childWorkspaceName, childWorkspaceCacheRoot, lfsTransferAgent,
|
||||
* lfsTransferAgentArgs, lfsStoragePaths, providerExecutable, gitHooksSkipList,
|
||||
* gitHooksRunBeforeBuild
|
||||
*
|
||||
* Special attention to boolean inputs: GitHub Actions always passes inputs as strings,
|
||||
* so 'false' must NOT evaluate as truthy (the #1 source of bugs).
|
||||
*/
|
||||
|
||||
import * as core from '@actions/core';
|
||||
import Input from './input';
|
||||
import Versioning from './versioning';
|
||||
import BuildParameters from './build-parameters';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Setup
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Part 1: Input getters — defaults and explicit values
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('Enterprise Input properties', () => {
|
||||
// -----------------------------------------------------------------------
|
||||
// Boolean inputs — default and string parsing
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
describe('localCacheEnabled', () => {
|
||||
it('returns false by default', () => {
|
||||
expect(Input.localCacheEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when string "true" is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('true');
|
||||
expect(Input.localCacheEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when string "false" is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('false');
|
||||
expect(Input.localCacheEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when empty string is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('');
|
||||
expect(Input.localCacheEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('localCacheLibrary', () => {
|
||||
it('returns true by default (library caching on by default when cache enabled)', () => {
|
||||
expect(Input.localCacheLibrary).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when string "true" is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('true');
|
||||
expect(Input.localCacheLibrary).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when string "false" is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('false');
|
||||
expect(Input.localCacheLibrary).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('localCacheLfs', () => {
|
||||
it('returns false by default', () => {
|
||||
expect(Input.localCacheLfs).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when string "true" is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('true');
|
||||
expect(Input.localCacheLfs).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when string "false" is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('false');
|
||||
expect(Input.localCacheLfs).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('childWorkspacesEnabled', () => {
|
||||
it('returns false by default', () => {
|
||||
expect(Input.childWorkspacesEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when string "true" is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('true');
|
||||
expect(Input.childWorkspacesEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when string "false" is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('false');
|
||||
expect(Input.childWorkspacesEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when empty string is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('');
|
||||
expect(Input.childWorkspacesEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('childWorkspacePreserveGit', () => {
|
||||
it('returns true by default', () => {
|
||||
expect(Input.childWorkspacePreserveGit).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when string "false" is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('false');
|
||||
expect(Input.childWorkspacePreserveGit).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when string "true" is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('true');
|
||||
expect(Input.childWorkspacePreserveGit).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('childWorkspaceSeparateLibrary', () => {
|
||||
it('returns true by default', () => {
|
||||
expect(Input.childWorkspaceSeparateLibrary).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when string "false" is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('false');
|
||||
expect(Input.childWorkspaceSeparateLibrary).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when string "true" is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('true');
|
||||
expect(Input.childWorkspaceSeparateLibrary).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('gitHooksEnabled', () => {
|
||||
it('returns false by default', () => {
|
||||
expect(Input.gitHooksEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when string "true" is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('true');
|
||||
expect(Input.gitHooksEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when string "false" is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('false');
|
||||
expect(Input.gitHooksEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when empty string is passed', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('');
|
||||
expect(Input.gitHooksEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Boolean truthiness edge cases — the #1 source of bugs
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
describe('boolean input string handling (edge cases)', () => {
|
||||
// These tests verify that the === 'true' comparison is correct.
|
||||
// In JavaScript, 'false' is truthy when used in a boolean context,
|
||||
// but the Input class correctly uses === 'true' comparison.
|
||||
|
||||
const booleanInputs: Array<{
|
||||
name: string;
|
||||
getter: () => boolean;
|
||||
defaultValue: boolean;
|
||||
}> = [
|
||||
{ name: 'localCacheEnabled', getter: () => Input.localCacheEnabled, defaultValue: false },
|
||||
{ name: 'localCacheLfs', getter: () => Input.localCacheLfs, defaultValue: false },
|
||||
{ name: 'childWorkspacesEnabled', getter: () => Input.childWorkspacesEnabled, defaultValue: false },
|
||||
{ name: 'gitHooksEnabled', getter: () => Input.gitHooksEnabled, defaultValue: false },
|
||||
|
||||
// These default to true:
|
||||
{ name: 'localCacheLibrary', getter: () => Input.localCacheLibrary, defaultValue: true },
|
||||
{ name: 'childWorkspacePreserveGit', getter: () => Input.childWorkspacePreserveGit, defaultValue: true },
|
||||
{ name: 'childWorkspaceSeparateLibrary', getter: () => Input.childWorkspaceSeparateLibrary, defaultValue: true },
|
||||
];
|
||||
|
||||
test.each(booleanInputs)('$name: "false" string does NOT evaluate as truthy', ({ getter }) => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('false');
|
||||
expect(getter()).toBe(false);
|
||||
});
|
||||
|
||||
test.each(booleanInputs)('$name: "true" string evaluates as truthy', ({ getter }) => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('true');
|
||||
expect(getter()).toBe(true);
|
||||
});
|
||||
|
||||
test.each(booleanInputs)('$name: "TRUE" (uppercase) does NOT evaluate as true (case sensitive)', ({ getter }) => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('TRUE');
|
||||
expect(getter()).toBe(false);
|
||||
});
|
||||
|
||||
test.each(booleanInputs)('$name: "1" does NOT evaluate as true', ({ getter }) => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('1');
|
||||
expect(getter()).toBe(false);
|
||||
});
|
||||
|
||||
test.each(booleanInputs)('$name: "yes" does NOT evaluate as true', ({ getter }) => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('yes');
|
||||
expect(getter()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// String inputs — defaults and explicit values
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
describe('submoduleProfilePath', () => {
|
||||
it('returns empty string by default', () => {
|
||||
expect(Input.submoduleProfilePath).toBe('');
|
||||
});
|
||||
|
||||
it('takes input from workflow', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('config/submodule-profiles/tow/ec/profile.yml');
|
||||
expect(Input.submoduleProfilePath).toBe('config/submodule-profiles/tow/ec/profile.yml');
|
||||
});
|
||||
});
|
||||
|
||||
describe('submoduleVariantPath', () => {
|
||||
it('returns empty string by default', () => {
|
||||
expect(Input.submoduleVariantPath).toBe('');
|
||||
});
|
||||
|
||||
it('takes input from workflow', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('config/submodule-profiles/tow/ec/server.yml');
|
||||
expect(Input.submoduleVariantPath).toBe('config/submodule-profiles/tow/ec/server.yml');
|
||||
});
|
||||
});
|
||||
|
||||
describe('submoduleToken', () => {
|
||||
it('returns empty string by default', () => {
|
||||
expect(Input.submoduleToken).toBe('');
|
||||
});
|
||||
|
||||
it('takes input from workflow', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('ghp_abc123');
|
||||
expect(Input.submoduleToken).toBe('ghp_abc123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('localCacheRoot', () => {
|
||||
it('returns empty string by default', () => {
|
||||
expect(Input.localCacheRoot).toBe('');
|
||||
});
|
||||
|
||||
it('takes input from workflow', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('/d/cache/game-ci');
|
||||
expect(Input.localCacheRoot).toBe('/d/cache/game-ci');
|
||||
});
|
||||
});
|
||||
|
||||
describe('childWorkspaceName', () => {
|
||||
it('returns empty string by default', () => {
|
||||
expect(Input.childWorkspaceName).toBe('');
|
||||
});
|
||||
|
||||
it('takes input from workflow', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('TurnOfWarEndlessCrusade');
|
||||
expect(Input.childWorkspaceName).toBe('TurnOfWarEndlessCrusade');
|
||||
});
|
||||
});
|
||||
|
||||
describe('childWorkspaceCacheRoot', () => {
|
||||
it('returns empty string by default', () => {
|
||||
expect(Input.childWorkspaceCacheRoot).toBe('');
|
||||
});
|
||||
|
||||
it('takes input from workflow', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('/d/workspaces');
|
||||
expect(Input.childWorkspaceCacheRoot).toBe('/d/workspaces');
|
||||
});
|
||||
});
|
||||
|
||||
describe('lfsTransferAgent', () => {
|
||||
it('returns empty string by default', () => {
|
||||
expect(Input.lfsTransferAgent).toBe('');
|
||||
});
|
||||
|
||||
it('takes input from workflow', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('/tools/elastic-git-storage');
|
||||
expect(Input.lfsTransferAgent).toBe('/tools/elastic-git-storage');
|
||||
});
|
||||
});
|
||||
|
||||
describe('lfsTransferAgentArgs', () => {
|
||||
it('returns empty string by default', () => {
|
||||
expect(Input.lfsTransferAgentArgs).toBe('');
|
||||
});
|
||||
|
||||
it('takes input from workflow', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('--verbose --timeout=60');
|
||||
expect(Input.lfsTransferAgentArgs).toBe('--verbose --timeout=60');
|
||||
});
|
||||
});
|
||||
|
||||
describe('lfsStoragePaths', () => {
|
||||
it('returns empty string by default', () => {
|
||||
expect(Input.lfsStoragePaths).toBe('');
|
||||
});
|
||||
|
||||
it('takes input from workflow', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('/storage/primary;/storage/secondary');
|
||||
expect(Input.lfsStoragePaths).toBe('/storage/primary;/storage/secondary');
|
||||
});
|
||||
});
|
||||
|
||||
describe('providerExecutable', () => {
|
||||
it('returns empty string by default', () => {
|
||||
expect(Input.providerExecutable).toBe('');
|
||||
});
|
||||
|
||||
it('takes input from workflow', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('/usr/local/bin/custom-provider');
|
||||
expect(Input.providerExecutable).toBe('/usr/local/bin/custom-provider');
|
||||
});
|
||||
});
|
||||
|
||||
describe('gitHooksSkipList', () => {
|
||||
it('returns empty string by default', () => {
|
||||
expect(Input.gitHooksSkipList).toBe('');
|
||||
});
|
||||
|
||||
it('takes input from workflow', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('pre-commit,pre-push');
|
||||
expect(Input.gitHooksSkipList).toBe('pre-commit,pre-push');
|
||||
});
|
||||
});
|
||||
|
||||
describe('gitHooksRunBeforeBuild', () => {
|
||||
it('returns empty string by default', () => {
|
||||
expect(Input.gitHooksRunBeforeBuild).toBe('');
|
||||
});
|
||||
|
||||
it('takes input from workflow', () => {
|
||||
jest.spyOn(core, 'getInput').mockReturnValue('pre-commit');
|
||||
expect(Input.gitHooksRunBeforeBuild).toBe('pre-commit');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Part 2: BuildParameters.create() maps new inputs to properties
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const testLicense =
|
||||
'<?xml version="1.0" encoding="UTF-8"?><root>\n <License id="Terms">\n <MachineBindings>\n <Binding Key="1" Value="576562626572264761624c65526f7578"/>\n <Binding Key="2" Value="576562626572264761624c65526f7578"/>\n </MachineBindings>\n <MachineID Value="D7nTUnjNAmtsUMcnoyrqkgIbYdM="/>\n <SerialHash Value="2033b8ac3e6faa3742ca9f0bfae44d18f2a96b80"/>\n <Features>\n <Feature Value="33"/>\n <Feature Value="1"/>\n <Feature Value="12"/>\n <Feature Value="2"/>\n <Feature Value="24"/>\n <Feature Value="3"/>\n <Feature Value="36"/>\n <Feature Value="17"/>\n <Feature Value="19"/>\n <Feature Value="62"/>\n </Features>\n <DeveloperData Value="AQAAAEY0LUJHUlgtWEQ0RS1aQ1dWLUM1SlctR0RIQg=="/>\n <SerialMasked Value="F4-BGRX-XD4E-ZCWV-C5JW-XXXX"/>\n <StartDate Value="2021-02-08T00:00:00"/>\n <UpdateDate Value="2021-02-09T00:34:57"/>\n <InitialActivationDate Value="2021-02-08T00:34:56"/>\n <LicenseVersion Value="6.x"/>\n <ClientProvidedVersion Value="2018.4.30f1"/>\n <AlwaysOnline Value="false"/>\n <Entitlements>\n <Entitlement Ns="unity_editor" Tag="UnityPersonal" Type="EDITOR" ValidTo="9999-12-31T00:00:00"/>\n <Entitlement Ns="unity_editor" Tag="DarkSkin" Type="EDITOR_FEATURE" ValidTo="9999-12-31T00:00:00"/>\n </Entitlements>\n </License>\n<Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"/><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><Reference URI="#Terms"><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><DigestValue>m0Db8UK+ktnOLJBtHybkfetpcKo=</DigestValue></Reference></SignedInfo><SignatureValue>o/pUbSQAukz7+ZYAWhnA0AJbIlyyCPL7bKVEM2lVqbrXt7cyey+umkCXamuOgsWPVUKBMkXtMH8L\n5etLmD0getWIhTGhzOnDCk+gtIPfL4jMo9tkEuOCROQAXCci23VFscKcrkB+3X6h4wEOtA2APhOY\nB+wvC794o8/82ffjP79aVAi57rp3Wmzx+9pe9yMwoJuljAy2sc2tIMgdQGWVmOGBpQm3JqsidyzI\nJWG2kjnc7pDXK9pwYzXoKiqUqqrut90d+kQqRyv7MSZXR50HFqD/LI69h68b7P8Bjo3bPXOhNXGR\n9YCoemH6EkfCJxp2gIjzjWW+l2Hj2EsFQi8YXw==</SignatureValue></Signature></root>';
|
||||
|
||||
describe('BuildParameters.create() enterprise property mapping', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(Versioning, 'determineBuildVersion').mockImplementation(async () => '1.3.37');
|
||||
process.env.UNITY_LICENSE = testLicense;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('maps submoduleProfilePath from Input', async () => {
|
||||
jest.spyOn(Input, 'submoduleProfilePath', 'get').mockReturnValue('/path/to/profile.yml');
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.submoduleProfilePath).toBe('/path/to/profile.yml');
|
||||
});
|
||||
|
||||
it('maps submoduleVariantPath from Input', async () => {
|
||||
jest.spyOn(Input, 'submoduleVariantPath', 'get').mockReturnValue('/path/to/variant.yml');
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.submoduleVariantPath).toBe('/path/to/variant.yml');
|
||||
});
|
||||
|
||||
it('maps submoduleToken from Input', async () => {
|
||||
jest.spyOn(Input, 'submoduleToken', 'get').mockReturnValue('ghp_token123');
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.submoduleToken).toBe('ghp_token123');
|
||||
});
|
||||
|
||||
it('maps localCacheEnabled from Input', async () => {
|
||||
jest.spyOn(Input, 'localCacheEnabled', 'get').mockReturnValue(true);
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.localCacheEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('maps localCacheRoot from Input', async () => {
|
||||
jest.spyOn(Input, 'localCacheRoot', 'get').mockReturnValue('/d/cache');
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.localCacheRoot).toBe('/d/cache');
|
||||
});
|
||||
|
||||
it('maps localCacheLibrary from Input', async () => {
|
||||
jest.spyOn(Input, 'localCacheLibrary', 'get').mockReturnValue(false);
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.localCacheLibrary).toBe(false);
|
||||
});
|
||||
|
||||
it('maps localCacheLfs from Input', async () => {
|
||||
jest.spyOn(Input, 'localCacheLfs', 'get').mockReturnValue(true);
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.localCacheLfs).toBe(true);
|
||||
});
|
||||
|
||||
it('maps childWorkspacesEnabled from Input', async () => {
|
||||
jest.spyOn(Input, 'childWorkspacesEnabled', 'get').mockReturnValue(true);
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.childWorkspacesEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('maps childWorkspaceName from Input', async () => {
|
||||
jest.spyOn(Input, 'childWorkspaceName', 'get').mockReturnValue('TurnOfWar');
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.childWorkspaceName).toBe('TurnOfWar');
|
||||
});
|
||||
|
||||
it('maps childWorkspaceCacheRoot from Input', async () => {
|
||||
jest.spyOn(Input, 'childWorkspaceCacheRoot', 'get').mockReturnValue('/cache/workspaces');
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.childWorkspaceCacheRoot).toBe('/cache/workspaces');
|
||||
});
|
||||
|
||||
it('maps childWorkspacePreserveGit from Input', async () => {
|
||||
jest.spyOn(Input, 'childWorkspacePreserveGit', 'get').mockReturnValue(false);
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.childWorkspacePreserveGit).toBe(false);
|
||||
});
|
||||
|
||||
it('maps childWorkspaceSeparateLibrary from Input', async () => {
|
||||
jest.spyOn(Input, 'childWorkspaceSeparateLibrary', 'get').mockReturnValue(false);
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.childWorkspaceSeparateLibrary).toBe(false);
|
||||
});
|
||||
|
||||
it('maps lfsTransferAgent from Input', async () => {
|
||||
jest.spyOn(Input, 'lfsTransferAgent', 'get').mockReturnValue('/tools/elastic-git-storage');
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.lfsTransferAgent).toBe('/tools/elastic-git-storage');
|
||||
});
|
||||
|
||||
it('maps lfsTransferAgentArgs from Input', async () => {
|
||||
jest.spyOn(Input, 'lfsTransferAgentArgs', 'get').mockReturnValue('--verbose');
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.lfsTransferAgentArgs).toBe('--verbose');
|
||||
});
|
||||
|
||||
it('maps lfsStoragePaths from Input', async () => {
|
||||
jest.spyOn(Input, 'lfsStoragePaths', 'get').mockReturnValue('/path/a;/path/b');
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.lfsStoragePaths).toBe('/path/a;/path/b');
|
||||
});
|
||||
|
||||
it('maps gitHooksEnabled from Input', async () => {
|
||||
jest.spyOn(Input, 'gitHooksEnabled', 'get').mockReturnValue(true);
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.gitHooksEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('maps gitHooksSkipList from Input', async () => {
|
||||
jest.spyOn(Input, 'gitHooksSkipList', 'get').mockReturnValue('pre-commit,pre-push');
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.gitHooksSkipList).toBe('pre-commit,pre-push');
|
||||
});
|
||||
|
||||
it('maps gitHooksRunBeforeBuild from Input', async () => {
|
||||
jest.spyOn(Input, 'gitHooksRunBeforeBuild', 'get').mockReturnValue('pre-commit');
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.gitHooksRunBeforeBuild).toBe('pre-commit');
|
||||
});
|
||||
|
||||
it('maps providerExecutable from Input', async () => {
|
||||
jest.spyOn(Input, 'providerExecutable', 'get').mockReturnValue('/usr/local/bin/provider');
|
||||
const parameters = await BuildParameters.create();
|
||||
expect(parameters.providerExecutable).toBe('/usr/local/bin/provider');
|
||||
});
|
||||
|
||||
// Test that all enterprise properties have correct defaults when not explicitly set
|
||||
it('has correct defaults for all enterprise properties', async () => {
|
||||
const parameters = await BuildParameters.create();
|
||||
|
||||
expect(parameters.submoduleProfilePath).toBe('');
|
||||
expect(parameters.submoduleVariantPath).toBe('');
|
||||
expect(parameters.submoduleToken).toBe('');
|
||||
expect(parameters.localCacheEnabled).toBe(false);
|
||||
expect(parameters.localCacheRoot).toBe('');
|
||||
expect(parameters.localCacheLibrary).toBe(true);
|
||||
expect(parameters.localCacheLfs).toBe(false);
|
||||
expect(parameters.childWorkspacesEnabled).toBe(false);
|
||||
expect(parameters.childWorkspaceName).toBe('');
|
||||
expect(parameters.childWorkspaceCacheRoot).toBe('');
|
||||
expect(parameters.childWorkspacePreserveGit).toBe(true);
|
||||
expect(parameters.childWorkspaceSeparateLibrary).toBe(true);
|
||||
expect(parameters.lfsTransferAgent).toBe('');
|
||||
expect(parameters.lfsTransferAgentArgs).toBe('');
|
||||
expect(parameters.lfsStoragePaths).toBe('');
|
||||
expect(parameters.gitHooksEnabled).toBe(false);
|
||||
expect(parameters.gitHooksSkipList).toBe('');
|
||||
expect(parameters.gitHooksRunBeforeBuild).toBe('');
|
||||
expect(parameters.providerExecutable).toBe('');
|
||||
});
|
||||
});
|
||||
@@ -83,6 +83,7 @@ submodules:
|
||||
mockedFs.readFileSync.mockImplementation((filePath: any) => {
|
||||
if (filePath === '/base.yml') return baseYaml;
|
||||
if (filePath === '/variant.yml') return variantYaml;
|
||||
|
||||
return '';
|
||||
});
|
||||
|
||||
@@ -196,6 +197,7 @@ submodules:
|
||||
mockedFs.readFileSync.mockImplementation((filePath: any) => {
|
||||
if (String(filePath).endsWith('profile.yml')) return profileYaml;
|
||||
if (String(filePath).endsWith('.gitmodules')) return gitmodulesContent;
|
||||
|
||||
return '';
|
||||
});
|
||||
|
||||
@@ -249,6 +251,7 @@ submodules:
|
||||
if (p.endsWith('profile.yml')) return profileYaml;
|
||||
if (p.endsWith('variant.yml')) return variantYaml;
|
||||
if (p.endsWith('.gitmodules')) return gitmodulesContent;
|
||||
|
||||
return '';
|
||||
});
|
||||
|
||||
@@ -309,4 +312,74 @@ submodules:
|
||||
expect(mockedSystem.Run).not.toHaveBeenCalledWith(expect.stringContaining('git config url'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute — command construction safety', () => {
|
||||
it('constructs expected git commands for a standard path', async () => {
|
||||
mockedSystem.Run.mockResolvedValue('');
|
||||
|
||||
const plan = [
|
||||
{
|
||||
name: 'Assets/_Game/Submodules/TurnOfWar',
|
||||
path: 'Assets/_Game/Submodules/TurnOfWar',
|
||||
branch: 'main',
|
||||
action: 'init' as const,
|
||||
},
|
||||
];
|
||||
|
||||
await SubmoduleProfileService.execute(plan, '/repo');
|
||||
|
||||
expect(mockedSystem.Run).toHaveBeenCalledWith('git submodule update --init Assets/_Game/Submodules/TurnOfWar');
|
||||
});
|
||||
|
||||
it('passes path directly into git commands (paths with spaces are not quoted)', async () => {
|
||||
mockedSystem.Run.mockResolvedValue('');
|
||||
|
||||
// This test documents current behavior: paths are passed as-is.
|
||||
// If a path contained shell metacharacters, they would be passed through.
|
||||
// This is acceptable because submodule paths come from .gitmodules (trusted source),
|
||||
// not from user input. Still, this test documents the behavior for awareness.
|
||||
const plan = [
|
||||
{ name: 'Module With Spaces', path: 'Assets/Module With Spaces', branch: 'main', action: 'init' as const },
|
||||
];
|
||||
|
||||
await SubmoduleProfileService.execute(plan, '/repo');
|
||||
|
||||
// The current implementation passes the path directly — no shell quoting
|
||||
expect(mockedSystem.Run).toHaveBeenCalledWith('git submodule update --init Assets/Module With Spaces');
|
||||
});
|
||||
|
||||
it('passes branch name directly into git checkout command', async () => {
|
||||
mockedSystem.Run.mockResolvedValue('');
|
||||
|
||||
// Document that branch names are passed as-is into shell commands.
|
||||
// Branch names come from the trusted profile YAML, not user input.
|
||||
const plan = [{ name: 'ModuleX', path: 'Assets/ModuleX', branch: 'feature/my-branch', action: 'init' as const }];
|
||||
|
||||
await SubmoduleProfileService.execute(plan, '/repo');
|
||||
|
||||
expect(mockedSystem.Run).toHaveBeenCalledWith('git -C Assets/ModuleX checkout feature/my-branch');
|
||||
});
|
||||
|
||||
it('constructs deinit command with error suppression for skip actions', async () => {
|
||||
mockedSystem.Run.mockResolvedValue('');
|
||||
|
||||
const plan = [{ name: 'Unused', path: 'Assets/Unused', branch: 'empty', action: 'skip' as const }];
|
||||
|
||||
await SubmoduleProfileService.execute(plan, '/repo');
|
||||
|
||||
expect(mockedSystem.Run).toHaveBeenCalledWith('git submodule deinit -f Assets/Unused 2>/dev/null || true');
|
||||
});
|
||||
|
||||
it('injects token into git config URL insteadOf pattern', async () => {
|
||||
mockedSystem.Run.mockResolvedValue('');
|
||||
|
||||
// The token is embedded directly into the URL pattern.
|
||||
// This is the standard Git credential approach for CI.
|
||||
await SubmoduleProfileService.execute([], '/repo', 'ghp_abc123xyz');
|
||||
|
||||
const configCall = mockedSystem.Run.mock.calls.find((call: any[]) => String(call[0]).includes('git config url'));
|
||||
expect(configCall).toBeDefined();
|
||||
expect(configCall![0]).toBe('git config url."https://ghp_abc123xyz@github.com/".insteadOf "https://github.com/"');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user