Repository: ChristopheBougere/asl-validator (ts)

Issue:
Duplicate state name is not caught during validation
**Describe the bug**
State names must be unique within the scope of the whole state machine. 

**To Reproduce**
PR is inbound w/ a test
```json
{
  "Comment": "Exhibits duplicate state error. State names MUST be unique within the scope of the whole state machine.",
  "StartAt": "Map",
  "States": {
    "Map": {
      "Type": "Map",
      "Next": "Final State",
      "InputPath": "$.input",
      "ItemsPath": "$.items",
      "MaxConcurrency": 0,
      "Iterator": {
        "StartAt": "Final State",
        "States": {
          "Final State": {
            "Type": "Wait",
            "Seconds": 20,
            "End": true
          }
        }
      },
      "ResultPath": "$.result"
    },
    "Final State": {
      "Type": "Pass",
      "End": true
    }
  }
}
```

**Expected behavior**
Report an error

**Version:** 
2.0.0

**Additional context**


Pull request:
enforce unique state names during validation
Added a check for duplicate state names during validation

this closes #80 

Patch:
diff --git a/src/lib/state-names.js b/src/lib/state-names.js
new file mode 100644
index 0000000..33d567a
--- /dev/null
+++ b/src/lib/state-names.js
@@ -0,0 +1,23 @@
+const jp = require('jsonpath');
+
+module.exports = (definition) => {
+  const errorMessages = [];
+  const names = new Map();
+  jp.query(definition, '$..[\'States\']')
+    .forEach((states) => {
+      Object.keys(states).forEach((stateName) => {
+        const current = names.get(stateName);
+        names.set(stateName, current ? current + 1 : 1);
+      });
+    });
+  names.forEach((value, key) => {
+    if (value > 1) {
+      errorMessages.push({
+        'Error code': 'DUPLICATE_STATE_NAMES',
+        Message: `A state with this name already exists: ${key}`,
+      });
+    }
+  });
+
+  return errorMessages;
+};
diff --git a/src/validator.js b/src/validator.js
index 0f8c956..d060f87 100644
--- a/src/validator.js
+++ b/src/validator.js
@@ -14,6 +14,7 @@ const errors = require('./schemas/errors');
 const checkJsonPath = require('./lib/json-path-errors');
 const missingTransitionTarget = require('./lib/missing-transition-target');
 const stateTransitions = require('./lib/state-transitions');
+const stateNames = require('./lib/state-names');
 
 function formatError(e) {
   const code = e.Code ? e.Code : e['Error code'];
@@ -37,32 +38,26 @@ function validator(definition) {
     ],
   });
 
-  // Validating JSON paths
-  const jsonPathErrors = checkJsonPath(definition);
-
-  // Check unreachable states
-  const missingTransitionTargetErrors = missingTransitionTarget(definition);
-
   // Validating JSON schemas
   const isJsonSchemaValid = ajv.validate('http://asl-validator.cloud/state-machine.json#', definition);
 
-  // Check for Parallel states
-  const transitionErrors = isJsonSchemaValid ? stateTransitions(definition) : [];
+  const postSchemaValidationErrors = [];
+  if (isJsonSchemaValid) {
+    postSchemaValidationErrors.push(...checkJsonPath(definition));
+    postSchemaValidationErrors.push(...missingTransitionTarget(definition));
+    postSchemaValidationErrors.push(...stateTransitions(definition));
+    postSchemaValidationErrors.push(...stateNames(definition));
+  }
 
   return {
-    isValid: isJsonSchemaValid && !jsonPathErrors.length && !missingTransitionTargetErrors.length
-        && !transitionErrors.length,
-    errors: jsonPathErrors.concat(ajv.errors || [])
-      .concat(missingTransitionTargetErrors || [])
-      .concat(transitionErrors || []),
+    isValid: isJsonSchemaValid && !postSchemaValidationErrors.length,
+    errors: (ajv.errors || []).concat(postSchemaValidationErrors || []),
     errorsText: (separator = '\n') => {
       const errorList = [];
-      errorList.push(jsonPathErrors.map(formatError).join(separator));
       if (ajv.errors) {
         errorList.push(ajv.errorsText(ajv.errors, { separator }));
       }
-      errorList.push(missingTransitionTargetErrors.map(formatError).join(separator));
-      errorList.push(transitionErrors.map(formatError).join(separator));
+      errorList.push(postSchemaValidationErrors.map(formatError).join(separator));
       return errorList.join(separator);
     },
   };


Test patch:
diff --git a/src/__tests__/definitions/invalid-map-dupe-state.json b/src/__tests__/definitions/invalid-map-dupe-state.json
new file mode 100644
index 0000000..ae04230
--- /dev/null
+++ b/src/__tests__/definitions/invalid-map-dupe-state.json
@@ -0,0 +1,28 @@
+{
+  "Comment": "Exhibits duplicate state error. State names MUST be unique within the scope of the whole state machine.",
+  "StartAt": "Map",
+  "States": {
+    "Map": {
+      "Type": "Map",
+      "Next": "Final State",
+      "InputPath": "$.input",
+      "ItemsPath": "$.items",
+      "MaxConcurrency": 0,
+      "Iterator": {
+        "StartAt": "Final State",
+        "States": {
+          "Final State": {
+            "Type": "Wait",
+            "Seconds": 20,
+            "End": true
+          }
+        }
+      },
+      "ResultPath": "$.result"
+    },
+    "Final State": {
+      "Type": "Pass",
+      "End": true
+    }
+  }
+}