Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ jobs:
- name: Install Dependencies
run: npm install

- name: Run Tests
- name: Run Unit Tests
run: npm test

11 changes: 9 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"test": "npx vitest semantic/specifications/"
"test": "npx vitest semantic/specifications/",
"bdd": "cucumber-js --loader ts-node/esm --import semantic/features/support/**/*.ts semantic/features"
},
"keywords": [],
"author": "",
Expand All @@ -13,6 +15,11 @@
"@accordproject/concerto-core": "^3.21.0",
"fs": "^0.0.1-security",
"path": "^0.12.7",
"vitest": "^3.2.2"
"vitest": "^3.2.1"
},
"devDependencies": {
"@cucumber/cucumber": "^11.3.0",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
}
}
42 changes: 42 additions & 0 deletions semantic/features/maps.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
Feature: Semantic Validation of CTO Map Specification

Scenario: Valid map key type should pass
Given I load the following models:
| model_file | alias |
| maps/models/MAP_KEY_TYPE_001/map_key_type_001_valid_key_type.cto | main |
When I validate the models
Then no error should be thrown

Scenario: Invalid map key type should throw error
Given I load the following models:
| model_file | alias |
| maps/models/MAP_KEY_TYPE_001/map_key_type_001_invalid_key_type.cto | main |
Then an error should be thrown with message "Expected \"DateTime\", \"String\""

Scenario: Valid map value type should pass
Given I load the following models:
| model_file | alias |
| maps/models/MAP_VALUE_TYPE_001/map_value_type_001_existing_value_type.cto | main |
When I validate the models
Then no error should be thrown

Scenario: Non-existent map value type should throw error
Given I load the following models:
| model_file | alias |
| maps/models/MAP_VALUE_TYPE_001/map_value_type_001_type_not_exist.cto | main |
When I validate the models
Then an error should be thrown with message "Cannot read properties of null"

Scenario: Duplicate map names should throw error
Given I load the following models:
| model_file | alias |
| maps/models/DECLARATION_001/declaration_001_duplicate_map_name.cto | main |
When I validate the models
Then an error should be thrown with message "Duplicate class name"

Scenario: Unique map names should pass
Given I load the following models:
| model_file | alias |
| maps/models/DECLARATION_001/declaration_001_unique_map_name.cto | main |
When I validate the models
Then no error should be thrown
73 changes: 73 additions & 0 deletions semantic/features/scalars.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
Feature: Semantic Validation of CTO Scalars specification

Scenario: should pass for valid number bounds
Given I load the following models:
| model_file |alias|
| scalars/models/NUMBER_VALIDATOR_001/number_validator_001_valid_bounds.cto |main|
Then no error should be thrown

Scenario: should throw for no number bounds
Given I load the following models:
| model_file |alias|
| scalars/models/NUMBER_VALIDATOR_001/number_validator_001_no_bounds.cto |main|
Then an error should be thrown with message "/Expected \"/"

Scenario: should pass for valid number range
Given I load the following models:
| model_file |alias|
| scalars/models/NUMBER_VALIDATOR_002/number_validator_002_valid_range.cto |main|
Then no error should be thrown

Scenario: should throw when lower > upper in number bounds
Given I load the following models:
| model_file |alias|
| scalars/models/NUMBER_VALIDATOR_002/number_validator_002_lower_greater_than_upper.cto |main|
Then an error should be thrown with message "/Lower bound must be less than or equal to upper bound/"

Scenario: should pass for valid string length bounds
Given I load the following models:
| model_file |alias|
| scalars/models/STRING_VALIDATOR_001/string_validator_001_valid_bounds.cto |main|
Then no error should be thrown

Scenario: should throw for empty string length bounds
Given I load the following models:
| model_file |alias|
| scalars/models/STRING_VALIDATOR_001/string_validator_001_no_bounds.cto |main|
Then an error should be thrown with message "/Expected \"/"

