Skip to content

Commit 4b22c32

Browse files
authored
Merge branch 'main' into Issue-1354
2 parents ac8571a + d0ddd14 commit 4b22c32

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+2095
-239
lines changed
Lines changed: 15 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,21 @@
1-
import { describe, it, expect, afterEach } from 'vitest';
1+
import { describe, it, afterEach } from 'vitest';
22
import { Docifier } from './docifier.js';
3-
import { rmSync, existsSync} from 'fs';
3+
import { rmSync} from 'fs';
44
import { join } from 'path';
5+
import {expectDirectoryMatch} from '../test/file-comparison';
56

67
const INPUT_DIR = join(
78
__dirname,
89
'../../test_fixtures/command/generate/expected-output'
910
);
10-
1111
const WORKSHOP_DIR = join(
1212
__dirname,
1313
'../../../calm/workshop/controls'
1414
);
1515

16-
const CALM_DIR = join(
17-
__dirname,
18-
'../../../calm/release/1.0-rc1/meta'
19-
);
20-
2116
const OUTPUT_DIR = join(__dirname, '../../test_fixtures/docify/workshop/actual-output');
17+
const EXPECTED_OUTPUT_DIR = join(__dirname, '../../test_fixtures/docify/workshop/expected-output');
18+
2219
const NON_SECURE_VERSION_DOC_WEBSITE = join(OUTPUT_DIR,'non-secure');
2320
const SECURE_VERSION_DOC_WEBSITE = join(OUTPUT_DIR,'secure');
2421

@@ -28,44 +25,29 @@ describe('Docifier E2E - Real Model and Template', () => {
2825
});
2926

3027
it('generates documentation from the conference-signup.arch.json model', async () => {
31-
const mapping = new Map<string, string>();
3228

33-
const docifier = new Docifier('WEBSITE', join(INPUT_DIR, 'conference-signup.arch.json'), NON_SECURE_VERSION_DOC_WEBSITE, mapping);
29+
const docifier = new Docifier('WEBSITE', join(INPUT_DIR, 'conference-signup.arch.json'), NON_SECURE_VERSION_DOC_WEBSITE, new Map<string,string>());
3430
await docifier.docify();
35-
36-
//Verifying a few files
37-
const packageJsonPath = join(NON_SECURE_VERSION_DOC_WEBSITE, 'package.json');
38-
const indexMdPath = join(NON_SECURE_VERSION_DOC_WEBSITE, 'docs/index.md');
39-
40-
expect(existsSync(NON_SECURE_VERSION_DOC_WEBSITE)).toBe(true);
41-
expect(existsSync(packageJsonPath)).toBe(true);
42-
expect(existsSync(indexMdPath)).toBe(true);
31+
await expectDirectoryMatch(join(EXPECTED_OUTPUT_DIR,'non-secure'),join(OUTPUT_DIR,'non-secure'));
4332

4433
});
4534

