Skip to content

Commit 18e6500

Browse files
Merge pull request finos#1450 from markscott-ms/1.0-tests
tests for model/interface for 1.0-rc2; clarify interface model construction (finos#1284)
2 parents c9aae8a + 82c9d94 commit 18e6500

File tree

10 files changed

+207
-92
lines changed

10 files changed

+207
-92
lines changed

shared/src/model/control.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ describe('CalmControlDetail', () => {
2929
expect(controlDetail).toBeInstanceOf(CalmControlDetail);
3030
expect(controlDetail.requirementUrl).toBe('https://example.com/requirement');
3131
expect(controlDetail.configUrl).toBe('https://example.com/config');
32+
expect(controlDetail.config).toBeUndefined();
3233
});
3334

3435
it('should create a CalmControlDetail from object-based JSON', () => {

shared/src/model/flow.spec.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { CalmFlow, CalmFlowTransition } from './flow.js';
22
import { CalmControl } from './control.js';
33
import { CalmMetadata } from './metadata.js';
4-
import {CalmFlowSchema} from '../types/flow-types.js';
4+
import { CalmFlowSchema } from '../types/flow-types.js';
55

66
describe('CalmFlow', () => {
77
it('should create an instance with given properties', () => {
@@ -38,12 +38,12 @@ describe('CalmFlow', () => {
3838
{
3939
'relationship-unique-id': 'rel-1',
4040
'sequence-number': 1,
41-
'summary': 'Transition 1'
41+
'description': 'Transition 1'
4242
},
4343
{
4444
'relationship-unique-id': 'rel-2',
4545
'sequence-number': 2,
46-
'summary': 'Transition 2',
46+
'description': 'Transition 2',
4747
'direction': 'destination-to-source' // Explicitly providing direction
4848
}
4949
],
@@ -63,10 +63,48 @@ describe('CalmFlow', () => {
6363
expect(flow.description).toBe('Flow created from JSON');
6464
expect(flow.transitions).toHaveLength(2);
6565
expect(flow.transitions[0]).toBeInstanceOf(CalmFlowTransition);
66+
expect(flow.transitions[0].description).toBe('Transition 1');
67+
expect(flow.transitions[0].direction).toBe('source-to-destination');
68+
expect(flow.transitions[1].direction).toBe('destination-to-source');
6669
expect(flow.controls).toBeDefined();
6770
expect(flow.controls[0].controlId).toBe('ctrl-1');
6871
expect(flow.controls[0].description).toBe('JSON Control 1');
6972
expect(flow.metadata).toBeDefined();
7073
});
7174

75+
it('should create an instance from minimal JSON data', () => {
76+
const jsonData: CalmFlowSchema = {
77+
'unique-id': 'flow-456',
78+
'name': 'JSON Flow',
79+
'description': 'Flow created from JSON',
80+
'transitions': [
81+
{
82+
'relationship-unique-id': 'rel-1',
83+
'sequence-number': 1,
84+
'description': 'Transition 1'
85+
},
86+
{
87+
'relationship-unique-id': 'rel-2',
88+
'sequence-number': 2,
89+
'description': 'Transition 2'
90+
}
91+
]
92+
};
93+
94+
const flow = CalmFlow.fromJson(jsonData);
95+
96+
97+
expect(flow).toBeInstanceOf(CalmFlow);
98+
expect(flow.uniqueId).toBe('flow-456');
99+
expect(flow.name).toBe('JSON Flow');
100+
expect(flow.description).toBe('Flow created from JSON');
101+
expect(flow.transitions).toHaveLength(2);
102+
expect(flow.transitions[0]).toBeInstanceOf(CalmFlowTransition);
103+
expect(flow.transitions[0].description).toBe('Transition 1');
104+
expect(flow.controls).toBeDefined();
105+
expect(flow.controls).toHaveLength(0);
106+
expect(flow.metadata).toBeDefined();
107+
expect(flow.metadata).toEqual(new CalmMetadata({}));
108+
});
109+
72110
});

shared/src/model/interface.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,31 @@ describe('CalmInterface.fromJson', () => {
5959
});
6060
});
6161

62+
describe('when both definition-url and config are present plus other properties', () => {
63+
it('returns a CalmInterfaceType with the correct fields', () => {
64+
const defData = {
65+
'unique-id': 'def-123',
66+
'definition-url': 'https://example.com/schema.json',
67+
config: { alpha: true, threshold: 5 },
68+
extraField: 'extraValue'
69+
};
70+
71+
const iface = CalmInterface.fromJson(defData);
72+
expect(iface).toBeInstanceOf(CalmInterfaceType);
73+
74+
// Narrow via instanceof so TypeScript recognizes the subclass
75+
if (!(iface instanceof CalmInterfaceType)) {
76+
throw new Error('Expected CalmInterfaceType');
77+
}
78+
expect(iface.uniqueId).toBe('def-123');
79+
expect(iface.additionalProperties).toEqual({
80+
'definition-url': 'https://example.com/schema.json',
81+
config: { alpha: true, threshold: 5 },
82+
extraField: 'extraValue'
83+
});
84+
});
85+
});
86+
6287
describe('when neither definition-url nor config is present', () => {
6388
it('returns a CalmInterfaceType with any extra properties', () => {
6489
const typeData = {

shared/src/model/interface.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,22 @@ import {
22
CalmNodeInterfaceSchema
33
} from '../types/interface-types.js';
44
import { CalmInterfaceDefinitionSchema, CalmInterfaceTypeSchema } from '../types/interface-types.js';
5-
import { CalmInterfaceSchema } from '../types/core-types.js';
5+
import { CalmInterfaceSchema } from '../types/core-types.js';
6+
7+
const calmInterfaceDefinitionRequiredProperties = [
8+
'unique-id', 'definition-url', 'config'
9+
].sort();
10+
611

712
export class CalmInterface {
8-
constructor(public uniqueId: string) {}
13+
constructor(public uniqueId: string) { }
914

1015
static fromJson(data: CalmInterfaceSchema): CalmInterface {
11-
if ('definition-url' in data && 'config' in data) {
16+
// Compare data property names with the required properties
17+
// for CalmInterfaceDefinition
18+
const dataKeys = Object.keys(data).sort();
19+
if (dataKeys.length === calmInterfaceDefinitionRequiredProperties.length
20+
&& dataKeys.every((val, index) => val === calmInterfaceDefinitionRequiredProperties[index])) {
1221
return CalmInterfaceDefinition.fromJson(data as CalmInterfaceDefinitionSchema);
1322
}
1423
return CalmInterfaceType.fromJson(data as CalmInterfaceTypeSchema);
@@ -48,7 +57,7 @@ export class CalmInterfaceType extends CalmInterface {
4857
}
4958

5059
export class CalmNodeInterface {
51-
constructor(public node: string, public interfaces: string[] = []) {}
60+
constructor(public node: string, public interfaces: string[] = []) { }
5261

5362
static fromJson(data: CalmNodeInterfaceSchema): CalmNodeInterface {
5463
return new CalmNodeInterface(data.node, data.interfaces ?? []);

shared/src/model/node.spec.ts

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { CalmNode, CalmNodeDetails } from './node.js';
2-
import {CalmNodeSchema} from '../types/core-types.js';
2+
import { CalmNodeSchema, CalmNodeDetailsSchema } from '../types/core-types.js';
33

44
const nodeData: CalmNodeSchema = {
55
'unique-id': 'node-001',
@@ -32,6 +32,28 @@ describe('CalmNodeDetails', () => {
3232
expect(nodeDetails.detailedArchitecture).toBe('https://example.com/architecture');
3333
expect(nodeDetails.requiredPattern).toBe('https://example.com/pattern');
3434
});
35+
36+
it('should create a CalmNodeDetails instance from JSON data with no pattern', () => {
37+
const nodeDetailsSchema: CalmNodeDetailsSchema = {
38+
'detailed-architecture': 'https://example.com/architecture',
39+
};
40+
const nodeDetails = CalmNodeDetails.fromJson(nodeDetailsSchema);
41+
42+
expect(nodeDetails).toBeInstanceOf(CalmNodeDetails);
43+
expect(nodeDetails.detailedArchitecture).toBe('https://example.com/architecture');
44+
expect(nodeDetails.requiredPattern).toBeUndefined();
45+
});
46+
47+
it('should create a CalmNodeDetails instance from JSON data with no architecture', () => {
48+
const nodeDetailsSchema: CalmNodeDetailsSchema = {
49+
'required-pattern': 'https://example.com/pattern',
50+
};
51+
const nodeDetails = CalmNodeDetails.fromJson(nodeDetailsSchema);
52+
53+
expect(nodeDetails).toBeInstanceOf(CalmNodeDetails);
54+
expect(nodeDetails.detailedArchitecture).toBeUndefined();
55+
expect(nodeDetails.requiredPattern).toBe('https://example.com/pattern');
56+
});
3557
});
3658

3759
describe('CalmNode', () => {
@@ -57,21 +79,34 @@ describe('CalmNode', () => {
5779
'unique-id': 'node-002',
5880
'node-type': 'service',
5981
name: 'Another Test Node',
60-
description: 'Another test node description',
61-
details: {
62-
'detailed-architecture': 'https://example.com/architecture-2',
63-
'required-pattern': 'https://example.com/pattern-2'
64-
},
65-
interfaces: [{ 'unique-id': 'interface-002', port: 8080 }],
66-
controls: { 'control-002': { description: 'Another test control', requirements: [{ 'requirement-url': 'https://example.com/requirement2', 'config-url': 'https://example.com/config2' }] } },
67-
metadata: [{ key: 'value' }]
82+
description: 'Another test node description'
6883
};
6984

7085
const nodeWithoutOptionalFields = CalmNode.fromJson(nodeDataWithoutOptionalFields);
7186

7287
expect(nodeWithoutOptionalFields).toBeInstanceOf(CalmNode);
7388
expect(nodeWithoutOptionalFields.uniqueId).toBe('node-002');
74-
expect(nodeWithoutOptionalFields['run-as']).toBeUndefined();
89+
expect(nodeWithoutOptionalFields.details.detailedArchitecture).toEqual('');
90+
expect(nodeWithoutOptionalFields.details.requiredPattern).toEqual('');
91+
expect(nodeWithoutOptionalFields.interfaces).toEqual([]);
92+
expect(nodeWithoutOptionalFields.controls).toEqual([]);
93+
expect(nodeWithoutOptionalFields.metadata.data).toEqual({});
94+
});
95+
96+
it('should handle adduitional properties', () => {
97+
const nodeDataWithAdditionalFields: CalmNodeSchema = {
98+
'unique-id': 'node-002',
99+
'node-type': 'service',
100+
name: 'Another Test Node',
101+
description: 'Another test node description',
102+
'another-property': 'some value'
103+
};
104+
105+
const nodeWithAdditionalFields = CalmNode.fromJson(nodeDataWithAdditionalFields);
106+
107+
expect(nodeWithAdditionalFields).toBeInstanceOf(CalmNode);
108+
expect(nodeWithAdditionalFields.uniqueId).toBe('node-002');
109+
expect(nodeWithAdditionalFields.additionalProperties['another-property']).toBe('some value');
75110
});
76111

77112
it('should handle empty interfaces, controls, and metadata', () => {
@@ -94,6 +129,6 @@ describe('CalmNode', () => {
94129
expect(nodeWithEmptyFields).toBeInstanceOf(CalmNode);
95130
expect(nodeWithEmptyFields.interfaces).toHaveLength(0);
96131
expect(nodeWithEmptyFields.controls).toHaveLength(0);
97-
expect(nodeWithEmptyFields.metadata).toEqual({ data: {}});
132+
expect(nodeWithEmptyFields.metadata).toEqual({ data: {} });
98133
});
99134
});

shared/src/model/node.ts

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
import {CalmInterface} from './interface.js';
2-
import {CalmControl} from './control.js';
3-
import {CalmMetadata} from './metadata.js';
4-
import {CalmNodeDetailsSchema, CalmNodeSchema} from '../types/core-types.js';
1+
import { CalmInterface } from './interface.js';
2+
import { CalmControl } from './control.js';
3+
import { CalmMetadata } from './metadata.js';
4+
import { CalmNodeDetailsSchema, CalmNodeSchema } from '../types/core-types.js';
55

66
export type CalmNodeType = 'actor' | 'ecosystem' | 'system' | 'service' | 'database' | 'network' | 'ldap' | 'webclient' | 'data-asset' | string;
77

88
export class CalmNodeDetails {
99
constructor(
1010
public detailedArchitecture: string,
1111
public requiredPattern: string
12-
){}
12+
) { }
1313
static fromJson(data: CalmNodeDetailsSchema): CalmNodeDetails {
1414
return new CalmNodeDetails(
1515
data['detailed-architecture'],
@@ -28,19 +28,31 @@ export class CalmNode {
2828
public details: CalmNodeDetails,
2929
public interfaces: CalmInterface[],
3030
public controls: CalmControl[],
31-
public metadata: CalmMetadata
32-
) {}
31+
public metadata: CalmMetadata,
32+
public additionalProperties: Record<string, unknown>
33+
) { }
3334

3435
static fromJson(data: CalmNodeSchema): CalmNode {
36+
const {
37+
'unique-id': uniqueId,
38+
'node-type': nodeType,
39+
'name': name,
40+
'description': description,
41+
'details': details,
42+
interfaces,
43+
controls,
44+
metadata,
45+
...additionalProperties } = data;
3546
return new CalmNode(
36-
data['unique-id'],
37-
data['node-type'],
38-
data.name,
39-
data.description,
40-
data.details ? CalmNodeDetails.fromJson(data.details) : new CalmNodeDetails('', ''),
41-
data.interfaces ? data.interfaces.map(CalmInterface.fromJson) : [],
42-
data.controls ? CalmControl.fromJson(data.controls) : [],
43-
data.metadata ? CalmMetadata.fromJson(data.metadata): new CalmMetadata({})
47+
uniqueId,
48+
nodeType,
49+
name,
50+
description,
51+
details ? CalmNodeDetails.fromJson(details) : new CalmNodeDetails('', ''),
52+
interfaces ? data.interfaces.map(CalmInterface.fromJson) : [],
53+
controls ? CalmControl.fromJson(controls) : [],
54+
metadata ? CalmMetadata.fromJson(metadata) : new CalmMetadata({}),
55+
additionalProperties
4456
);
4557
}
4658
}

shared/src/model/relationship.spec.ts

Lines changed: 25 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
CalmDeployedInType,
66
CalmComposedOfType,
77
CalmOptionsRelationshipType,
8-
CalmOptionType
8+
CalmDecisionType
99
} from './relationship.js';
1010
import { CalmRelationshipSchema } from '../types/core-types.js';
1111
import { CalmNodeInterface } from './interface.js';
@@ -124,20 +124,16 @@ describe('CalmRelationship', () => {
124124
description: 'A choice between which nodes will be in the architecture',
125125
'relationship-type': {
126126
options: [
127-
[
128-
{
129-
description: 'This is option 1',
130-
nodes: ['node-1'],
131-
relationships: ['relationship-1-x']
132-
}
133-
],
134-
[
135-
{
136-
description: 'This is option 2',
137-
nodes: ['node-2'],
138-
relationships: ['relationship-2-x']
139-
}
140-
]
127+
{
128+
description: 'This is option 1',
129+
nodes: ['node-1'],
130+
relationships: ['relationship-1-x']
131+
},
132+
{
133+
description: 'This is option 2',
134+
nodes: ['node-2'],
135+
relationships: ['relationship-2-x']
136+
}
141137
]
142138
},
143139
protocol: 'TCP',
@@ -162,24 +158,20 @@ describe('CalmRelationship', () => {
162158
);
163159

164160
const optionsRt = relationship.relationshipType as CalmOptionsRelationshipType;
165-
expect(optionsRt.options).toHaveLength(2);
166-
expect(optionsRt.options[0]).toBeInstanceOf(CalmOptionType);
167-
expect(optionsRt.options[1]).toBeInstanceOf(CalmOptionType);
168-
169-
expect(optionsRt.options[0].decisions).toEqual([
170-
{
171-
description: 'This is option 1',
172-
nodes: ['node-1'],
173-
relationships: ['relationship-1-x']
174-
}
175-
]);
176-
expect(optionsRt.options[1].decisions).toEqual([
177-
{
178-
description: 'This is option 2',
179-
nodes: ['node-2'],
180-
relationships: ['relationship-2-x']
181-
}
182-
]);
161+
expect(optionsRt.decisions).toHaveLength(2);
162+
expect(optionsRt.decisions[0]).toBeInstanceOf(CalmDecisionType);
163+
expect(optionsRt.decisions[1]).toBeInstanceOf(CalmDecisionType);
164+
165+
expect(optionsRt.decisions[0]).toEqual({
166+
description: 'This is option 1',
167+
nodes: ['node-1'],
168+
relationships: ['relationship-1-x']
169+
});
170+
expect(optionsRt.decisions[1]).toEqual({
171+
description: 'This is option 2',
172+
nodes: ['node-2'],
173+
relationships: ['relationship-2-x']
174+
});
183175
});
184176

185177
});

0 commit comments

Comments
 (0)