github-issue-tower-defence-management 1.84.1 → 1.86.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/create-pr.yml +7 -2
- package/CHANGELOG.md +14 -0
- package/README.md +15 -3
- package/bin/adapter/entry-points/cli/index.js +37 -0
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/cli/projectConfig.js +2 -0
- package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleServer.js +204 -0
- package/bin/adapter/entry-points/console/consoleServer.js.map +1 -0
- package/bin/domain/entities/WorkflowStatus.js +6 -1
- package/bin/domain/entities/WorkflowStatus.js.map +1 -1
- package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js +1 -0
- package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +126 -0
- package/src/adapter/entry-points/cli/index.ts +66 -0
- package/src/adapter/entry-points/cli/projectConfig.ts +4 -0
- package/src/adapter/entry-points/console/consoleServer.test.ts +297 -0
- package/src/adapter/entry-points/console/consoleServer.ts +220 -0
- package/src/domain/entities/WorkflowStatus.ts +5 -0
- package/src/domain/usecases/SetupTowerDefenceProjectUseCase.test.ts +89 -1
- package/src/domain/usecases/console/GenerateConsoleListsUseCase.test.ts +2 -0
- package/src/domain/usecases/console/GenerateConsoleListsUseCase.ts +1 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
- package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleServer.d.ts +19 -0
- package/types/adapter/entry-points/console/consoleServer.d.ts.map +1 -0
- package/types/domain/entities/WorkflowStatus.d.ts +1 -0
- package/types/domain/entities/WorkflowStatus.d.ts.map +1 -1
- package/types/domain/usecases/console/GenerateConsoleListsUseCase.d.ts.map +1 -1
|
@@ -60,6 +60,25 @@ jest.mock('../handlers/HandleScheduledEventUseCaseHandler', () => ({
|
|
|
60
60
|
handle: jest.fn().mockResolvedValue(null),
|
|
61
61
|
})),
|
|
62
62
|
}));
|
|
63
|
+
import type { StartConsoleServerOptions } from '../console/consoleServer';
|
|
64
|
+
|
|
65
|
+
const mockStartConsoleServer = jest
|
|
66
|
+
.fn<Promise<unknown>, [StartConsoleServerOptions]>()
|
|
67
|
+
.mockResolvedValue({
|
|
68
|
+
close: jest.fn(),
|
|
69
|
+
address: jest.fn().mockReturnValue({ port: 9981 }),
|
|
70
|
+
});
|
|
71
|
+
jest.mock('../console/consoleServer', () => {
|
|
72
|
+
const actual: Record<string, unknown> = jest.requireActual(
|
|
73
|
+
'../console/consoleServer',
|
|
74
|
+
);
|
|
75
|
+
return {
|
|
76
|
+
...actual,
|
|
77
|
+
startConsoleServer: (
|
|
78
|
+
options: StartConsoleServerOptions,
|
|
79
|
+
): Promise<unknown> => mockStartConsoleServer(options),
|
|
80
|
+
};
|
|
81
|
+
});
|
|
63
82
|
|
|
64
83
|
describe('CLI', () => {
|
|
65
84
|
const originalEnv = process.env;
|
|
@@ -1644,4 +1663,111 @@ mysteryKey: 'value'
|
|
|
1644
1663
|
processExitSpy.mockRestore();
|
|
1645
1664
|
});
|
|
1646
1665
|
});
|
|
1666
|
+
|
|
1667
|
+
describe('serveConsole', () => {
|
|
1668
|
+
it('should appear in the CLI help output', () => {
|
|
1669
|
+
const helpText = program.helpInformation();
|
|
1670
|
+
expect(helpText).toContain('serveConsole');
|
|
1671
|
+
});
|
|
1672
|
+
|
|
1673
|
+
it('should start the server on the default port 9981 when --port is omitted', async () => {
|
|
1674
|
+
writeConfig({ ...defaultConfig, consoleAccessToken: 'config-token' });
|
|
1675
|
+
const logSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
1676
|
+
|
|
1677
|
+
await program.parseAsync([
|
|
1678
|
+
'node',
|
|
1679
|
+
'test',
|
|
1680
|
+
'serveConsole',
|
|
1681
|
+
'--configFilePath',
|
|
1682
|
+
configFilePath,
|
|
1683
|
+
]);
|
|
1684
|
+
|
|
1685
|
+
expect(mockStartConsoleServer).toHaveBeenCalledTimes(1);
|
|
1686
|
+
const callArg = mockStartConsoleServer.mock.calls[0][0];
|
|
1687
|
+
expect(callArg.port).toBe(9981);
|
|
1688
|
+
expect(callArg.accessToken).toBe('config-token');
|
|
1689
|
+
expect(callArg.consoleDataOutputDir).toBeNull();
|
|
1690
|
+
expect(typeof callArg.uiDistDir).toBe('string');
|
|
1691
|
+
|
|
1692
|
+
logSpy.mockRestore();
|
|
1693
|
+
});
|
|
1694
|
+
|
|
1695
|
+
it('should use the provided --port and --consoleDataOutputDir', async () => {
|
|
1696
|
+
writeConfig({ ...defaultConfig, consoleAccessToken: 'config-token' });
|
|
1697
|
+
const logSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
1698
|
+
|
|
1699
|
+
await program.parseAsync([
|
|
1700
|
+
'node',
|
|
1701
|
+
'test',
|
|
1702
|
+
'serveConsole',
|
|
1703
|
+
'--configFilePath',
|
|
1704
|
+
configFilePath,
|
|
1705
|
+
'--port',
|
|
1706
|
+
'12345',
|
|
1707
|
+
'--consoleDataOutputDir',
|
|
1708
|
+
'/tmp/console-data',
|
|
1709
|
+
]);
|
|
1710
|
+
|
|
1711
|
+
const callArg = mockStartConsoleServer.mock.calls[0][0];
|
|
1712
|
+
expect(callArg.port).toBe(12345);
|
|
1713
|
+
expect(callArg.consoleDataOutputDir).toBe('/tmp/console-data');
|
|
1714
|
+
|
|
1715
|
+
logSpy.mockRestore();
|
|
1716
|
+
});
|
|
1717
|
+
|
|
1718
|
+
it('should exit with error when consoleAccessToken is missing from config', async () => {
|
|
1719
|
+
writeConfig(defaultConfig);
|
|
1720
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
1721
|
+
const processExitSpy = jest
|
|
1722
|
+
.spyOn(process, 'exit')
|
|
1723
|
+
.mockImplementation(() => {
|
|
1724
|
+
throw new Error('process.exit called');
|
|
1725
|
+
});
|
|
1726
|
+
|
|
1727
|
+
await expect(
|
|
1728
|
+
program.parseAsync([
|
|
1729
|
+
'node',
|
|
1730
|
+
'test',
|
|
1731
|
+
'serveConsole',
|
|
1732
|
+
'--configFilePath',
|
|
1733
|
+
configFilePath,
|
|
1734
|
+
]),
|
|
1735
|
+
).rejects.toThrow('process.exit called');
|
|
1736
|
+
|
|
1737
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
1738
|
+
'consoleAccessToken is required. Provide it via the config file.',
|
|
1739
|
+
);
|
|
1740
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
1741
|
+
|
|
1742
|
+
consoleErrorSpy.mockRestore();
|
|
1743
|
+
processExitSpy.mockRestore();
|
|
1744
|
+
});
|
|
1745
|
+
|
|
1746
|
+
it('should exit with error for an invalid --port value', async () => {
|
|
1747
|
+
writeConfig({ ...defaultConfig, consoleAccessToken: 'config-token' });
|
|
1748
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
1749
|
+
const processExitSpy = jest
|
|
1750
|
+
.spyOn(process, 'exit')
|
|
1751
|
+
.mockImplementation(() => {
|
|
1752
|
+
throw new Error('process.exit called');
|
|
1753
|
+
});
|
|
1754
|
+
|
|
1755
|
+
await expect(
|
|
1756
|
+
program.parseAsync([
|
|
1757
|
+
'node',
|
|
1758
|
+
'test',
|
|
1759
|
+
'serveConsole',
|
|
1760
|
+
'--configFilePath',
|
|
1761
|
+
configFilePath,
|
|
1762
|
+
'--port',
|
|
1763
|
+
'not-a-number',
|
|
1764
|
+
]),
|
|
1765
|
+
).rejects.toThrow('process.exit called');
|
|
1766
|
+
|
|
1767
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
1768
|
+
|
|
1769
|
+
consoleErrorSpy.mockRestore();
|
|
1770
|
+
processExitSpy.mockRestore();
|
|
1771
|
+
});
|
|
1772
|
+
});
|
|
1647
1773
|
});
|
|
@@ -31,6 +31,11 @@ import { NodeLocalCommandRunner } from '../../repositories/NodeLocalCommandRunne
|
|
|
31
31
|
import { GitHubIssueCommentRepository } from '../../repositories/GitHubIssueCommentRepository';
|
|
32
32
|
import { FetchWebhookRepository } from '../../repositories/FetchWebhookRepository';
|
|
33
33
|
import { RevertOrphanedPreparationUseCase } from '../../../domain/usecases/RevertOrphanedPreparationUseCase';
|
|
34
|
+
import * as path from 'path';
|
|
35
|
+
import {
|
|
36
|
+
DEFAULT_CONSOLE_PORT,
|
|
37
|
+
startConsoleServer,
|
|
38
|
+
} from '../console/consoleServer';
|
|
34
39
|
|
|
35
40
|
type StartDaemonOptions = {
|
|
36
41
|
projectUrl?: string;
|
|
@@ -60,6 +65,12 @@ type CheckIssueReviewReadinessOptions = {
|
|
|
60
65
|
configFilePath: string;
|
|
61
66
|
};
|
|
62
67
|
|
|
68
|
+
type ServeConsoleOptions = {
|
|
69
|
+
configFilePath: string;
|
|
70
|
+
port?: string;
|
|
71
|
+
consoleDataOutputDir?: string;
|
|
72
|
+
};
|
|
73
|
+
|
|
63
74
|
const buildGithubRepositoryParams = (
|
|
64
75
|
localStorageRepository: LocalStorageRepository,
|
|
65
76
|
token: string,
|
|
@@ -554,6 +565,61 @@ program
|
|
|
554
565
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
555
566
|
});
|
|
556
567
|
|
|
568
|
+
program
|
|
569
|
+
.command('serveConsole')
|
|
570
|
+
.description('Start the local TDPM Console HTTP server')
|
|
571
|
+
.requiredOption(
|
|
572
|
+
'--configFilePath <path>',
|
|
573
|
+
'Path to config file for tower defence management',
|
|
574
|
+
)
|
|
575
|
+
.option(
|
|
576
|
+
'--port <number>',
|
|
577
|
+
`Port for the console HTTP server (default: ${DEFAULT_CONSOLE_PORT})`,
|
|
578
|
+
)
|
|
579
|
+
.option(
|
|
580
|
+
'--consoleDataOutputDir <path>',
|
|
581
|
+
'Directory where console data files are written and served from',
|
|
582
|
+
)
|
|
583
|
+
.action(async (options: ServeConsoleOptions) => {
|
|
584
|
+
const config = loadConfigFile(options.configFilePath);
|
|
585
|
+
|
|
586
|
+
const accessToken = config.consoleAccessToken;
|
|
587
|
+
if (!accessToken) {
|
|
588
|
+
console.error(
|
|
589
|
+
'consoleAccessToken is required. Provide it via the config file.',
|
|
590
|
+
);
|
|
591
|
+
process.exit(1);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
let port = DEFAULT_CONSOLE_PORT;
|
|
595
|
+
if (options.port !== undefined) {
|
|
596
|
+
const parsedPort = Number(options.port);
|
|
597
|
+
if (
|
|
598
|
+
!Number.isFinite(parsedPort) ||
|
|
599
|
+
!Number.isInteger(parsedPort) ||
|
|
600
|
+
parsedPort <= 0 ||
|
|
601
|
+
parsedPort > 65535
|
|
602
|
+
) {
|
|
603
|
+
console.error(
|
|
604
|
+
'Invalid value for --port. It must be a positive integer between 1 and 65535.',
|
|
605
|
+
);
|
|
606
|
+
process.exit(1);
|
|
607
|
+
}
|
|
608
|
+
port = parsedPort;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const uiDistDir = path.join(__dirname, 'ui-dist');
|
|
612
|
+
const consoleDataOutputDir = options.consoleDataOutputDir ?? null;
|
|
613
|
+
|
|
614
|
+
await startConsoleServer({
|
|
615
|
+
accessToken,
|
|
616
|
+
uiDistDir,
|
|
617
|
+
consoleDataOutputDir,
|
|
618
|
+
port,
|
|
619
|
+
});
|
|
620
|
+
console.log(`TDPM Console server listening on port ${port}`);
|
|
621
|
+
});
|
|
622
|
+
|
|
557
623
|
/* istanbul ignore next */
|
|
558
624
|
if (process.argv && require.main === module) {
|
|
559
625
|
program.parse(process.argv);
|
|
@@ -21,6 +21,7 @@ export type ConfigFile = {
|
|
|
21
21
|
awLogStaleThresholdMinutes?: number;
|
|
22
22
|
labelsAsLlmAgentName?: string[];
|
|
23
23
|
changeTargetPathAliases?: Record<string, string>;
|
|
24
|
+
consoleAccessToken?: string;
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
const getStringValue = (
|
|
@@ -145,6 +146,7 @@ export const loadConfigFile = (configFilePath: string): ConfigFile => {
|
|
|
145
146
|
parsed,
|
|
146
147
|
'changeTargetPathAliases',
|
|
147
148
|
),
|
|
149
|
+
consoleAccessToken: getStringValue(parsed, 'consoleAccessToken'),
|
|
148
150
|
};
|
|
149
151
|
} catch (error) {
|
|
150
152
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -303,6 +305,8 @@ export const mergeConfigs = (
|
|
|
303
305
|
readmeOverrides.changeTargetPathAliases ??
|
|
304
306
|
cliOverrides.changeTargetPathAliases ??
|
|
305
307
|
configFile.changeTargetPathAliases,
|
|
308
|
+
consoleAccessToken:
|
|
309
|
+
cliOverrides.consoleAccessToken ?? configFile.consoleAccessToken,
|
|
306
310
|
});
|
|
307
311
|
|
|
308
312
|
type GraphqlProjectV2ReadmeResponse = {
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import * as http from 'http';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_CONSOLE_PORT,
|
|
7
|
+
CONSOLE_TOKEN_HEADER,
|
|
8
|
+
hasDotSegment,
|
|
9
|
+
requiresToken,
|
|
10
|
+
isTokenValid,
|
|
11
|
+
extractProvidedToken,
|
|
12
|
+
startConsoleServer,
|
|
13
|
+
} from './consoleServer';
|
|
14
|
+
|
|
15
|
+
describe('consoleServer pure helpers', () => {
|
|
16
|
+
describe('DEFAULT_CONSOLE_PORT', () => {
|
|
17
|
+
it('is 9981', () => {
|
|
18
|
+
expect(DEFAULT_CONSOLE_PORT).toBe(9981);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('hasDotSegment', () => {
|
|
23
|
+
it('returns true for a top-level dot segment', () => {
|
|
24
|
+
expect(hasDotSegment('/.git')).toBe(true);
|
|
25
|
+
expect(hasDotSegment('/.env')).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('returns true for a nested dot segment', () => {
|
|
29
|
+
expect(hasDotSegment('/foo/.bar')).toBe(true);
|
|
30
|
+
expect(hasDotSegment('/a/b/.hidden/c')).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('returns false for paths without dot segments', () => {
|
|
34
|
+
expect(hasDotSegment('/')).toBe(false);
|
|
35
|
+
expect(hasDotSegment('/index.html')).toBe(false);
|
|
36
|
+
expect(hasDotSegment('/assets/app.js')).toBe(false);
|
|
37
|
+
expect(hasDotSegment('/api/itembody')).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('requiresToken', () => {
|
|
42
|
+
it('requires a token for .json paths', () => {
|
|
43
|
+
expect(requiresToken('/data/situation.json')).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('requires a token for /api/* paths', () => {
|
|
47
|
+
expect(requiresToken('/api/review')).toBe(true);
|
|
48
|
+
expect(requiresToken('/api')).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('does not require a token for bootstrap assets', () => {
|
|
52
|
+
expect(requiresToken('/')).toBe(false);
|
|
53
|
+
expect(requiresToken('/index.html')).toBe(false);
|
|
54
|
+
expect(requiresToken('/assets/app.js')).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('isTokenValid', () => {
|
|
59
|
+
it('accepts a matching token', () => {
|
|
60
|
+
expect(isTokenValid('expected', 'expected')).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('rejects a missing or mismatched token', () => {
|
|
64
|
+
expect(isTokenValid('expected', null)).toBe(false);
|
|
65
|
+
expect(isTokenValid('expected', 'other')).toBe(false);
|
|
66
|
+
expect(isTokenValid('expected', '')).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('extractProvidedToken', () => {
|
|
71
|
+
it('prefers the query token', () => {
|
|
72
|
+
expect(extractProvidedToken('fromQuery', 'fromHeader')).toBe('fromQuery');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('falls back to the header token', () => {
|
|
76
|
+
expect(extractProvidedToken(null, 'fromHeader')).toBe('fromHeader');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('returns null when neither is present', () => {
|
|
80
|
+
expect(extractProvidedToken(null, undefined)).toBeNull();
|
|
81
|
+
expect(extractProvidedToken('', undefined)).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('consoleServer integration', () => {
|
|
87
|
+
const testToken = 'integration-test-token-value';
|
|
88
|
+
|
|
89
|
+
const requestServer = (
|
|
90
|
+
server: http.Server,
|
|
91
|
+
requestPath: string,
|
|
92
|
+
headers: http.OutgoingHttpHeaders = {},
|
|
93
|
+
): Promise<{
|
|
94
|
+
statusCode: number;
|
|
95
|
+
body: string;
|
|
96
|
+
cacheControl: string | undefined;
|
|
97
|
+
contentType: string | undefined;
|
|
98
|
+
}> => {
|
|
99
|
+
const address = server.address();
|
|
100
|
+
if (address === null || typeof address === 'string') {
|
|
101
|
+
throw new Error('server is not listening on a TCP port');
|
|
102
|
+
}
|
|
103
|
+
const port = address.port;
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const request = http.request(
|
|
106
|
+
{ host: '127.0.0.1', port, path: requestPath, headers },
|
|
107
|
+
(response) => {
|
|
108
|
+
const chunks: Uint8Array[] = [];
|
|
109
|
+
response.on('data', (chunk: Uint8Array) => chunks.push(chunk));
|
|
110
|
+
response.on('end', () => {
|
|
111
|
+
resolve({
|
|
112
|
+
statusCode: response.statusCode ?? 0,
|
|
113
|
+
body: Buffer.concat(chunks).toString('utf-8'),
|
|
114
|
+
cacheControl: response.headers['cache-control'],
|
|
115
|
+
contentType: response.headers['content-type'],
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
request.on('error', reject);
|
|
121
|
+
request.end();
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const closeServer = (server: http.Server): Promise<void> =>
|
|
126
|
+
new Promise((resolve, reject) => {
|
|
127
|
+
server.close((error) => {
|
|
128
|
+
if (error) {
|
|
129
|
+
reject(error);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
resolve();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('starts on an ephemeral port and closes gracefully', async () => {
|
|
137
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
138
|
+
const server = await startConsoleServer({
|
|
139
|
+
accessToken: testToken,
|
|
140
|
+
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
141
|
+
consoleDataOutputDir: null,
|
|
142
|
+
port: 0,
|
|
143
|
+
});
|
|
144
|
+
const address = server.address();
|
|
145
|
+
expect(address).not.toBeNull();
|
|
146
|
+
expect(typeof address).not.toBe('string');
|
|
147
|
+
await closeServer(server);
|
|
148
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('serves the placeholder index without a token when ui-dist is absent', async () => {
|
|
152
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
153
|
+
const server = await startConsoleServer({
|
|
154
|
+
accessToken: testToken,
|
|
155
|
+
uiDistDir: path.join(tmpDir, 'missing-ui-dist'),
|
|
156
|
+
consoleDataOutputDir: null,
|
|
157
|
+
port: 0,
|
|
158
|
+
});
|
|
159
|
+
try {
|
|
160
|
+
const root = await requestServer(server, '/');
|
|
161
|
+
expect(root.statusCode).toBe(200);
|
|
162
|
+
expect(root.body).toContain('TDPM Console');
|
|
163
|
+
expect(root.cacheControl).toBe('no-store');
|
|
164
|
+
expect(root.contentType).toContain('text/html');
|
|
165
|
+
|
|
166
|
+
const indexHtml = await requestServer(server, '/index.html');
|
|
167
|
+
expect(indexHtml.statusCode).toBe(200);
|
|
168
|
+
expect(indexHtml.cacheControl).toBe('no-store');
|
|
169
|
+
} finally {
|
|
170
|
+
await closeServer(server);
|
|
171
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('returns 404 for a missing non-index file when ui-dist is absent', async () => {
|
|
176
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
177
|
+
const server = await startConsoleServer({
|
|
178
|
+
accessToken: testToken,
|
|
179
|
+
uiDistDir: path.join(tmpDir, 'missing-ui-dist'),
|
|
180
|
+
consoleDataOutputDir: null,
|
|
181
|
+
port: 0,
|
|
182
|
+
});
|
|
183
|
+
try {
|
|
184
|
+
const missing = await requestServer(server, '/assets/app.js');
|
|
185
|
+
expect(missing.statusCode).toBe(404);
|
|
186
|
+
expect(missing.cacheControl).toBe('no-store');
|
|
187
|
+
} finally {
|
|
188
|
+
await closeServer(server);
|
|
189
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('serves built bootstrap assets without a token', async () => {
|
|
194
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
195
|
+
const uiDistDir = path.join(tmpDir, 'ui-dist');
|
|
196
|
+
fs.mkdirSync(path.join(uiDistDir, 'assets'), { recursive: true });
|
|
197
|
+
fs.writeFileSync(
|
|
198
|
+
path.join(uiDistDir, 'index.html'),
|
|
199
|
+
'<!DOCTYPE html><title>built</title>',
|
|
200
|
+
);
|
|
201
|
+
fs.writeFileSync(
|
|
202
|
+
path.join(uiDistDir, 'assets', 'app.js'),
|
|
203
|
+
'console.log("app");',
|
|
204
|
+
);
|
|
205
|
+
const server = await startConsoleServer({
|
|
206
|
+
accessToken: testToken,
|
|
207
|
+
uiDistDir,
|
|
208
|
+
consoleDataOutputDir: null,
|
|
209
|
+
port: 0,
|
|
210
|
+
});
|
|
211
|
+
try {
|
|
212
|
+
const index = await requestServer(server, '/');
|
|
213
|
+
expect(index.statusCode).toBe(200);
|
|
214
|
+
expect(index.body).toContain('built');
|
|
215
|
+
|
|
216
|
+
const appJs = await requestServer(server, '/assets/app.js');
|
|
217
|
+
expect(appJs.statusCode).toBe(200);
|
|
218
|
+
expect(appJs.body).toContain('app');
|
|
219
|
+
expect(appJs.contentType).toContain('text/javascript');
|
|
220
|
+
expect(appJs.cacheControl).toBe('no-store');
|
|
221
|
+
} finally {
|
|
222
|
+
await closeServer(server);
|
|
223
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('rejects dot-prefixed paths with 404', async () => {
|
|
228
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
229
|
+
const uiDistDir = path.join(tmpDir, 'ui-dist');
|
|
230
|
+
fs.mkdirSync(uiDistDir, { recursive: true });
|
|
231
|
+
fs.writeFileSync(path.join(uiDistDir, '.env'), 'SECRET=should-not-serve');
|
|
232
|
+
const server = await startConsoleServer({
|
|
233
|
+
accessToken: testToken,
|
|
234
|
+
uiDistDir,
|
|
235
|
+
consoleDataOutputDir: null,
|
|
236
|
+
port: 0,
|
|
237
|
+
});
|
|
238
|
+
try {
|
|
239
|
+
const dotEnv = await requestServer(server, '/.env');
|
|
240
|
+
expect(dotEnv.statusCode).toBe(404);
|
|
241
|
+
expect(dotEnv.body).not.toContain('SECRET');
|
|
242
|
+
|
|
243
|
+
const dotGit = await requestServer(server, '/.git/config');
|
|
244
|
+
expect(dotGit.statusCode).toBe(404);
|
|
245
|
+
|
|
246
|
+
const nestedDot = await requestServer(server, '/foo/.bar');
|
|
247
|
+
expect(nestedDot.statusCode).toBe(404);
|
|
248
|
+
} finally {
|
|
249
|
+
await closeServer(server);
|
|
250
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('rejects .json and /api/* without a token and passes the gate with a valid token', async () => {
|
|
255
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
256
|
+
const server = await startConsoleServer({
|
|
257
|
+
accessToken: testToken,
|
|
258
|
+
uiDistDir: path.join(tmpDir, 'ui-dist'),
|
|
259
|
+
consoleDataOutputDir: null,
|
|
260
|
+
port: 0,
|
|
261
|
+
});
|
|
262
|
+
try {
|
|
263
|
+
const jsonNoToken = await requestServer(server, '/data/situation.json');
|
|
264
|
+
expect(jsonNoToken.statusCode).toBe(401);
|
|
265
|
+
expect(jsonNoToken.cacheControl).toBe('no-store');
|
|
266
|
+
|
|
267
|
+
const apiNoToken = await requestServer(server, '/api/review');
|
|
268
|
+
expect(apiNoToken.statusCode).toBe(401);
|
|
269
|
+
|
|
270
|
+
const jsonWithQueryToken = await requestServer(
|
|
271
|
+
server,
|
|
272
|
+
`/data/situation.json?k=${testToken}`,
|
|
273
|
+
);
|
|
274
|
+
expect(jsonWithQueryToken.statusCode).toBe(404);
|
|
275
|
+
|
|
276
|
+
const apiWithQueryToken = await requestServer(
|
|
277
|
+
server,
|
|
278
|
+
`/api/review?k=${testToken}`,
|
|
279
|
+
);
|
|
280
|
+
expect(apiWithQueryToken.statusCode).toBe(404);
|
|
281
|
+
|
|
282
|
+
const apiWithHeaderToken = await requestServer(server, '/api/review', {
|
|
283
|
+
[CONSOLE_TOKEN_HEADER]: testToken,
|
|
284
|
+
});
|
|
285
|
+
expect(apiWithHeaderToken.statusCode).toBe(404);
|
|
286
|
+
|
|
287
|
+
const apiWithWrongToken = await requestServer(
|
|
288
|
+
server,
|
|
289
|
+
'/api/review?k=wrong-token',
|
|
290
|
+
);
|
|
291
|
+
expect(apiWithWrongToken.statusCode).toBe(401);
|
|
292
|
+
} finally {
|
|
293
|
+
await closeServer(server);
|
|
294
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
});
|