Repository: asyncapi/parser-js
Pull request #913 (2023-11-28 09:27:09)

feat: add Spectral rules to validate required operation channe and channel servers field
**Description**

Part of https://github.com/asyncapi/spec/issues/991

Despite the name of the branch, this PR adds two new Spectral rules (packed all together to minimize review efforts and to release asap).

This PR adds [the last two new  Spectral rules](https://github.com/asyncapi/spec/issues/991#issuecomment-1825700075) related with https://github.com/asyncapi/spec/issues/991.

1. Checks that the `channel` field of a required operation (under root) points to a required (under root) channel and not to an optional (under components) channel, no matter if from the same document or from an external.
2. Checks that the `servers` field of a required channel (under root) points to a subset of required (under root) servers.

**Related issue(s)**
https://github.com/asyncapi/spec/issues/991

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=asyncapi_parser-js&pullRequest=913)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=asyncapi_parser-js&pullRequest=913) No Coverage information  
[![No Duplication information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/NoDuplicationInfo-16px.png 'No Duplication information')](https://sonarcloud.io/component_measures?id=asyncapi_parser-js&pullRequest=913&metric=duplicated_lines_density&view=list) No Duplication information


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=asyncapi_parser-js&pullRequest=913)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=asyncapi_parser-js&pullRequest=913) No Coverage information  
[![No Duplication information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/NoDuplicationInfo-16px.png 'No Duplication information')](https://sonarcloud.io/component_measures?id=asyncapi_parser-js&pullRequest=913&metric=duplicated_lines_density&view=list) No Duplication information


smoya: > I'm sorry but I can't approve until I see "Spectral rules master" added as a skill in your LinkedIn.

Oops, something might went wrong. Can you please check again?


smoya: @jonaslagoni @fmvilas FYI I've added to this PR also another rule that checks that the servers field of a required channel (under root) points to a subset of required (under root) servers.
sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=asyncapi_parser-js&pullRequest=913)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=asyncapi_parser-js&pullRequest=913&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=asyncapi_parser-js&pullRequest=913) No Coverage information  
[![No Duplication information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/NoDuplicationInfo-16px.png 'No Duplication information')](https://sonarcloud.io/component_measures?id=asyncapi_parser-js&pullRequest=913&metric=duplicated_lines_density&view=list) No Duplication information


smoya: /rtm

Patch:
diff --git a/src/ruleset/v3/ruleset.ts b/src/ruleset/v3/ruleset.ts
index 56f7b68..724339e 100644
--- a/src/ruleset/v3/ruleset.ts
+++ b/src/ruleset/v3/ruleset.ts
@@ -2,6 +2,7 @@
 
 import { AsyncAPIFormats } from '../formats';
 import { operationMessagesUnambiguity } from './functions/operationMessagesUnambiguity';
+import { pattern } from '@stoplight/spectral-functions';
 
 export const v3CoreRuleset = {
   description: 'Core AsyncAPI 3.x.x ruleset.',
@@ -24,5 +25,37 @@ export const v3CoreRuleset = {
         function: operationMessagesUnambiguity,
       },
     },
+    'asyncapi3-required-operation-channel-unambiguity': {
+      description: 'The "channel" field of an operation under the root "operations" object must always reference a channel under the root "channels" object.',
+      severity: 'error',
+      recommended: true,
+      resolved: false, // We use the JSON pointer to match the channel.
+      given: '$.operations.*',      
+      then: {
+        field: 'channel.$ref',
+        function: pattern,
+        functionOptions: {
+          match: '#\\/channels\\/', // If doesn't match, rule fails.
+        },
+      },
+    },
+    
+    /**
+     * Channel Object rules
+     */
+    'asyncapi3-required-channel-servers-unambiguity': {
+      description: 'The "servers" field of a channel under the root "channels" object must always reference a subset of the servers under the root "servers" object.',
+      severity: 'error',
+      recommended: true,
+      resolved: false, // We use the JSON pointer to match the channel.
+      given: '$.channels.*',      
+      then: {
+        field: '$.servers.*.$ref',
+        function: pattern,
+        functionOptions: {
+          match: '#\\/servers\\/', // If doesn't match, rule fails.
+        },
+      },
+    }
   },
 };