46-
it('generates documentation from the conference-secure-signup.arch.json model', async () => {
35+
it('generates documentation from the conference-secure-signup.arch.json model with explicit local mapping', async () => {
4736
const mapping = new Map<string, string>([
48-
['https://calm.finos.org/release/1.0-rc1/meta/control-requirement.json', join(CALM_DIR, 'control-requirement.json')],
49-
['https://calm.finos.org/workshop/controls/micro-segmentation.config.json', join(WORKSHOP_DIR, 'micro-segmentation.config.json')],
50-
['https://calm.finos.org/workshop/controls/permitted-connection-http.config.json', join(WORKSHOP_DIR, 'permitted-connection-http.config.json')],
51-
['https://calm.finos.org/workshop/controls/permitted-connection-jdbc.config.json', join(WORKSHOP_DIR, 'permitted-connection-jdbc.config.json')],
5237
['https://calm.finos.org/workshop/controls/micro-segmentation.config.json', join(WORKSHOP_DIR, 'micro-segmentation.config.json')],
53-
['https://calm.finos.org/workshop/controls/micro-segmentation.requirement.json', join(WORKSHOP_DIR, 'micro-segmentation.requirement.json')],
54-
['https://calm.finos.org/workshop/controls/permitted-connection.requirement.json', join(WORKSHOP_DIR, 'permitted-connection.requirement.json')],
5538
]);
5639

57-
5840
const docifier = new Docifier('WEBSITE', join(INPUT_DIR, 'conference-secure-signup-amended.arch.json'), SECURE_VERSION_DOC_WEBSITE, mapping);
5941

6042
await docifier.docify();
43+
await expectDirectoryMatch(join(EXPECTED_OUTPUT_DIR,'secure'),join(OUTPUT_DIR,'secure'));
6144

62-
//Verifying a few files
63-
const packageJsonPath = join(SECURE_VERSION_DOC_WEBSITE, 'package.json');
64-
const indexMdPath = join(SECURE_VERSION_DOC_WEBSITE, 'docs/index.md');
45+
});
6546

66-
expect(existsSync(SECURE_VERSION_DOC_WEBSITE)).toBe(true);
67-
expect(existsSync(packageJsonPath)).toBe(true);
68-
expect(existsSync(indexMdPath)).toBe(true);
47+
it('generates documentation from the conference-secure-signup.arch.json model with no mapping as workshop documents are available', async () => {
48+
const docifier = new Docifier('WEBSITE', join(INPUT_DIR, 'conference-secure-signup-amended.arch.json'), SECURE_VERSION_DOC_WEBSITE, new Map<string,string>());
49+
await docifier.docify();
50+
await expectDirectoryMatch(join(EXPECTED_OUTPUT_DIR,'secure'),join(OUTPUT_DIR,'secure'));
6951

7052
});
71-
});
53+
});

shared/src/docify/graphing/control-registry.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable @typescript-eslint/no-explicit-any */
21
import { Architecture } from '../../model/core';
32
import { CalmControl } from '../../model/control';
43

