Skip to content

Commit

Permalink
New centralised model (#790)
Browse files Browse the repository at this point in the history
  • Loading branch information
LeighFinegold committed Feb 16, 2025
1 parent 768b4e9 commit 94e0ad9
Show file tree
Hide file tree
Showing 28 changed files with 1,421 additions and 7 deletions.
2 changes: 1 addition & 1 deletion calm/draft/2025-01/meta/core.json
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@
"network",
"ldap",
"webclient",
"data-assset"
"data-asset"
]
},
"interacts-type": {
Expand Down
6 changes: 3 additions & 3 deletions calm/draft/2025-01/meta/flow.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@
"sequence-number",
"summary"
]
},
"minItems": 1
}
},
"metadata": {
"type": "array",
Expand Down Expand Up @@ -63,7 +62,8 @@
"type": "array",
"items": {
"$ref": "#/defs/transition"
}
},
"minItems": 1
},
"controls": {
"$ref": "control.json#/defs/controls"
Expand Down
35 changes: 35 additions & 0 deletions shared/src/model/control-requirement.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { CalmControlRequirement } from './control-requirement.js'; // Importing the model
import { CalmControlRequirementSchema } from '../types/control-requirement-types.js'; // Importing the schema

// Mock data for testing
const controlRequirementData: CalmControlRequirementSchema = {
'control-id': 'control-123',
name: 'Test Control Requirement',
description: 'This is a description of the test control requirement.'
};

describe('CalmControlRequirement', () => {
it('should create a CalmControlRequirement instance from JSON data', () => {
const controlRequirement = CalmControlRequirement.fromJson(controlRequirementData);

expect(controlRequirement).toBeInstanceOf(CalmControlRequirement);
expect(controlRequirement.controlId).toBe('control-123');
expect(controlRequirement.name).toBe('Test Control Requirement');
expect(controlRequirement.description).toBe('This is a description of the test control requirement.');
});

it('should handle missing description', () => {
const controlRequirementWithNoDescription: CalmControlRequirementSchema = {
'control-id': 'control-124',
name: 'Control with No Description',
description: ''
};
const controlRequirement = CalmControlRequirement.fromJson(controlRequirementWithNoDescription);

expect(controlRequirement).toBeInstanceOf(CalmControlRequirement);
expect(controlRequirement.controlId).toBe('control-124');
expect(controlRequirement.name).toBe('Control with No Description');
expect(controlRequirement.description).toBe('');
});

});
17 changes: 17 additions & 0 deletions shared/src/model/control-requirement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {CalmControlRequirementSchema} from '../types/control-requirement-types.js';

export class CalmControlRequirement {
constructor(
public controlId: string,
public name: string,
public description: string
) {}

static fromJson(data: CalmControlRequirementSchema): CalmControlRequirement {
return new CalmControlRequirement(
data['control-id'],
data['name'],
data['description']
);
}
}
83 changes: 83 additions & 0 deletions shared/src/model/control.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { CalmControl, CalmControlDetail } from './control.js';
import { CalmControlDetailSchema, CalmControlsSchema } from '../types/control-types.js';

const controlDetailData: CalmControlDetailSchema = {
'control-requirement-url': 'https://example.com/requirement',
'control-config-url': 'https://example.com/config'
};

const controlData: CalmControlsSchema = {
'control-1': {
description: 'Test Control 1',
requirements: [controlDetailData]
},
'control-2': {
description: 'Test Control 2',
requirements: [controlDetailData]
}
};

describe('CalmControlDetail', () => {
it('should create a CalmControlDetail instance from JSON data', () => {
const controlDetail = CalmControlDetail.fromJson(controlDetailData);

expect(controlDetail).toBeInstanceOf(CalmControlDetail);
expect(controlDetail.controlRequirementUrl).toBe('https://example.com/requirement');
expect(controlDetail.controlConfigUrl).toBe('https://example.com/config');
});
});

describe('CalmControl', () => {
it('should create a CalmControl instance from JSON data', () => {
const controls = CalmControl.fromJson(controlData);

expect(controls).toHaveLength(2);
expect(controls[0]).toBeInstanceOf(CalmControl);
expect(controls[0].controlId).toBe('control-1');
expect(controls[0].description).toBe('Test Control 1');
expect(controls[0].requirements).toHaveLength(1);
expect(controls[0].requirements[0]).toBeInstanceOf(CalmControlDetail);
expect(controls[0].requirements[0].controlRequirementUrl).toBe('https://example.com/requirement');
expect(controls[0].requirements[0].controlConfigUrl).toBe('https://example.com/config');
});

it('should handle an empty ControlsSchema', () => {
const emptyControls: CalmControlsSchema = {};
const controls = CalmControl.fromJson(emptyControls);

expect(controls).toHaveLength(0);
});

it('should handle missing requirements in Control', () => {
const controlWithNoRequirements: CalmControlsSchema = {
'control-3': {
description: 'Control with no requirements',
requirements: []
}
};
const controls = CalmControl.fromJson(controlWithNoRequirements);

expect(controls).toHaveLength(1);
expect(controls[0].requirements).toHaveLength(0);
});

it('should handle multiple controls and requirements', () => {
const controlWithMultipleRequirements: CalmControlsSchema = {
'control-4': {
description: 'Control with multiple requirements',
requirements: [
{ 'control-requirement-url': 'https://example.com/requirement-1', 'control-config-url': 'https://example.com/config-1' },
{ 'control-requirement-url': 'https://example.com/requirement-2', 'control-config-url': 'https://example.com/config-2' }
]
}
};

const controls = CalmControl.fromJson(controlWithMultipleRequirements);

expect(controls).toHaveLength(1);
expect(controls[0].controlId).toBe('control-4');
expect(controls[0].requirements).toHaveLength(2);
expect(controls[0].requirements[0].controlRequirementUrl).toBe('https://example.com/requirement-1');
expect(controls[0].requirements[1].controlRequirementUrl).toBe('https://example.com/requirement-2');
});
});
35 changes: 35 additions & 0 deletions shared/src/model/control.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {CalmControlDetailSchema, CalmControlsSchema} from '../types/control-types.js';

export class CalmControlDetail {
constructor(
public controlRequirementUrl: string,
public controlConfigUrl: string
) {}

static fromJson(data: CalmControlDetailSchema): CalmControlDetail {
return new CalmControlDetail(
data['control-requirement-url'],
data['control-config-url']
);
}
}

export class CalmControl {
constructor(
public controlId: string,
public description: string,
public requirements: CalmControlDetail[]
) {}

static fromJson(data: CalmControlsSchema): CalmControl[] {
if(!data) return [];
return Object.entries(data).map(([controlId, controlData]) =>
new CalmControl(
controlId,
controlData.description,
controlData.requirements.map(CalmControlDetail.fromJson)
)
);
}

}
85 changes: 85 additions & 0 deletions shared/src/model/core.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { CalmCore } from './core.js';
import { CalmCoreSchema } from '../types/core-types.js';

const coreData: CalmCoreSchema = {
nodes: [
{
'unique-id': 'node-001',
'node-type': 'system',
name: 'Test Node',
description: 'This is a test node',
details: {
'detailed-architecture': 'https://example.com/architecture',
'required-pattern': 'https://example.com/pattern'
},
interfaces: [{ 'unique-id': 'interface-001', hostname: 'localhost'}],
controls: { 'control-001': { description: 'Test control', requirements: [{ 'control-requirement-url': 'https://example.com/requirement', 'control-config-url': 'https://example.com/config' }] } },
metadata: [{ key: 'value' }],
'data-classification': 'Public',
'run-as': 'admin',
instance: 'instance-1'
}
],
relationships: [
{
'unique-id': 'relationship-001',
description: 'Test Relationship',
'relationship-type': {
interacts: {
actor: 'actor-001',
nodes: ['node-001', 'node-002']
}
},
protocol: 'HTTP',
authentication: 'OAuth2',
metadata: [{ key: 'value' }],
controls: { 'control-001': { description: 'Test control', requirements: [{ 'control-requirement-url': 'https://example.com/requirement', 'control-config-url': 'https://example.com/config' }] } }
}
],
metadata: [{ key: 'value' }],
controls: { 'control-001': { description: 'Test control', requirements: [{ 'control-requirement-url': 'https://example.com/requirement', 'control-config-url': 'https://example.com/config' }] } },
flows: []
};

describe('CalmCore', () => {
it('should create a CalmCore instance from CoreSchema data', () => {
const core = CalmCore.fromJson(coreData);

expect(core).toBeInstanceOf(CalmCore);
expect(core.nodes).toHaveLength(1);
expect(core.relationships).toHaveLength(1);
expect(core.metadata).toEqual({ data: { key: 'value' } });
expect(core.controls).toHaveLength(1);
expect(core.controls[0].controlId).toBe('control-001');
expect(core.flows).toHaveLength(0);
});

it('should handle optional fields in CalmCore', () => {
const coreDataWithoutOptionalFields: CalmCoreSchema = {
nodes: [
{
'unique-id': 'node-002',
'node-type': 'service',
name: 'Another Test Node',
description: 'Another test node description',
details: {
'detailed-architecture': 'https://example.com/architecture-2',
'required-pattern': 'https://example.com/pattern-2'
},
interfaces: [{ 'unique-id': 'interface-001', hostname: 'localhost'}],
controls: { 'control-002': { description: 'Another test control', requirements: [{ 'control-requirement-url': 'https://example.com/requirement2', 'control-config-url': 'https://example.com/config2' }] } },
metadata: [{ key: 'value' }]
}
],
metadata: [{ key: 'value' }],
controls: { 'control-002': { description: 'Another test control', requirements: [{ 'control-requirement-url': 'https://example.com/requirement2', 'control-config-url': 'https://example.com/config2' }] } },
};

const coreWithoutOptionalFields = CalmCore.fromJson(coreDataWithoutOptionalFields);

expect(coreWithoutOptionalFields).toBeInstanceOf(CalmCore);
expect(coreWithoutOptionalFields.nodes).toHaveLength(1);
expect(coreWithoutOptionalFields.relationships).toHaveLength(0);
expect(coreWithoutOptionalFields.flows).toHaveLength(0);
});
});
29 changes: 29 additions & 0 deletions shared/src/model/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { CalmNode } from './node.js';
import { CalmRelationship } from './relationship.js';
import { CalmFlow } from './flow.js';
import { CalmControl } from './control.js';
import { CalmMetadata } from './metadata.js';
import { CalmCoreSchema } from '../types/core-types.js';

export class CalmCore {
constructor(
public nodes: CalmNode[],
public relationships: CalmRelationship[],
public metadata: CalmMetadata,
public controls: CalmControl[],
public flows: CalmFlow[]
) {}

static fromJson(data: CalmCoreSchema): CalmCore {
return new CalmCore(
data.nodes? data.nodes.map(CalmNode.fromJson) : [],
data.relationships? data.relationships.map(CalmRelationship.fromJson) : [],
data.metadata? CalmMetadata.fromJson(data.metadata) : new CalmMetadata({}),
data.controls? CalmControl.fromJson(data.controls) : [],
data.flows? data.flows.map(CalmFlow.fromJson) : []
);
}
}

export { CalmCore as Architecture };
export { CalmCore as Pattern };
72 changes: 72 additions & 0 deletions shared/src/model/flow.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { CalmFlow, CalmFlowTransition } from './flow.js';
import { CalmControl } from './control.js';
import { CalmMetadata } from './metadata.js';
import {CalmFlowSchema} from '../types/flow-types.js';

describe('CalmFlow', () => {
it('should create an instance with given properties', () => {
const transitions = [
new CalmFlowTransition('rel-1', 1, 'First transition'),
new CalmFlowTransition('rel-2', 2, 'Second transition', 'destination-to-source')
];

const controls = [
new CalmControl('ctrl-1', 'Test Control 1', []),
new CalmControl('ctrl-2', 'Test Control 2', [])
];

const metadata = new CalmMetadata({ key: 'value' });

const flow = new CalmFlow('flow-123', 'Test Flow', 'A test description', transitions, 'http://requirement.url', controls, metadata);

expect(flow.uniqueId).toBe('flow-123');
expect(flow.name).toBe('Test Flow');
expect(flow.description).toBe('A test description');
expect(flow.transitions).toHaveLength(2);
expect(flow.transitions[0]).toBeInstanceOf(CalmFlowTransition);
expect(flow.controls).toBeDefined();
expect(flow.controls).toHaveLength(2);
expect(flow.metadata).toBeInstanceOf(CalmMetadata);
});

it('should create an instance from JSON data', () => {
const jsonData: CalmFlowSchema = {
'unique-id': 'flow-456',
'name': 'JSON Flow',
'description': 'Flow created from JSON',
'transitions': [
{
'relationship-unique-id': 'rel-1',
'sequence-number': 1,
'summary': 'Transition 1'
},
{
'relationship-unique-id': 'rel-2',
'sequence-number': 2,
'summary': 'Transition 2',
'direction': 'destination-to-source' // Explicitly providing direction
}
],
'requirement-url': 'http://json.requirement.url',
'controls': {
'ctrl-1': { 'description': 'JSON Control 1', 'requirements': [] }
},
'metadata': [{ 'key': 'value' }]
};

const flow = CalmFlow.fromJson(jsonData);


expect(flow).toBeInstanceOf(CalmFlow);
expect(flow.uniqueId).toBe('flow-456');
expect(flow.name).toBe('JSON Flow');
expect(flow.description).toBe('Flow created from JSON');
expect(flow.transitions).toHaveLength(2);
expect(flow.transitions[0]).toBeInstanceOf(CalmFlowTransition);
expect(flow.controls).toBeDefined();
expect(flow.controls[0].controlId).toBe('ctrl-1');
expect(flow.controls[0].description).toBe('JSON Control 1');
expect(flow.metadata).toBeDefined();
});

});
Loading

0 comments on commit 94e0ad9

Please sign in to comment.