Test patch:
diff --git a/test/ruleset/rules/v3/asyncapi3-required-channel-servers-unambiguity.spec.ts b/test/ruleset/rules/v3/asyncapi3-required-channel-servers-unambiguity.spec.ts
new file mode 100644
index 0000000..1a299ee
--- /dev/null
+++ b/test/ruleset/rules/v3/asyncapi3-required-channel-servers-unambiguity.spec.ts
@@ -0,0 +1,210 @@
+import { testRule, DiagnosticSeverity } from '../../tester';
+
+testRule('asyncapi3-required-channel-servers-unambiguity', [
+  {
+    name: 'valid case - required channel (under root) server field points to a subset of required servers (under root)',
+    document: {
+      asyncapi: '3.0.0',
+      info: {
+        title: 'Account Service',
+        version: '1.0.0'
+      },
+      servers: {
+        prod: {
+          host: 'my-api.com',
+          protocol: 'ws',
+        },
+        dev: {
+          host: 'localhost',
+          protocol: 'ws',
+        },
+      },
+      channels: {
+        UserSignedUp: {
+          servers: [
+            { $ref: '#/servers/prod' },
+            { $ref: '#/servers/dev' },
+          ]
+        }
+      },
+    },
+    errors: [],
+  },
+  {
+    name: 'valid case - required channel (under root) server field points to a subset of required servers (under root) from an external doc',
+    document: {
+      asyncapi: '3.0.0',
+      info: {
+        title: 'Account Service',
+        version: '1.0.0'
+      },
+      channels: {
+        UserSignedUp: {
+          servers: [
+            { $ref: 'http://foo.bar/components/file.yml#/servers/prod' },
+            { $ref: 'http://foo.bar/components/file.yml#/servers/dev' },
+          ]
+        }
+      },
+    },
+    errors: [],
+  },
+  {
+    name: 'valid case - optional channel (under components) server field points to a subset of required servers (under root)',
+    document: {
+      asyncapi: '3.0.0',
+      info: {
+        title: 'Account Service',
+        version: '1.0.0'
+      },
+      servers: {
+        prod: {
+          host: 'my-api.com',
+          protocol: 'ws',
+        },
+        dev: {
+          host: 'localhost',
+          protocol: 'ws',
+        },
+      },
+      components: {
+        channels: {
+          UserSignedUp: {
+            servers: [
+              { $ref: '#/servers/prod' },
+              { $ref: '#/servers/dev' },
+            ]
+          }
+        },
+      },
+    },
+    errors: [],
+  },
+  {
+    name: 'valid case - optional channel (under components) server field points to a subset of optional servers (under components)',
+    document: {
+      asyncapi: '3.0.0',
+      info: {
+        title: 'Account Service',
+        version: '1.0.0'
+      },
+      components: {
+        servers: {
+          prod: {
+            host: 'my-api.com',
+            protocol: 'ws',
+          },
+          dev: {
+            host: 'localhost',
+            protocol: 'ws',
+          },
+        },
+        channels: {
+          UserSignedUp: {
+            messages: {
+              UserSignedUp: {
+                payload: {
+                  type: 'object',
+                  properties: {
+                    displayName: {
+                      type: 'string'
+                    },
+                    email: {
+                      type: 'string'
+                    }
+                  }
+                }
+              }
+            }
+          }
+        },
+        operations: {
+          UserSignedUp: {
+            action: 'send',
+            channel: {
+              $ref: '#/components/channels/UserSignedUp'
+            },
+            messages: [
+              {
+                $ref: '#/components/channels/UserSignedUp/messages/UserSignedUp'
+              }
+            ]
+          }
+        }
+      }
+    },
+    errors: [],
+  },
+  {
+    name: 'invalid case - required channel (in root) servers field points to a subset of optional servers (under components)',
+    document: {
+      asyncapi: '3.0.0',
+      info: {
+        title: 'Account Service',
+        version: '1.0.0'
+      },
+      channels: {
+        UserSignedUp: {
+          servers: [
+            { $ref: '#/components/servers/prod' },
+            { $ref: '#/components/servers/dev' },
+          ]
+        }
+      },
+      components: {
+        servers: {
+          prod: {
+            host: 'my-api.com',
+            protocol: 'ws',
+          },
+          dev: {
+            host: 'localhost',
+            protocol: 'ws',
+          },
+        }
+      }
+    },
+    errors: [
+      {
+        message: 'The "servers" field of a channel under the root "channels" object must always reference a subset of the servers under the root "servers" object.',
+        path: ['channels', 'UserSignedUp', 'servers', '0', '$ref'],
+        severity: DiagnosticSeverity.Error,
+      },
+      {
+        message: 'The "servers" field of a channel under the root "channels" object must always reference a subset of the servers under the root "servers" object.',
+        path: ['channels', 'UserSignedUp', 'servers', '1', '$ref'],
+        severity: DiagnosticSeverity.Error,
+      }
+    ],
+  },
+  {
+    name: 'invalid case - required channel (in root) servers field points to a subset of optional servers (under components) from an external doc',
+    document: {
+      asyncapi: '3.0.0',
+      info: {
+        title: 'Account Service',
+        version: '1.0.0'
+      },
+      channels: {
+        UserSignedUp: {
+          servers: [
+            { $ref: 'http://foo.bar/components/file.yml#/components/servers/prod' },
+            { $ref: 'http://foo.bar/components/file.yml#/components/servers/dev' },
+          ]
+        }
+      }
+    },
+    errors: [
+      {
+        message: 'The "servers" field of a channel under the root "channels" object must always reference a subset of the servers under the root "servers" object.',
+        path: ['channels', 'UserSignedUp', 'servers', '0', '$ref'],
+        severity: DiagnosticSeverity.Error,
+      },
+      {
+        message: 'The "servers" field of a channel under the root "channels" object must always reference a subset of the servers under the root "servers" object.',
+        path: ['channels', 'UserSignedUp', 'servers', '1', '$ref'],
+        severity: DiagnosticSeverity.Error,
+      }
+    ],
+  },
+]);
diff --git a/test/ruleset/rules/v3/asyncapi3-required-operation-channel-unambiguity.spec.ts b/test/ruleset/rules/v3/asyncapi3-required-operation-channel-unambiguity.spec.ts
new file mode 100644
index 0000000..44e269b
--- /dev/null
+++ b/test/ruleset/rules/v3/asyncapi3-required-operation-channel-unambiguity.spec.ts
@@ -0,0 +1,261 @@
+import { testRule, DiagnosticSeverity } from '../../tester';
+
+testRule('asyncapi3-required-operation-channel-unambiguity', [
+  {
+    name: 'valid case - required operation (under root) channel field points to a required channel (under root)',
+    document: {
+      asyncapi: '3.0.0',
+      info: {
+        title: 'Account Service',
+        version: '1.0.0'
+      },
+      channels: {
+        UserSignedUp: {
+          messages: {
+            UserSignedUp: {
+              payload: {
+                type: 'object',
+                properties: {
+                  displayName: {
+                    type: 'string'
+                  },
+                  email: {
+                    type: 'string'
+                  }
+                }
+              }
+            }
+          }
+        }
+      },
+      operations: {
+        UserSignedUp: {
+          action: 'send',
+          channel: {
+            $ref: '#/channels/UserSignedUp'
+          },
+          messages: [
+            {
+              $ref: '#/channels/UserSignedUp/messages/UserSignedUp'
+            }
+          ]
+        }
+      }
+    },
+    errors: [],
+  },
+  {
+    name: 'valid case - required operation (under root) channel field points to a required channel (under root) from an external doc',
+    document: {
+      asyncapi: '3.0.0',
+      info: {
+        title: 'Account Service',
+        version: '1.0.0'
+      },
+      channels: {
+        UserSignedUp: {
+          messages: {
+            UserSignedUp: {
+              payload: {
+                type: 'object',
+                properties: {
+                  displayName: {
+                    type: 'string'
+                  },
+                  email: {
+                    type: 'string'
+                  }
+                }
+              }
+            }
+          }
+        }
+      },
+      operations: {
+        UserSignedUp: {
+          action: 'send',
+          channel: {
+            $ref: 'http://foo.bar/components/file.yml#/channels/UserSignedUp'
+          },
+          messages: [
+            {
+              $ref: 'http://foo.bar/components/file.yml#/channels/UserSignedUp/messages/UserSignedUp'
+            }
+          ]
+        }
+      }
+    },
+    errors: [],
+  },
+  {
+    name: 'valid case - optional operation (under components) channel field points to a required channel (under root)',
+    document: {
+      asyncapi: '3.0.0',
+      info: {
+        title: 'Account Service',
+        version: '1.0.0'
+      },
+      channels: {
+        UserSignedUp: {
+          messages: {
+            UserSignedUp: {
+              payload: {
+                type: 'object',
+                properties: {
+                  displayName: {
+                    type: 'string'
+                  },
+                  email: {
+                    type: 'string'
+                  }
+                }
+              }
+            }
+          }
+        }
+      },
+      components: {
+        operations: {
+          UserSignedUp: {
+            action: 'send',
+            channel: {
+              $ref: '#/channels/UserSignedUp'
+            },
+            messages: [
+              {
+                $ref: '#/channels/UserSignedUp/messages/UserSignedUp'
+              }
+            ]
+          }
+        }
+      }
+    },
+    errors: [],
+  },
+  {
+    name: 'valid case - optional operation (under components) channel field points to an optional channel (under components)',
+    document: {
+      asyncapi: '3.0.0',
+      info: {
+        title: 'Account Service',
+        version: '1.0.0'
+      },
+      components: {
+        channels: {
+          UserSignedUp: {
+            messages: {
+              UserSignedUp: {
+                payload: {
+                  type: 'object',
+                  properties: {
+                    displayName: {
+                      type: 'string'
+                    },
+                    email: {
+                      type: 'string'
+                    }
+                  }
+                }
+              }
+            }
+          }
+        },
+        operations: {
+          UserSignedUp: {
+            action: 'send',
+            channel: {
+              $ref: '#/components/channels/UserSignedUp'
+            },
+            messages: [
+              {
+                $ref: '#/components/channels/UserSignedUp/messages/UserSignedUp'
+              }
+            ]
+          }
+        }
+      }
+    },
+    errors: [],
+  },
+  {
+    name: 'invalid case - required operation (in root) channel field points to an optional channel (under components)',
+    document: {
+      asyncapi: '3.0.0',
+      info: {
+        title: 'Account Service',
+        version: '1.0.0'
+      },
+      operations: {
+        UserSignedUp: {
+          action: 'send',
+          channel: {
+            $ref: '#/components/channels/UserSignedUp'
+          },
+          messages: [
+            {
+              $ref: '#/components/channels/UserSignedUp/messages/UserSignedUp'
+            }
+          ]
+        }
+      },
+      components: {
+        channels: {
+          UserSignedUp: {
+            messages: {
+              UserSignedUp: {
+                payload: {
+                  type: 'object',
+                  properties: {
+                    displayName: {
+                      type: 'string'
+                    },
+                    email: {
+                      type: 'string'
+                    }
+                  }
+                }
+              }
+            }
+          }
+        },
+      },
+    },
+    errors: [
+      {
+        message: 'The "channel" field of an operation under the root "operations" object must always reference a channel under the root "channels" object.',
+        path: ['operations', 'UserSignedUp', 'channel', '$ref'],
+        severity: DiagnosticSeverity.Error,
+      }
+    ],
+  },
+  {
+    name: 'invalid case - required operation (in root) channel field points to an optional channel (under components) from an external doc',
+    document: {
+      asyncapi: '3.0.0',
+      info: {
+        title: 'Account Service',
+        version: '1.0.0'
+      },
+      operations: {
+        UserSignedUp: {
+          action: 'send',
+          channel: {
+            $ref: 'http://foo.bar/components/file.yml#/components/channels/UserSignedUp'
+          },
+          messages: [
+            {
+              $ref: 'http://foo.bar/components/file.yml#/components/channels/UserSignedUp/messages/UserSignedUp'
+            }
+          ]
+        }
+      }
+    },
+    errors: [
+      {
+        message: 'The "channel" field of an operation under the root "operations" object must always reference a channel under the root "channels" object.',
+        path: ['operations', 'UserSignedUp', 'channel', '$ref'],
+        severity: DiagnosticSeverity.Error,
+      }
+    ],
+  },
+]);