Scenario: should pass for positive string length bounds
Given I load the following models:
| model_file |alias|
| scalars/models/STRING_VALIDATOR_002/string_validator_002_positive_bounds.cto |main|
Then no error should be thrown

Scenario: should throw for negative bounds in string length
Given I load the following models:
| model_file |alias|
| scalars/models/STRING_VALIDATOR_002/string_validator_002_negative_bounds.cto |main|
Then an error should be thrown with message "/minLength and-or maxLength must be positive integers/"

Scenario: should pass when lower < upper in string length
Given I load the following models:
| model_file |alias|
| scalars/models/STRING_VALIDATOR_003/string_validator_003_valid_order.cto |main|
Then no error should be thrown

Scenario: should throw when lower > upper in string length
Given I load the following models:
| model_file |alias|
| scalars/models/STRING_VALIDATOR_003/string_validator_003_lower_greater_than_upper.cto |main|
Then an error should be thrown with message "/minLength must be less than or equal to maxLength/"

Scenario: should throw for invalid regex
Given I load the following models:
| model_file |alias|
| scalars/models/STRING_VALIDATOR_004/string_validator_004_valid_regex.cto |main|
Then no error should be thrown

Scenario: should pass for valid regex pattern
Given I load the following models:
| model_file |alias|
| scalars/models/STRING_VALIDATOR_004/string_validator_004_invalid_regex.cto |main|
Then an error should be thrown with message "/Expected comment/"
51 changes: 51 additions & 0 deletions semantic/features/support/steps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Given, When, Then } from '@cucumber/cucumber';
import { loadCTO } from './utils/loadCTO.ts';
import assert from 'assert';

Given('I load the following models:', function (dataTable) {
for (const row of dataTable.hashes()) {
const modelContent = loadCTO(row.model_file);
try {
this.modelManager.addCTOModel(modelContent, row.model_file, true);
} catch (err) {
this.error = err as Error;
break;
}
}
});

When('I validate the models', function () {
try {
this.modelManager.validateModelFiles();
this.error = null;
} catch (err) {
this.error = err as Error;
}
});

Then('an error should be thrown with message {string}', function (expected: string) {
assert(this.error, 'Expected an error to be thrown, but none was');

let isMatch: boolean;
const match = expected.match(/^\/(.+)\/([gimsuy]*)?$/);

if (match) {
const pattern = new RegExp(match[1], match[2]);
isMatch = pattern.test(this.error.message);
assert(
isMatch,
`Expected error to match regex ${pattern}, but got: "${this.error.message}"`
);
} else {
isMatch = this.error.message.includes(expected);
assert(
isMatch,
`Expected error to include "${expected}", but got: "${this.error.message}"`
);
}
});


Then('no error should be thrown', function () {
assert.strictEqual(this.error, null, `Expected no error, but got: ${this.error?.message}`);
});
7 changes: 7 additions & 0 deletions semantic/features/support/utils/loadCTO.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import fs from 'fs';
import path from 'path';

export const loadCTO = (relativePath: string): string => {
const basePath = path.resolve('semantic/specifications/');
return fs.readFileSync(path.join(basePath, relativePath), 'utf8');
};
14 changes: 14 additions & 0 deletions semantic/features/support/world.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { setWorldConstructor, IWorldOptions } from '@cucumber/cucumber';
import { ModelManager } from '@accordproject/concerto-core';

export class CustomWorld {
public modelManager: ModelManager;
public error: Error | null;

constructor(options: IWorldOptions) {
this.modelManager = new ModelManager({ enableMapType: true });
this.error = null;
}
}

setWorldConstructor(CustomWorld);
17 changes: 17 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "Node16",
"moduleResolution": "Node16",
"allowImportingTsExtensions": true,
"noEmit": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true
},
"include": ["semantic/features/**/*.ts"],
"exclude": ["node_modules"]
}
Loading