shared/src/docify/graphing/flow-sequence-helper.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable @typescript-eslint/no-explicit-any */
21
import { Architecture } from '../../model/core';
32
import { CalmFlowTransition } from '../../model/flow';
43
import { CalmRelationship, CalmInteractsType, CalmConnectsType, CalmComposedOfType } from '../../model/relationship';
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { readFileSync } from 'fs';
2+
import { join } from 'path';
3+
import { describe, it, expect } from 'vitest';
4+
import { extractNetworkAddressables, AddressableEntry } from './network-addressable-extractor.js';
5+
6+
const WORKSHOP_DIR = join(__dirname, '../../../calm/workshop');
7+
const jsonPath = join(WORKSHOP_DIR, 'architecture/conference-secure-signup-amended.arch.json');
8+
const jsonDoc = readFileSync(jsonPath, 'utf-8');
9+
const entries: AddressableEntry[] = extractNetworkAddressables(jsonDoc);
10+
11+
describe('extractNetworkAddressables E2E', () => {
12+
it('extracts the web URL from conference-website interface', () => {
13+
expect(entries).toContainEqual({
14+
path: 'root.nodes[0].interfaces[0].url',
15+
key: 'url',
16+
value: 'https://calm.finos.org/amazing-website'
17+
});
18+
});
19+
20+
it('extracts control requirement URLs from node controls', () => {
21+
expect(entries).toContainEqual({
22+
path: 'root.nodes[4].controls.security.requirements[0].control-requirement-url',
23+
key: 'control-requirement-url',
24+
value: 'https://calm.finos.org/workshop/controls/micro-segmentation.requirement.json'
25+
});
26+
});
27+
28+
it('extracts control config URLs from node controls', () => {
29+
expect(entries).toContainEqual({
30+
path: 'root.nodes[4].controls.security.requirements[0].control-config-url',
31+
key: 'control-config-url',
32+
value: 'https://calm.finos.org/workshop/controls/micro-segmentation.config.json'
33+
});
34+
});
35+
36+
it('extracts requirement URLs from relationships controls', () => {
37+
const relReqs = entries
38+
.filter(e => e.key === 'control-requirement-url' && e.path.startsWith('root.relationships'))
39+
.map(e => e.value)
40+
.sort();
41+
expect(relReqs).toEqual([
42+
'https://calm.finos.org/workshop/controls/permitted-connection.requirement.json',
43+
'https://calm.finos.org/workshop/controls/permitted-connection.requirement.json',
44+
'https://calm.finos.org/workshop/controls/permitted-connection.requirement.json'
45+
].sort());
46+
});
47+
48+
it('extracts config URLs from relationships controls', () => {
49+
const relConfigs = entries
50+
.filter(e => e.key === 'control-config-url' && e.path.startsWith('root.relationships'))
51+
.map(e => e.value)
52+
.sort();
53+
expect(relConfigs).toEqual([
54+
'https://calm.finos.org/workshop/controls/permitted-connection-http.config.json',
55+
'https://calm.finos.org/workshop/controls/permitted-connection-http.config.json',
56+
'https://calm.finos.org/workshop/controls/permitted-connection-jdbc.config.json'
57+
].sort());
58+
});
59+
60+
it('does not extract non-URL values', () => {
61+
expect(entries.every(e => /^https?:\/\//.test(e.value))).toBe(true);
62+
});
63+
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { extractNetworkAddressables } from './network-addressable-extractor.js';
3+
4+
describe('extractNetworkAddressables', () => {
5+
it('extracts URLs from string values', () => {
6+
const json = JSON.stringify({
7+
website: 'http://example.com',
8+
api: 'https://api.example.org/v1'
9+
});
10+
const entries = extractNetworkAddressables(json);
11+
expect(entries).toEqual(
12+
expect.arrayContaining([
13+
{ path: 'root.website', key: 'website', value: 'http://example.com' },
14+
{ path: 'root.api', key: 'api', value: 'https://api.example.org/v1' }
15+
])
16+
);
17+
});
18+
19+
it('extracts URLs from nested objects and arrays', () => {
20+
const doc = {
21+
items: [
22+
{ link: 'http://foo.com' },
23+
['https://bar.com', { deep: 'http://deep.example.com' }]
24+
],
25+
'https://in-key.com': 'value'
26+
};
27+
const json = JSON.stringify(doc);
28+
const entries = extractNetworkAddressables(json);
29+
expect(entries).toEqual(
30+
expect.arrayContaining([
31+
{ path: 'root.items[0].link', key: 'link', value: 'http://foo.com' },
32+
{ path: 'root.items[1][0]', key: '0', value: 'https://bar.com' },
33+
{ path: 'root.items[1][1].deep', key: 'deep', value: 'http://deep.example.com' },
34+
{ path: 'root.https://in-key.com', key: 'https://in-key.com', value: 'https://in-key.com' }
35+
])
36+
);
37+
});
38+
39+
it('throws on invalid JSON input', () => {
40+
expect(() => extractNetworkAddressables('not a json')).toThrow(/Invalid JSON string provided/);
41+
});
42+
43+
it('does not extract when no URLs present', () => {
44+
const json = JSON.stringify({ a: 1, b: ['x', { c: 'y' }] });
45+
const entries = extractNetworkAddressables(json);
46+
expect(entries).toEqual([]);
47+
});
48+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
export interface AddressableEntry {
2+
path: string;
3+
key: string;
4+
value: string;
5+
}
6+
7+
const URL_REGEX = /https?:\/\/[^\s"']+/g;
8+
9+
export function extractNetworkAddressables(jsonString: string): AddressableEntry[] {
10+
let data: unknown;
11+
try {
12+
data = JSON.parse(jsonString);
13+
} catch (err) {
14+
throw new Error(`Invalid JSON string provided: ${err.message}`);
15+
}
16+
const results: AddressableEntry[] = [];
17+
traverse(data, 'root', results);
18+
return results;
19+
}
20+
21+
function traverse(current: unknown, path: string, results: AddressableEntry[]): void {
22+
if (typeof current === 'string') {
23+
pushUrls(path, extractKey(path), current, results);
24+
return;
25+
}
26+
27+
if (Array.isArray(current)) {
28+
current.forEach((item, idx) => traverse(item, `${path}[${idx}]`, results));
29+
return;
30+
}
31+
32+
if (current && typeof current === 'object') {
33+
const obj = current as Record<string, unknown>;
34+
for (const [key, value] of Object.entries(obj)) {
35+
const keyPath = `${path}.${key}`;
36+
pushUrls(keyPath, key, key, results);
37+
if (typeof value === 'string') {
38+
pushUrls(keyPath, key, value, results);
39+
} else if (value) {
40+
traverse(value, keyPath, results);
41+
}
42+
}
43+
}
44+
}
45+
46+
function extractKey(path: string): string {
47+
const match = path.match(/(?:\.([^.[\]]+)|\[(\d+)])$/);
48+
return match ? (match[1] ?? match[2]!) : '';
49+
}
50+
51+
function pushUrls(path: string, key: string, text: string, results: AddressableEntry[]) {
52+
const matches = text.match(URL_REGEX);
53+
matches?.forEach(url => results.push({ path, key, value: url }));
54+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { readFileSync } from 'fs';
2+
import { join } from 'path';
3+
import { describe, it, expect } from 'vitest';
4+
import { extractNetworkAddressables, AddressableEntry } from './network-addressable-extractor.js';
5+
import { NetworkAddressableValidator, ValidationResult } from './network-addressable-validator.js';
6+
7+
const WORKSHOP_DIR = join(__dirname, '../../../calm/workshop');
8+
const jsonPath = join(
9+
WORKSHOP_DIR,
10+
'architecture/conference-secure-signup-amended.arch.json'
11+
);
12+
const jsonDoc = readFileSync(jsonPath, 'utf-8');
13+
const entries: AddressableEntry[] = extractNetworkAddressables(jsonDoc);
14+
15+
// Using default HttpReferenceResolver; ensure network access is available
16+
const validator = new NetworkAddressableValidator();
17+
18+
describe('NetworkAddressableValidator E2E', () => {
19+
let results: ValidationResult[];
20+
21+
beforeAll(async () => {
22+
results = await validator.validate(entries);
23+
});
24+
25+
it('validates the conference website URL as reachable and not a schema', () => {
26+
const entry = entries.find(
27+
e => e.value === 'https://calm.finos.org/amazing-website'
28+
);
29+
const result = results.find(r => r.entry === entry);
30+
expect(result).toBeDefined();
31+
expect(result?.reachable).toBe(false);
32+
expect(result?.isSchemaDefinition).toBe(false);
33+
expect(result?.isSchemaImplementation).toBe(false);
34+
expect(result?.error).toBe('HTTP request failed for https://calm.finos.org/amazing-website: Request failed with status code 404');
35+
});
36+
37+
it('validates a control-requirement URL as schema document', () => {
38+
const entry = entries.find(
39+
e => e.key === 'control-requirement-url'
40+
);
41+
const result = results.find(r => r.entry === entry);
42+
expect(result).toBeDefined();
43+
expect(result?.reachable).toBe(true);
44+
expect(result?.isSchemaDefinition).toBe(true);
45+
expect(result?.isSchemaImplementation).toBe(false);
46+
});
47+
48+
it('validates a control-config URL as schema document', () => {
49+
const entry = entries.find(
50+
e => e.key === 'control-config-url'
51+
);
52+
const result = results.find(r => r.entry === entry);
53+
expect(result).toBeDefined();
54+
expect(result?.reachable).toBe(true);
55+
expect(result?.isSchemaDefinition).toBe(false);
56+
expect(result?.isSchemaImplementation).toBe(true);
57+
});
58+
59+
it('all entries have a reachable or error result', () => {
60+
expect(results.length).toBe(entries.length);
61+
results.forEach(r => {
62+
expect(r.reachable || r.error).toBeDefined();
63+
});
64+
});
65+
});

0 commit comments

Comments
 (0)