├── .changeset ├── README.md └── config.json ├── .eslintrc.json ├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .lintstagedrc.yml ├── .nvmrc ├── LICENSE ├── README.md ├── babel.config.js ├── codecov.yml ├── husky.config.js ├── lerna.json ├── mocha-setup.js ├── package.json ├── tsconfig.json ├── validator-analyse ├── .eslintignore ├── .eslintrc.json ├── .npmignore ├── CHANGELOG.md ├── README.md ├── babel.config.js ├── checks │ ├── analyse-representation.spec.ts │ ├── analyse-representation.ts │ ├── api-documentation │ │ ├── ensure-single-resource.spec.ts │ │ ├── ensure-single-resource.ts │ │ ├── entrypointCheck.ts │ │ ├── hasSupportedClasses.ts │ │ ├── index.spec.ts │ │ └── index.ts │ ├── response │ │ ├── api-doc-link.spec.ts │ │ ├── api-doc-link.ts │ │ ├── status-code.spec.ts │ │ └── status-code.ts │ ├── url-resolvable.spec.ts │ └── url-resolvable.ts ├── index.ts ├── jest.config.js ├── package.json └── tsconfig.json ├── validator-cli ├── .eslintignore ├── .eslintrc.json ├── CHANGELOG.md ├── README.md ├── index.ts ├── package.json └── tsconfig.json ├── validator-core ├── .babelrc ├── .eslintignore ├── .eslintrc.json ├── .npmignore ├── CHANGELOG.md ├── README.md ├── index.ts ├── jest.config.js ├── namespace.ts ├── package.json ├── run-checks.spec.ts ├── run-checks.ts └── tsconfig.json ├── validator-e2e ├── .eslintignore ├── .npmignore ├── CHANGELOG.md ├── README.md ├── babel.config.json ├── example │ ├── data-cube-curation.hydra │ ├── flick-mis-traemli.hydra │ └── hydracg-movies.hydra ├── index.spec.ts ├── index.ts ├── lib │ ├── checkRunner.spec.ts │ ├── checkRunner.ts │ ├── comparison.spec.ts │ ├── comparison.ts │ ├── docsLoader.ts │ ├── headers.ts │ ├── steps │ │ ├── StepSpyMixin.spec.ts │ │ ├── StepSpyMixin.ts │ │ ├── constraints │ │ │ ├── Constraint.ts │ │ │ ├── PropertyConstraint.spec.ts │ │ │ ├── PropertyConstraint.ts │ │ │ ├── StatusConstraint.spec.ts │ │ │ ├── StatusConstraint.ts │ │ │ ├── conditions │ │ │ │ ├── index.spec.ts │ │ │ │ └── index.ts │ │ │ ├── index.spec.ts │ │ │ └── index.ts │ │ ├── factory.ts │ │ ├── index.ts │ │ ├── representation │ │ │ ├── identifier │ │ │ │ ├── index.spec.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── link │ │ │ │ ├── follow.spec.ts │ │ │ │ ├── follow.ts │ │ │ │ ├── index.spec.ts │ │ │ │ └── index.ts │ │ │ ├── operation │ │ │ │ ├── index.spec.ts │ │ │ │ ├── index.ts │ │ │ │ ├── invocation.spec.ts │ │ │ │ └── invocation.ts │ │ │ └── property │ │ │ │ ├── index.spec.ts │ │ │ │ └── index.ts │ │ ├── response │ │ │ ├── header.spec.ts │ │ │ ├── header.ts │ │ │ ├── status.spec.ts │ │ │ └── status.ts │ │ └── stub.ts │ ├── strictRunVerification.spec.ts │ ├── strictRunVerification.ts │ └── testHelpers.ts ├── package.json ├── tsconfig.json └── types.ts ├── validator-ui ├── .browserslistrc ├── .eslintignore ├── .eslintrc.json ├── .lintstagedrc.yml ├── .npmignore ├── CHANGELOG.md ├── README.md ├── index.html ├── package.json ├── src │ ├── hydra-validator-ui.ts │ ├── validator-shell.ts │ └── views │ │ ├── icon-items.ts │ │ └── index.ts ├── tsconfig.json └── webpack.config.js └── yarn.lock /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/master/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.5.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "linked": [], 6 | "access": "restricted", 7 | "baseBranch": "master", 8 | "updateInternalDependencies": "patch", 9 | "ignore": [] 10 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@tpluscode" 4 | ], 5 | "env": { 6 | "browser": true 7 | }, 8 | "parserOptions": { 9 | "project": "./tsconfig.json" 10 | }, 11 | "rules": { 12 | "@typescript-eslint/no-this-alias": 0 13 | }, 14 | "overrides": [ 15 | { 16 | "files": ["*.spec.ts", "mocha-setup.js"], 17 | "rules": { 18 | "@typescript-eslint/no-non-null-assertion": 0, 19 | "import/first": 0, 20 | "import/no-extraneous-dependencies": ["error", {"devDependencies": true}], 21 | "no-unused-expressions": "off" 22 | } 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Repo 14 | uses: actions/checkout@master 15 | with: 16 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 17 | fetch-depth: 0 18 | 19 | - name: Setup Node.js 20 | uses: actions/setup-node@v2.1.2 21 | with: 22 | node-version: 13 23 | 24 | - name: Install Dependencies 25 | run: yarn 26 | 27 | - name: Create Release Pull Request or Publish to npm 28 | id: changesets 29 | uses: changesets/action@master 30 | with: 31 | # This expects you to have a script called release which does a build for your packages and calls changeset publish 32 | publish: yarn release 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-node@v1 12 | with: 13 | node-version: '14' 14 | - run: yarn 15 | - run: yarn lint 16 | - run: yarn test 17 | - name: Codecov 18 | uses: codecov/codecov-action@v1.0.5 19 | with: 20 | token: ${{ secrets.CODECOV_TOKEN }} 21 | - run: yarn build 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | coverage/ 4 | *.js 5 | !*.config.js 6 | !*.conf.js 7 | !validator-ui/src/**/*.js 8 | *.log 9 | *.hydra.*.json 10 | *.d.ts 11 | *.js.map 12 | /CHANGELOG.md 13 | -------------------------------------------------------------------------------- /.lintstagedrc.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | "*.{js,ts}": 3 | - eslint --fix --quiet 4 | - git add 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 13 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 hypermedia.app 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > # hydra-validator 2 | > A tool (also website) validating a Hydra API against possible mistakes 3 | 4 | ## Usage 5 | 6 | This is a monorepo. Check the packages for more information: 7 | 8 | ### Online tool 9 | 10 | Visit [https://analyse.hypermedia.app](https://analyse.hypermedia.app) 11 | 12 | More info in [validator-ui](validator-ui) package. 13 | 14 | ### Command Line tool 15 | 16 | It is also possible to run verification of a Hydra API from the command line. This may be useful to run sanity checks 17 | locally during development or as part of a CI pipeline. 18 | 19 | More info in [validator-cli](validator-cli) package. 20 | 21 | ### Core package 22 | 23 | Code shared between other packages. 24 | 25 | More info in [validator-core](validator-core) package. 26 | 27 | ### Static API Documentation analysis 28 | 29 | Static analyser of a Hydra API. Checks the triples and hypermedia controls for potential errors. 30 | 31 | More info in [validator-analyse](validator-analyse) package. 32 | 33 | ### E2E plugin (WIP) 34 | 35 | End-to-end rules executed against a Hydra API. 36 | 37 | More info in [validator-e2e](validator-e2e) package. 38 | 39 | ## Contributing 40 | 41 | ### Creating individual checks 42 | 43 | Each verification check is a parameterless function, which returns the result and optionally an array of child checks. 44 | It can also be async. 45 | 46 | ```ts 47 | export interface CheckResult { 48 | result?: IResult; 49 | results?: IResult[]; 50 | nextChecks?: checkChain[]; 51 | sameLevel?: boolean; 52 | } 53 | 54 | export type checkChain = (this: Context) => Promise | CheckResult 55 | ``` 56 | 57 | To implement a check, you'd usually wrap the check function in a closure to pass in dependencies. This way checks are chained 58 | to run when the previous check succeeds. 59 | 60 | Here's an example which could verify that the input is a number and then pass on to a check for parity. 61 | 62 | ```ts 63 | // isnum.ts 64 | import {checkChain, Result} from '../check'; 65 | import iseven from './iseven'; 66 | 67 | export default function (maybeNum: any): checkChain { 68 | return () => { 69 | if (Number.isInteger(maybeNum)) { 70 | return { 71 | result: Result.Success('Value is number'), 72 | nextChecks: [ iseven(maybeNum) ] 73 | } 74 | } 75 | 76 | return { 77 | result: Result.Failure('Value is not a number') 78 | } 79 | } 80 | } 81 | 82 | // iseven.ts 83 | import {checkChain, Result} from '../check'; 84 | 85 | export default function (num: number): checkChain { 86 | return () => { 87 | const result = num % 2 === 0 88 | ? Result.Success(`Number ${num} is even`) 89 | : Result.Failure(`Number ${num} is odd`) 90 | 91 | return { result } 92 | } 93 | } 94 | ``` 95 | 96 | Any new check must be added to existing chains in a similar matter to how `iseven` follows `isnum`. 97 | 98 | Results can be reported with four factory methods: `Result.Succes`, `Result.Failure`, `Result.Warning` 99 | and `Result.Informational`. 100 | 101 | Note that there is no restriction for chaining. Additional checks can follow a successful check as well as failed ones. 102 | 103 | ### Creating a CLI plugin 104 | 105 | To create a plugin, create a project called `hydra-validator-uber-check`, where `uber-check` will become 106 | the CLI command. 107 | 108 | In the package main module, export a default `checkChain` function which will be called first from 109 | the CLI. 110 | 111 | Optionally, add `export const options`, which exports an array of command line parameters. Here's an example 112 | of one such option: 113 | 114 | ``` 115 | { 116 | flags: '-l, --log-level ', 117 | description: 'Minimum log level', 118 | defaultValue: 'INFO' 119 | } 120 | ``` 121 | 122 | An object with the values passed from the command line will be provided as the second argument to the 123 | main `checkChain` function. 124 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | '@babel/preset-typescript', 12 | ], 13 | plugins: [ 14 | '@babel/proposal-class-properties', 15 | '@babel/proposal-object-rest-spread', 16 | '@babel/plugin-proposal-optional-chaining', 17 | ], 18 | } 19 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - validator-e2e/lib/steps/stub.ts 3 | - validator-e2e/lib/testHelpers.ts 4 | - "**/*.spec.ts" 5 | - "**/*.js" 6 | -------------------------------------------------------------------------------- /husky.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | hooks: { 3 | 'pre-commit': 'lint-staged', 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "validator-cli", 4 | "validator-ui", 5 | "validator-e2e", 6 | "validator-analyse", 7 | "validator-core" 8 | ], 9 | "version": "independent", 10 | "command": { 11 | "publish": { 12 | "conventionalCommits": true 13 | }, 14 | "version": { 15 | "message": "chore: release new versions", 16 | "conventionalCommits": true, 17 | "noPush": true 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /mocha-setup.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | require('@babel/register')({ 3 | configFile: './babel.config.js', 4 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 5 | }) 6 | 7 | const chai = require('chai') 8 | 9 | const sinonChai = require('sinon-chai') 10 | chai.use(sinonChai) 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "scripts": { 5 | "lint": "eslint . --ext .ts --quiet --ignore-path .gitignore", 6 | "test": "run-p test:*", 7 | "test:jest": "lerna run test", 8 | "test:mocha": "c8 mocha validator-e2e", 9 | "example": "ts-node validator-cli/index.ts e2e --docs validator-e2e/example/hydracg-movies.hydra.json http://hydra-movies.herokuapp.com", 10 | "build": "lerna run prepack", 11 | "release": "changeset publish" 12 | }, 13 | "devDependencies": { 14 | "@babel/plugin-proposal-class-properties": "^7.8.3", 15 | "@babel/plugin-proposal-object-rest-spread": "^7.9.5", 16 | "@babel/plugin-proposal-optional-chaining": "^7.9.0", 17 | "@babel/preset-env": "^7.9.5", 18 | "@babel/preset-typescript": "^7.9.0", 19 | "@babel/register": "^7.12.13", 20 | "@changesets/cli": "^2.14.1", 21 | "@hydrofoil/hypertest": "^0.7.2", 22 | "@tpluscode/eslint-config": "^0.1.1", 23 | "@types/chai": "^4.2.15", 24 | "@types/mocha": "^8.2.1", 25 | "@types/node": "^13.1.8", 26 | "@typescript-eslint/eslint-plugin": "^4.15.2", 27 | "@typescript-eslint/parser": "^4.15.2", 28 | "@types/sinon": "^9.0.10", 29 | "@types/sinon-chai": "^3.2.5", 30 | "chai": "^4.3.0", 31 | "c8": "^7.6.0", 32 | "eslint": "^7.20.0", 33 | "eslint-config-standard": "^16.0.2", 34 | "eslint-plugin-import": "^2.22.1", 35 | "eslint-plugin-node": "^11.1.0", 36 | "eslint-plugin-jest": "^24.1.5", 37 | "husky": "^2.4.1", 38 | "lerna": "^3.20.2", 39 | "minimist": ">=1.2.2", 40 | "mocha": "^8.3.0", 41 | "npm-run-all": "^4.1.5", 42 | "standard": "^16.0.3", 43 | "sinon": "^9.2.4", 44 | "sinon-chai": "^3.5.0", 45 | "ts-node": "^8.8.2", 46 | "typescript": "^3" 47 | }, 48 | "peerDependencies": { 49 | "hydra-validator-core": "*", 50 | "hydra-validator-e2e": "*" 51 | }, 52 | "workspaces": [ 53 | "validator-analyse", 54 | "validator-cli", 55 | "validator-core", 56 | "validator-e2e", 57 | "validator-ui" 58 | ], 59 | "mocha": { 60 | "extension": "spec.ts", 61 | "recursive": true, 62 | "require": [ 63 | "mocha-setup.js" 64 | ] 65 | }, 66 | "c8": { 67 | "all": true, 68 | "reporter": "lcov", 69 | "exclude": [ 70 | "node_modules", 71 | "validator-analyse", 72 | "validator-core" 73 | ] 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "module": "commonjs", 5 | "moduleResolution": "Node", 6 | "lib": [ 7 | "es2018", 8 | "dom" 9 | ], 10 | "strict": true, 11 | "declaration": false, 12 | "sourceMap": false, 13 | "esModuleInterop": true, 14 | "skipLibCheck": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /validator-analyse/.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | *.d.ts 3 | -------------------------------------------------------------------------------- /validator-analyse/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest/globals": true 4 | }, 5 | "plugins": [ 6 | "jest" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /validator-analyse/.npmignore: -------------------------------------------------------------------------------- 1 | *.spec.js 2 | *.ts 3 | !*.d.ts 4 | *.spec.d.ts 5 | *.config.js 6 | coverage/ 7 | example/ 8 | tsconfig.json 9 | /.* 10 | -------------------------------------------------------------------------------- /validator-analyse/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.3.1](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-analyse@0.3.0...hydra-validator-analyse@0.3.1) (2020-09-22) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * headers and update alcaeus ([e5d44b2](https://github.com/hypermedia-app/hydra-validator/commit/e5d44b2a4d6d190f4fb5c078c9dae6be7afd6a34)) 12 | 13 | 14 | 15 | 16 | 17 | # [0.3.0](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-analyse@0.3.0-alpha.1...hydra-validator-analyse@0.3.0) (2020-04-15) 18 | 19 | **Note:** Version bump only for package hydra-validator-analyse 20 | 21 | 22 | 23 | 24 | 25 | # [0.3.0-alpha.1](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-analyse@0.3.0-alpha.0...hydra-validator-analyse@0.3.0-alpha.1) (2020-04-15) 26 | 27 | **Note:** Version bump only for package hydra-validator-analyse 28 | 29 | 30 | 31 | 32 | 33 | # [0.3.0-alpha.0](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-analyse@0.2.0...hydra-validator-analyse@0.3.0-alpha.0) (2020-04-10) 34 | 35 | **Note:** Version bump only for package hydra-validator-analyse 36 | 37 | 38 | 39 | 40 | 41 | # [0.2.0](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-analyse@0.1.8...hydra-validator-analyse@0.2.0) (2020-03-17) 42 | 43 | **Note:** Version bump only for package hydra-validator-analyse 44 | 45 | 46 | 47 | 48 | 49 | ## [0.1.8](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-analyse@0.1.7...hydra-validator-analyse@0.1.8) (2020-01-23) 50 | 51 | **Note:** Version bump only for package hydra-validator-analyse 52 | 53 | 54 | 55 | 56 | 57 | ## [0.1.7](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-analyse@0.1.6...hydra-validator-analyse@0.1.7) (2019-12-08) 58 | 59 | **Note:** Version bump only for package hydra-validator-analyse 60 | 61 | 62 | 63 | 64 | 65 | ## [0.1.6](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-analyse@0.1.5...hydra-validator-analyse@0.1.6) (2019-08-29) 66 | 67 | **Note:** Version bump only for package hydra-validator-analyse 68 | 69 | 70 | 71 | 72 | 73 | ## [0.1.5](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-analyse@0.1.4...hydra-validator-analyse@0.1.5) (2019-08-22) 74 | 75 | **Note:** Version bump only for package hydra-validator-analyse 76 | 77 | 78 | 79 | 80 | 81 | ## [0.1.4](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-analyse@0.1.3...hydra-validator-analyse@0.1.4) (2019-08-19) 82 | 83 | **Note:** Version bump only for package hydra-validator-analyse 84 | 85 | 86 | 87 | 88 | 89 | ## [0.1.3](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-analyse@0.1.2...hydra-validator-analyse@0.1.3) (2019-07-31) 90 | 91 | **Note:** Version bump only for package hydra-validator-analyse 92 | 93 | 94 | 95 | 96 | 97 | ## [0.1.2](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-analyse@0.1.1...hydra-validator-analyse@0.1.2) (2019-07-31) 98 | 99 | 100 | ### Reverts 101 | 102 | * roll back fetch hack ([a43d0d0](https://github.com/hypermedia-app/hydra-validator/commit/a43d0d0)), closes [#16](https://github.com/hypermedia-app/hydra-validator/issues/16) 103 | 104 | 105 | 106 | 107 | 108 | ## 0.1.1 (2019-07-23) 109 | 110 | **Note:** Version bump only for package hydra-validator-analyse 111 | -------------------------------------------------------------------------------- /validator-analyse/README.md: -------------------------------------------------------------------------------- 1 | > # hydra-validator-analyse 2 | > CLI plugin which performs static tests of a Hydra API 3 | 4 | ## Installation 5 | 6 | ```shell 7 | npm i hydra-validator hydra-validator-analyse 8 | ``` 9 | 10 | ## Usage 11 | 12 | Installing the plugin adds a command to the runner: 13 | 14 | ``` 15 | > hydra-validator analyse --help 16 | 17 | Usage: analyse [options] 18 | 19 | Options: 20 | -h, --help output usage information 21 | ``` 22 | 23 | Example test run 24 | 25 | ``` 26 | hydra-validator analyse https://hydra-movies.herokuapp.com/ 27 | ``` 28 | -------------------------------------------------------------------------------- /validator-analyse/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'presets': [ 3 | [ 4 | '@babel/env', 5 | { 6 | 'targets': { 7 | 'node': 'current', 8 | }, 9 | }, 10 | ], 11 | '@babel/preset-typescript', 12 | ], 13 | 'plugins': [ 14 | '@babel/proposal-class-properties', 15 | '@babel/proposal-object-rest-spread', 16 | ], 17 | } 18 | -------------------------------------------------------------------------------- /validator-analyse/checks/analyse-representation.spec.ts: -------------------------------------------------------------------------------- 1 | jest.mock('./api-documentation') 2 | 3 | import check from './analyse-representation' 4 | import apiDocsChecks from './api-documentation' 5 | import rdf from 'rdf-ext' 6 | 7 | describe('analyse-representation', () => { 8 | describe('when parsing succeeds', () => { 9 | let response: Response & any 10 | 11 | beforeEach(() => { 12 | response = { 13 | url: 'https://example.com/', 14 | dataset: jest.fn(), 15 | } 16 | 17 | response.dataset.mockResolvedValue(rdf.dataset()) 18 | }) 19 | 20 | test('queues api doc tests if flag is true', async () => { 21 | // given 22 | (apiDocsChecks as any).mockReturnValue([]) 23 | 24 | // when 25 | const { nextChecks } = await check(response, true).call({}) 26 | 27 | // then 28 | expect(apiDocsChecks).toHaveBeenCalled() 29 | expect(nextChecks).toBeDefined() 30 | }) 31 | 32 | test('should keep the current level', async () => { 33 | // given 34 | (apiDocsChecks as any).mockReturnValue([]) 35 | 36 | // when 37 | const { sameLevel } = await check(response, false).call({}) 38 | 39 | // then 40 | expect(sameLevel).toBeTruthy() 41 | }) 42 | 43 | test('should pass', async () => { 44 | // when 45 | const { result } = await check(response, false).call({}) 46 | 47 | // then 48 | expect(result!.status).toEqual('success') 49 | }) 50 | }) 51 | 52 | describe('when parsing fails', () => { 53 | test('should fail', async () => { 54 | // given 55 | const response = { 56 | url: 'https://example.com', 57 | dataset: jest.fn(), 58 | } 59 | 60 | response.dataset.mockRejectedValue(new Error()) 61 | 62 | // when 63 | const { result } = await check(response, false).call({}) 64 | 65 | // then 66 | expect(result!.status).toEqual('failure') 67 | }) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /validator-analyse/checks/analyse-representation.ts: -------------------------------------------------------------------------------- 1 | import clownface from 'clownface' 2 | import DatasetExt from 'rdf-ext/lib/Dataset' 3 | import { checkChain, Result } from 'hydra-validator-core' 4 | import apiDocsChecks from './api-documentation' 5 | 6 | export default function (response: Response & any, isApiDoc: boolean): checkChain { 7 | return function representation() { 8 | return response.dataset() 9 | .then((dataset: DatasetExt) => { 10 | const graph = clownface({ dataset }) 11 | 12 | const nextChecks: checkChain[] = [] 13 | 14 | if (isApiDoc) { 15 | nextChecks.push(...apiDocsChecks(graph)) 16 | } 17 | 18 | return { 19 | result: Result.Success(`Successfully parsed ${dataset.length} triples`), 20 | nextChecks, 21 | sameLevel: true, 22 | } 23 | }) 24 | .catch((e: Error) => { 25 | return { 26 | result: Result.Failure(`Failed to parse ${response.url}`, e), 27 | } 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /validator-analyse/checks/api-documentation/ensure-single-resource.spec.ts: -------------------------------------------------------------------------------- 1 | import ensureSingleResource from './ensure-single-resource' 2 | import { Context } from 'hydra-validator-core' 3 | 4 | describe('api-documentation', () => { 5 | test('should queue failure when no api doc nodes were found', async () => { 6 | // given 7 | const fakeClownface = { 8 | values: [], 9 | } 10 | 11 | // when 12 | const { result } = await ensureSingleResource(fakeClownface).call({}) 13 | 14 | // then 15 | expect(result!.status).toEqual('failure') 16 | }) 17 | 18 | test('should queue failure when multiple api doc nodes were found', async () => { 19 | // given 20 | const fakeClownface = { 21 | values: [1, 2, 3], 22 | } 23 | 24 | // when 25 | const { result } = await ensureSingleResource(fakeClownface).call({}) 26 | 27 | // then 28 | expect(result!.status).toEqual('failure') 29 | }) 30 | 31 | test('sets discovered api doc to context', async () => { 32 | // given 33 | const fakeClownface = { 34 | values: ['urn:api:documentation'], 35 | } 36 | const context: Context = {} 37 | 38 | // when 39 | await ensureSingleResource(fakeClownface).call(context) 40 | 41 | // then 42 | expect(context.apiDocumentation).toEqual(fakeClownface) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /validator-analyse/checks/api-documentation/ensure-single-resource.ts: -------------------------------------------------------------------------------- 1 | import { checkChain, Result } from 'hydra-validator-core' 2 | import entrypointPresent from './entrypointCheck' 3 | import hasSupportedClasses from './hasSupportedClasses' 4 | 5 | export default function (apiDocumentation: any): checkChain { 6 | const nextChecks = [ 7 | entrypointPresent(apiDocumentation), 8 | hasSupportedClasses(apiDocumentation), 9 | ] 10 | 11 | return function ensureSingleNode() { 12 | if (apiDocumentation.values.length > 1) { 13 | return { 14 | result: Result.Failure('Multiple ApiDocumentation resources found in representation'), 15 | } 16 | } 17 | 18 | if (apiDocumentation.values.length === 0) { 19 | return { 20 | result: Result.Failure('ApiDocumentation resource not found in the representation'), 21 | } 22 | } 23 | 24 | this.apiDocumentation = apiDocumentation 25 | 26 | return { 27 | nextChecks, 28 | sameLevel: true, 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /validator-analyse/checks/api-documentation/entrypointCheck.ts: -------------------------------------------------------------------------------- 1 | import { checkChain, Result } from 'hydra-validator-core' 2 | import { Hydra } from 'hydra-validator-core/namespace' 3 | import checkDereference from '../url-resolvable' 4 | 5 | export default function (apiDoc: any): checkChain { 6 | return function entrypoint() { 7 | const entrypoint = apiDoc.out(Hydra.entrypoint) 8 | 9 | if (entrypoint.values.length === 0) { 10 | return { 11 | result: Result.Warning('Entrypoint not found in api documentation'), 12 | } 13 | } 14 | 15 | if (entrypoint.term.termType === 'Literal') { 16 | return { 17 | result: Result.Failure('hydra:entrypoint property found but the value was a literal'), 18 | } 19 | } 20 | 21 | return { 22 | result: Result.Success(`Entrypoint found: ${entrypoint.term.value}`), 23 | nextChecks: [checkDereference(entrypoint.term.value, { fetchOnly: true })], 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /validator-analyse/checks/api-documentation/hasSupportedClasses.ts: -------------------------------------------------------------------------------- 1 | import { Hydra } from 'hydra-validator-core/namespace' 2 | import { checkChain, Result } from 'hydra-validator-core' 3 | 4 | export default function (apiDoc: any): checkChain { 5 | return function supportedClasses() { 6 | const classes = apiDoc.out(Hydra.supportedClass) 7 | 8 | if (classes.values.length === 0) { 9 | return { 10 | result: Result.Warning('No SupportedClasses found'), 11 | } 12 | } 13 | 14 | return { 15 | result: Result.Success(`Found ${classes.values.length} Supported Classes`), 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /validator-analyse/checks/api-documentation/index.spec.ts: -------------------------------------------------------------------------------- 1 | jest.mock('./ensure-single-resource') 2 | 3 | import clownface from 'clownface' 4 | import $rdf from 'rdf-ext' 5 | import { Hydra, rdf } from 'hydra-validator-core/namespace' 6 | import check from './index' 7 | import ensureSingleNode from './ensure-single-resource' 8 | 9 | describe('api-documentation', () => { 10 | test('should pass ApiDocumentation node to subsequent check', () => { 11 | // given 12 | const graph = clownface({ dataset: $rdf.dataset() }) 13 | graph.node('urn:api:doc').addOut(rdf.type, Hydra.ApiDocumentation) 14 | 15 | // when 16 | check(graph) 17 | 18 | // then 19 | const callArg = (ensureSingleNode as any).mock.calls[0][0] 20 | expect(callArg.value).toEqual('urn:api:doc') 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /validator-analyse/checks/api-documentation/index.ts: -------------------------------------------------------------------------------- 1 | import ensureSingleNode from './ensure-single-resource' 2 | import { Hydra, rdf } from 'hydra-validator-core/namespace' 3 | 4 | export default function (graph: any) { 5 | const apiDocs = graph.has(rdf.type, Hydra.ApiDocumentation) 6 | 7 | return [ 8 | ensureSingleNode(apiDocs), 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /validator-analyse/checks/response/api-doc-link.spec.ts: -------------------------------------------------------------------------------- 1 | jest.mock('../analyse-representation') 2 | jest.mock('../url-resolvable') 3 | 4 | import 'isomorphic-fetch' 5 | import check from './api-doc-link' 6 | import representationCheck from '../analyse-representation' 7 | import urlResolveCheck from '../url-resolvable' 8 | 9 | describe('api-doc-link', () => { 10 | test('Fails when there is no link', async () => { 11 | // given 12 | const response = new Response(null, { 13 | headers: {}, 14 | }) 15 | const context = {} 16 | 17 | // when 18 | const { result } = await check(response).call(context) 19 | 20 | // then 21 | expect(result!.status).toEqual('failure') 22 | }) 23 | 24 | test('Fails when there is no api doc link relation', async () => { 25 | // given 26 | const response = new Response(null, { 27 | headers: { 28 | Link: '; rel="up"', 29 | }, 30 | }) 31 | const context = {} 32 | 33 | // when 34 | const { result } = await check(response).call(context) 35 | 36 | // then 37 | expect(result!.status).toEqual('failure') 38 | }) 39 | 40 | test('queues api doc checks when link is same as fetched doc', async () => { 41 | // given 42 | const response: any = { 43 | url: 'urn:doc:link', 44 | headers: new Headers({ 45 | Link: '; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"', 46 | }), 47 | } 48 | 49 | // when 50 | await check(response).call({}) 51 | 52 | // then 53 | expect(representationCheck).toHaveBeenCalledWith(response, true) 54 | }) 55 | 56 | test('warns if api doc link is relative', async () => { 57 | // given 58 | const response: any = { 59 | url: 'http://example.com/', 60 | headers: new Headers({ 61 | Link: '; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"', 62 | }), 63 | } 64 | 65 | // when 66 | const { results } = await check(response).call({}) 67 | 68 | // then 69 | expect(results!.find(result => result.status === 'warning')).toBeDefined() 70 | }) 71 | 72 | test('queues dereferencing check using doc link when link is relative', async () => { 73 | // given 74 | const response: any = { 75 | url: 'http://example.com/test/resource', 76 | headers: new Headers({ 77 | Link: '; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"', 78 | }), 79 | } 80 | 81 | // when 82 | await check(response).call({}) 83 | 84 | // then 85 | expect(urlResolveCheck).toHaveBeenCalledWith('http://example.com/test/doc', { isApiDoc: true }) 86 | }) 87 | 88 | test('queues dereferencing check using doc link', async () => { 89 | // given 90 | const response: any = { 91 | url: 'http://example.com/test/resource', 92 | headers: new Headers({ 93 | Link: '; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"', 94 | }), 95 | } 96 | 97 | // when 98 | await check(response).call({}) 99 | 100 | // then 101 | expect(urlResolveCheck).toHaveBeenCalledWith('http://example.com/doc', { isApiDoc: true }) 102 | }) 103 | 104 | test('should keep same level when resource is api doc already', async () => { 105 | // given 106 | const response: any = { 107 | url: 'urn:doc:link', 108 | headers: new Headers({ 109 | Link: '; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"', 110 | }), 111 | } 112 | 113 | // when 114 | const { sameLevel } = await check(response).call({}) 115 | 116 | // then 117 | expect(sameLevel).toBeTruthy() 118 | }) 119 | 120 | test('queues up representation check if link is not api doc', async () => { 121 | // given 122 | const response: any = { 123 | url: 'http://example.com/test/resource', 124 | headers: new Headers({ 125 | Link: '; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"', 126 | }), 127 | } 128 | 129 | // when 130 | await check(response).call({}) 131 | 132 | // then 133 | expect(representationCheck).toHaveBeenCalledWith(response, false) 134 | }) 135 | }) 136 | -------------------------------------------------------------------------------- /validator-analyse/checks/response/api-doc-link.ts: -------------------------------------------------------------------------------- 1 | import { checkChain, Result } from 'hydra-validator-core' 2 | import parse from 'parse-link-header' 3 | import { Hydra } from 'hydra-validator-core/namespace' 4 | import urlResolveCheck from '../url-resolvable' 5 | import analyseRepresentation from '../analyse-representation' 6 | 7 | export default function (response: Response): checkChain { 8 | return async function apiDocLink() { 9 | if (!response.headers.has('link')) { 10 | return { 11 | result: Result.Failure('Link header missing'), 12 | } 13 | } 14 | 15 | const linkHeaders = response.headers.get('link') 16 | const links = parse(linkHeaders || '') 17 | 18 | if (!links || !links[Hydra.apiDocumentation.value]) { 19 | return { 20 | result: Result.Failure(`rel=<${Hydra.apiDocumentation.value}> link not found in the response`), 21 | } 22 | } 23 | 24 | const linkUrl = links[Hydra.apiDocumentation.value].url 25 | const apiDocUrl = new URL(linkUrl, response.url).toString() 26 | const responseUrl = new URL(response.url).toString() 27 | 28 | if (responseUrl !== apiDocUrl) { 29 | const results = [Result.Success('Api Documentation link found')] 30 | if (apiDocUrl !== linkUrl) { 31 | results.push(Result.Warning('Relative Api Documentation link may not be supported by clients')) 32 | } 33 | 34 | return { 35 | results, 36 | nextChecks: [ 37 | urlResolveCheck(apiDocUrl, { isApiDoc: true }), 38 | analyseRepresentation(response, false), 39 | ], 40 | sameLevel: true, 41 | } 42 | } 43 | 44 | return { 45 | result: Result.Informational('Resource is Api Documentation'), 46 | nextChecks: [analyseRepresentation(response, true)], 47 | sameLevel: true, 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /validator-analyse/checks/response/status-code.spec.ts: -------------------------------------------------------------------------------- 1 | import 'isomorphic-fetch' 2 | import check from './status-code' 3 | 4 | describe('status-code', () => { 5 | test('should pass when status is successful', async () => { 6 | // given 7 | const response = new Response(null, { 8 | status: 200, 9 | }) 10 | const context = {} 11 | 12 | // when 13 | const { result } = await check(response).call(context) 14 | 15 | // then 16 | expect(result!.status).toEqual('success') 17 | }) 18 | 19 | test('should fail when status is not successful', async () => { 20 | // given 21 | const response = new Response(null, { 22 | status: 404, 23 | }) 24 | const context = {} 25 | 26 | // when 27 | const { result } = await check(response).call(context) 28 | 29 | // then 30 | expect(result!.status).toEqual('failure') 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /validator-analyse/checks/response/status-code.ts: -------------------------------------------------------------------------------- 1 | import { checkChain, Result } from 'hydra-validator-core' 2 | 3 | export default function (response: Response): checkChain { 4 | return function statusCode() { 5 | if (response.ok) { 6 | return { 7 | result: Result.Success(`Response status ${response.status}`), 8 | } 9 | } else { 10 | return { 11 | result: Result.Failure('Request failed', `Status code was ${response.status}`), 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /validator-analyse/checks/url-resolvable.spec.ts: -------------------------------------------------------------------------------- 1 | jest.mock('@rdfjs/fetch-lite') 2 | jest.mock('./response/api-doc-link') 3 | jest.mock('./analyse-representation') 4 | 5 | import 'isomorphic-fetch' 6 | import realFetch from '@rdfjs/fetch-lite' 7 | import check from './url-resolvable' 8 | import apiLinkCheck from './response/api-doc-link' 9 | import representationCheck from './analyse-representation' 10 | 11 | const fetch = realFetch as jest.Mock 12 | 13 | function testContext(visitedUrls: string[] = []) { 14 | return { 15 | visitedUrls, 16 | } 17 | } 18 | 19 | describe('url-resolvable', () => { 20 | test('does not append to visitedUrls when URL is already present', async () => { 21 | // given 22 | const response = { 23 | dataset: jest.fn(), 24 | } 25 | fetch.mockResolvedValue(response) 26 | const context = testContext(['http://exmaple.com/']) 27 | 28 | // when 29 | await check('http://exmaple.com/').call(context) 30 | 31 | // then 32 | expect(context.visitedUrls.length).toEqual(1) 33 | }) 34 | 35 | test('returns informational when URL has already been visited', async () => { 36 | // given 37 | const inputContext = testContext(['urn:already:seen']) 38 | 39 | // when 40 | const { result, nextChecks } = await check('urn:already:seen').call(inputContext) 41 | 42 | // then 43 | expect(result!.status).toEqual('informational') 44 | expect(nextChecks).toBeUndefined() 45 | }) 46 | 47 | describe('when fetch fails', () => { 48 | test('returns failure', async () => { 49 | // given 50 | fetch.mockRejectedValue(new Error()) 51 | 52 | // when 53 | const { result } = await check('https://example.com').call(testContext()) 54 | 55 | // then 56 | expect(result!.status).toEqual('failure') 57 | }) 58 | 59 | test('appends url to visitedUrls', async () => { 60 | // given 61 | fetch.mockRejectedValue(new Error()) 62 | const context = testContext() 63 | 64 | // when 65 | await check('https://example.com').call(context) 66 | 67 | // then 68 | expect(context.visitedUrls).toContain('https://example.com/') 69 | }) 70 | }) 71 | 72 | describe('when request succeeds', () => { 73 | test('returns success', async () => { 74 | // given 75 | fetch.mockResolvedValue({ 76 | }) 77 | 78 | // when 79 | const { result } = await check('https://example.com').call(testContext()) 80 | 81 | // then 82 | expect(result!.status).toEqual('success') 83 | }) 84 | 85 | test('does not queue contents check if fetchOnly is true', async () => { 86 | // given 87 | fetch.mockResolvedValue(new Response()) 88 | 89 | // when 90 | const { nextChecks } = await check('https://example.com', { fetchOnly: true }).call(testContext()) 91 | 92 | // then 93 | expect(nextChecks!.length).toEqual(1) 94 | expect(nextChecks![0].name).toEqual('statusCode') 95 | }) 96 | 97 | test('does not queue up Link check if apiDoc param is true', async () => { 98 | // given 99 | fetch.mockResolvedValue({ 100 | url: 'https://example.com', 101 | }) 102 | 103 | // when 104 | await check('https://example.com', { isApiDoc: true }).call(testContext()) 105 | 106 | // then 107 | expect(apiLinkCheck).not.toHaveBeenCalled() 108 | }) 109 | 110 | test('queues up Link check if apiDoc param is false', async () => { 111 | // given 112 | const response = { 113 | } 114 | fetch.mockResolvedValue(response) 115 | 116 | // when 117 | await check('https://example.com', { isApiDoc: false }).call(testContext()) 118 | 119 | // then 120 | expect(apiLinkCheck).toHaveBeenCalledWith(response) 121 | }) 122 | 123 | test('queues up representation check when apiDoc param is true', async () => { 124 | // given 125 | const response = { 126 | dataset: jest.fn(), 127 | } 128 | fetch.mockResolvedValue(response) 129 | 130 | // when 131 | await check('https://example.com', { isApiDoc: true }).call(testContext()) 132 | 133 | // then 134 | expect(representationCheck).toHaveBeenCalledWith(response, true) 135 | }) 136 | 137 | test('appends url to visitedUrls', async () => { 138 | // given 139 | const response = { 140 | dataset: jest.fn(), 141 | } 142 | fetch.mockResolvedValue(response) 143 | const context = testContext() 144 | 145 | // when 146 | await check('https://example.com').call(context) 147 | 148 | // then 149 | expect(context!.visitedUrls).toContain('https://example.com/') 150 | }) 151 | 152 | beforeEach(() => { 153 | jest.clearAllMocks() 154 | .resetModules() 155 | }) 156 | }) 157 | }) 158 | -------------------------------------------------------------------------------- /validator-analyse/checks/url-resolvable.ts: -------------------------------------------------------------------------------- 1 | import fetch from '@rdfjs/fetch-lite' 2 | import formats from '@rdfjs/formats-common' 3 | import rdf from 'rdf-ext' 4 | import { checkChain, Context, Result } from 'hydra-validator-core' 5 | import statusCheck from './response/status-code' 6 | import apiDocLink from './response/api-doc-link' 7 | import analyseRepresentation from './analyse-representation' 8 | 9 | export default function (url: string, { fetchOnly = false, isApiDoc = false } = {}): checkChain { 10 | return function tryFetch(this: Context) { 11 | const urlNormalised = new URL(url).toString() 12 | 13 | if (this.visitedUrls.includes(urlNormalised)) { 14 | return { 15 | result: Result.Informational(`Skipping already visited resource <${url}>`), 16 | } 17 | } 18 | this.visitedUrls.push(urlNormalised) 19 | 20 | return fetch(url, { formats, factory: rdf }) 21 | .then(async (response: Response) => { 22 | const nextChecks = [ 23 | statusCheck(response), 24 | ] 25 | 26 | if (!fetchOnly) { 27 | if (isApiDoc) { 28 | nextChecks.push(analyseRepresentation(response, true)) 29 | } else { 30 | nextChecks.push(apiDocLink(response)) 31 | } 32 | } 33 | 34 | return { 35 | result: Result.Success(`Successfully fetched ${url}`), 36 | nextChecks, 37 | } 38 | }) 39 | .catch((e: Error) => ({ 40 | result: Result.Failure('Failed to fetch resource', e), 41 | })) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /validator-analyse/index.ts: -------------------------------------------------------------------------------- 1 | import urlResolvable from './checks/url-resolvable' 2 | 3 | export const check = urlResolvable 4 | -------------------------------------------------------------------------------- /validator-analyse/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: [''], 3 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$', 4 | collectCoverage: true, 5 | collectCoverageFrom: ['**/*.ts'], 6 | transformIgnorePatterns: [ 7 | 'node_modules/(?!(alcaeus|hydra-validator-core)/)', 8 | ], 9 | } 10 | -------------------------------------------------------------------------------- /validator-analyse/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hydra-validator-analyse", 3 | "version": "0.3.1", 4 | "description": "Static analysis checks for hydra-validator", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "prepack": "tsc", 9 | "precommit": "lint-staged" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/hypermedia-app/hydra-validator.git" 14 | }, 15 | "keywords": [ 16 | "hydra", 17 | "hypermedia", 18 | "validator" 19 | ], 20 | "author": "Tomasz Pluskiewicz ", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/hypermedia-app/hydra-validator/issues" 24 | }, 25 | "homepage": "https://github.com/hypermedia-app/hydra-validator#readme", 26 | "devDependencies": { 27 | "@babel/plugin-proposal-class-properties": "^7.5.5", 28 | "@babel/plugin-proposal-object-rest-spread": "^7.5.5", 29 | "@babel/preset-env": "^7.5.5", 30 | "@babel/preset-typescript": "^7.3.3", 31 | "@types/clownface": "^1.2.0", 32 | "@types/jest": "^24.0.12", 33 | "@types/node": "^11.13.9", 34 | "@types/node-fetch": "^2.3.3", 35 | "@types/parse-link-header": "^1.0.0", 36 | "@types/rdf-ext": "^1.3.8", 37 | "@types/rdf-js": "^4", 38 | "@types/rdfjs__fetch-lite": "^2.0.1", 39 | "@types/rdfjs__formats-common": "^2.0.0", 40 | "isomorphic-fetch": "^2.2.1", 41 | "jest": "^24.7.1", 42 | "lint-staged": "^8.2.1", 43 | "minimist": ">=1.2.2" 44 | }, 45 | "dependencies": { 46 | "@rdfjs/fetch-lite": "^2.0.1", 47 | "@rdfjs/formats-common": "^2.0.1", 48 | "clownface": "^1.2.0", 49 | "hydra-validator-core": "^0.5.0", 50 | "parse-link-header": "^1.0.1", 51 | "rdf-ext": "^1.3.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /validator-analyse/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": [ 5 | "es2015", 6 | "dom" 7 | ], 8 | "strict": true, 9 | "declaration": true, 10 | "sourceMap": false, 11 | "esModuleInterop": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /validator-cli/.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | *.d.ts 3 | -------------------------------------------------------------------------------- /validator-cli/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest/globals": true 4 | }, 5 | "plugins": [ 6 | "jest" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /validator-cli/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 2.0.1 4 | 5 | ### Patch Changes 6 | 7 | - 807feca: Update builds to output proper import paths 8 | - Updated dependencies [807feca] 9 | - hydra-validator-core@0.5.1 10 | 11 | ## 2.0.0 12 | 13 | ### Major Changes 14 | 15 | - 43da1a5: Publish as modules 16 | 17 | All notable changes to this project will be documented in this file. 18 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 19 | 20 | ## [1.3.7](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator@1.3.6...hydra-validator@1.3.7) (2020-09-22) 21 | 22 | **Note:** Version bump only for package hydra-validator 23 | 24 | ## [1.3.6](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator@1.3.5...hydra-validator@1.3.6) (2020-09-22) 25 | 26 | **Note:** Version bump only for package hydra-validator 27 | 28 | ## [1.3.5](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator@1.3.4...hydra-validator@1.3.5) (2020-05-04) 29 | 30 | **Note:** Version bump only for package hydra-validator 31 | 32 | ## [1.3.4](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator@1.3.3...hydra-validator@1.3.4) (2020-05-02) 33 | 34 | ### Bug Fixes 35 | 36 | - **e2e:** latest alcaeus fixes managing resource state ([cb07289](https://github.com/hypermedia-app/hydra-validator/commit/cb07289d78df1080b2fad7457f4a856be3a666f6)) 37 | 38 | ## [1.3.3](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator@1.3.2...hydra-validator@1.3.3) (2020-04-25) 39 | 40 | **Note:** Version bump only for package hydra-validator 41 | 42 | ## [1.3.2](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator@1.3.1...hydra-validator@1.3.2) (2020-04-24) 43 | 44 | **Note:** Version bump only for package hydra-validator 45 | 46 | ## [1.3.1](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator@1.3.0...hydra-validator@1.3.1) (2020-04-20) 47 | 48 | **Note:** Version bump only for package hydra-validator 49 | 50 | # [1.3.0](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator@1.3.0-alpha.1...hydra-validator@1.3.0) (2020-04-15) 51 | 52 | **Note:** Version bump only for package hydra-validator 53 | 54 | # [1.3.0-alpha.1](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator@1.3.0-alpha.0...hydra-validator@1.3.0-alpha.1) (2020-04-15) 55 | 56 | **Note:** Version bump only for package hydra-validator 57 | 58 | # [1.3.0-alpha.0](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator@1.2.0...hydra-validator@1.3.0-alpha.0) (2020-04-10) 59 | 60 | **Note:** Version bump only for package hydra-validator 61 | 62 | # [1.2.0](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator@1.1.3...hydra-validator@1.2.0) (2020-03-17) 63 | 64 | **Note:** Version bump only for package hydra-validator 65 | 66 | ## [1.1.3](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator@1.1.2...hydra-validator@1.1.3) (2020-01-23) 67 | 68 | **Note:** Version bump only for package hydra-validator 69 | 70 | ## [1.1.2](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator@1.1.1...hydra-validator@1.1.2) (2019-12-08) 71 | 72 | **Note:** Version bump only for package hydra-validator 73 | 74 | ## [1.1.1](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator@1.1.0...hydra-validator@1.1.1) (2019-12-07) 75 | 76 | **Note:** Version bump only for package hydra-validator 77 | 78 | # [1.1.0](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator@1.0.12...hydra-validator@1.1.0) (2019-12-06) 79 | 80 | ### Features 81 | 82 | - apply default scenario headers when performing requests ([db260d8](https://github.com/hypermedia-app/hydra-validator/commit/db260d8)) 83 | 84 | ## [1.0.12](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator@1.0.11...hydra-validator@1.0.12) (2019-12-03) 85 | 86 | **Note:** Version bump only for package hydra-validator 87 | 88 | ## [1.0.11](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator@1.0.10...hydra-validator@1.0.11) (2019-11-08) 89 | 90 | **Note:** Version bump only for package hydra-validator 91 | 92 | ## [1.0.10](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator@1.0.9...hydra-validator@1.0.10) (2019-10-31) 93 | 94 | **Note:** Version bump only for package hydra-validator 95 | 96 | ## [1.0.9](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator@1.0.8...hydra-validator@1.0.9) (2019-10-11) 97 | 98 | **Note:** Version bump only for package hydra-validator 99 | 100 | ## [1.0.8](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator@1.0.7...hydra-validator@1.0.8) (2019-08-29) 101 | 102 | **Note:** Version bump only for package hydra-validator 103 | 104 | ## [1.0.7](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator@1.0.6...hydra-validator@1.0.7) (2019-08-29) 105 | 106 | **Note:** Version bump only for package hydra-validator 107 | 108 | ## [1.0.6](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator@1.0.5...hydra-validator@1.0.6) (2019-08-22) 109 | 110 | **Note:** Version bump only for package hydra-validator 111 | 112 | ## [1.0.5](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator@1.0.4...hydra-validator@1.0.5) (2019-08-19) 113 | 114 | **Note:** Version bump only for package hydra-validator 115 | 116 | ## [1.0.4](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator@1.0.3...hydra-validator@1.0.4) (2019-08-05) 117 | 118 | **Note:** Version bump only for package hydra-validator 119 | 120 | ## [1.0.3](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator@1.0.2...hydra-validator@1.0.3) (2019-07-31) 121 | 122 | ### Bug Fixes 123 | 124 | - cli must return appropriate status code ([dfc285d](https://github.com/hypermedia-app/hydra-validator/commit/dfc285d)) 125 | 126 | ## [1.0.2](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator@1.0.1...hydra-validator@1.0.2) (2019-07-31) 127 | 128 | ### Reverts 129 | 130 | - roll back fetch hack ([a43d0d0](https://github.com/hypermedia-app/hydra-validator/commit/a43d0d0)), closes [#16](https://github.com/hypermedia-app/hydra-validator/issues/16) 131 | 132 | ## [1.0.1](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator@1.0.0...hydra-validator@1.0.1) (2019-07-23) 133 | 134 | ### Bug Fixes 135 | 136 | - the cli must look for plugin in calling package and not itself ([435e298](https://github.com/hypermedia-app/hydra-validator/commit/435e298)) 137 | 138 | # 1.0.0 (2019-07-23) 139 | 140 | ### Bug Fixes 141 | 142 | - **hydra-validator:** load docs file based on current working dir ([ee400ba](https://github.com/hypermedia-app/hydra-validator/commit/ee400ba)) 143 | - **validator-cli:** pass description and default value to commanderjs ([4939e43](https://github.com/hypermedia-app/hydra-validator/commit/4939e43)) 144 | 145 | ### chore 146 | 147 | - **validator-cli:** remove direct dependency on analyse package ([53d83dc](https://github.com/hypermedia-app/hydra-validator/commit/53d83dc)) 148 | 149 | ### Features 150 | 151 | - **hydra-validator:** load check packages dynamically ([87b0d78](https://github.com/hypermedia-app/hydra-validator/commit/87b0d78)) 152 | - **hydra-validator:** set up cli commands from plugin packages ([0857ed7](https://github.com/hypermedia-app/hydra-validator/commit/0857ed7)) 153 | - **validator-e2e:** first PoC of E2E runner ([ca9f95c](https://github.com/hypermedia-app/hydra-validator/commit/ca9f95c)) 154 | 155 | ### BREAKING CHANGES 156 | 157 | - **validator-cli:** hydra-validator-analyse must be installed separately 158 | 159 | # 0.1.0 (2019-06-18) 160 | 161 | ## 0.0.3 (2019-05-06) 162 | 163 | ## 0.0.2 (2019-05-06) 164 | 165 | ## 0.0.1 (2019-05-06) 166 | 167 | # [0.1.0](https://github.com/hypermedia-app/hydra-validator/compare/v0.0.3...v0.1.0) (2019-06-18) 168 | 169 | **Note:** Version bump only for package hydra-validator 170 | -------------------------------------------------------------------------------- /validator-cli/README.md: -------------------------------------------------------------------------------- 1 | > # hydra-validator 2 | > CLI interface for running tests against Hydra APIs 3 | 4 | ## Installation 5 | 6 | ```shell 7 | npm i hydra-validator 8 | ``` 9 | 10 | ## Usage 11 | 12 | The CLI needs plugins to operate: 13 | 14 | * [Static analysis rules plugin](../validator-analyse) 15 | * [End-to-end test plugin](../validator-e2e) 16 | -------------------------------------------------------------------------------- /validator-cli/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import program from 'commander' 4 | import { runChecks } from 'hydra-validator-core/run-checks.js' 5 | import deps from 'matchdep' 6 | import debug, { Debugger } from 'debug' 7 | import { ResultKind } from 'hydra-validator-core' 8 | 9 | export type Loggers = Record 10 | const loggers: Loggers = { 11 | success: debug('SUCCESS'), 12 | informational: debug('INFO'), 13 | warning: debug('WARNING'), 14 | failure: debug('FAILURE'), 15 | error: debug('ERROR'), 16 | } 17 | 18 | debug.enable('*') 19 | 20 | debug.formatters.p = (level) => { 21 | return level === 0 ? '' : ` ${'-'.repeat(level * 2)}` 22 | } 23 | 24 | debug.formatters.l = (logger: ResultKind) => { 25 | if (logger === 'informational') { 26 | return ' ' 27 | } 28 | 29 | if (logger === 'error') { 30 | return ' ' 31 | } 32 | 33 | return '' 34 | } 35 | 36 | const plugins = deps.filterAll(['hydra-validator-*', 'hydra-validator-core'], `${process.cwd()}/package.json`) 37 | 38 | async function main() { 39 | for (const plugin of plugins) { 40 | const match = plugin.match(/^hydra-validator-([\d\w]+)$/) 41 | if (!match) { 42 | continue 43 | } 44 | 45 | const commandName = match[1] 46 | const { check, options } = await import(plugin) 47 | 48 | const command = program 49 | .command(`${commandName} `) 50 | 51 | if (options && Array.isArray(options)) { 52 | for (const option of options) { 53 | command.option(option.flags, option.description, option.defaultValue) 54 | } 55 | } 56 | 57 | command.action(function (this: any, url: string) { 58 | Promise.resolve() 59 | .then(async () => { 60 | let unsucessfulCount = 0 61 | const firstCheck = check(url, { ...this, cwd: process.cwd(), log: loggers }) 62 | 63 | const checkGenerator = runChecks(firstCheck) 64 | 65 | for await (const check of checkGenerator) { 66 | loggers[check.result.status]('%l %p %s %s', check.result.status, check.level, check.result.description, (check.result as any).details || '') 67 | 68 | if (check.result.status === 'failure' || check.result.status === 'error') { 69 | unsucessfulCount++ 70 | } 71 | } 72 | 73 | process.exit(unsucessfulCount) 74 | }) 75 | }) 76 | } 77 | } 78 | 79 | // eslint-disable-next-line no-console 80 | main().then(() => program.parse(process.argv)).catch(console.error) 81 | -------------------------------------------------------------------------------- /validator-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hydra-validator", 3 | "version": "2.0.1", 4 | "description": "CLI tool to run checks against a Hydra API", 5 | "main": "index.js", 6 | "type": "module", 7 | "bin": { 8 | "hydra-validator": "index.js" 9 | }, 10 | "files": [ 11 | "**/*.js", 12 | "**/*.d.ts" 13 | ], 14 | "scripts": { 15 | "prepack": "tsc", 16 | "precommit": "lint-staged", 17 | "start": "ts-node index.ts e2e https://wikibus-sources.lndo.site/ --docs /Users/tomaszpluskiewicz/projects/github/wikibus/sources.wikibus.org/e2e-tests/Brochure/Create.hydra.hypertest.json" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/hypermedia-app/hydra-validator.git" 22 | }, 23 | "keywords": [ 24 | "hydra", 25 | "hypermedia", 26 | "validator" 27 | ], 28 | "author": "Tomasz Pluskiewicz ", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/hypermedia-app/hydra-validator/issues" 32 | }, 33 | "homepage": "https://github.com/hypermedia-app/hydra-validator#readme", 34 | "devDependencies": { 35 | "@babel/plugin-proposal-class-properties": "^7.8.3", 36 | "@babel/plugin-proposal-object-rest-spread": "^7.9.5", 37 | "@babel/plugin-proposal-optional-chaining": "^7.9.0", 38 | "@babel/preset-env": "^7.9.5", 39 | "@babel/preset-typescript": "^7.9.0", 40 | "@types/debug": "^4.1.4", 41 | "@types/jest": "^24.0.12", 42 | "@types/matchdep": "^2", 43 | "@types/node": "^11.13.9", 44 | "@types/node-fetch": "^2.3.3", 45 | "hydra-validator-analyse": "^0.3.1", 46 | "hydra-validator-e2e": "^0.12.1", 47 | "isomorphic-fetch": "^2.2.1", 48 | "jest": "^24.7.1", 49 | "lint-staged": "^8.2.1", 50 | "minimist": ">=1.2.2", 51 | "rimraf": "^2.6.3" 52 | }, 53 | "dependencies": { 54 | "commander": "^2.20.0", 55 | "debug": "^4.1.1", 56 | "hydra-validator-core": "^0.5.1", 57 | "matchdep": "^2.0.0" 58 | }, 59 | "publishConfig": { 60 | "access": "public" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /validator-cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": [ 5 | "es2015", 6 | "dom" 7 | ], 8 | "module": "ES2020", 9 | "strict": true, 10 | "declaration": true, 11 | "sourceMap": false, 12 | "esModuleInterop": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /validator-core/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ], 11 | "@babel/preset-typescript" 12 | ], 13 | "plugins": [ 14 | "@babel/proposal-class-properties", 15 | "@babel/proposal-object-rest-spread" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /validator-core/.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | *.d.ts 3 | -------------------------------------------------------------------------------- /validator-core/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest/globals": true 4 | }, 5 | "plugins": [ 6 | "jest" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /validator-core/.npmignore: -------------------------------------------------------------------------------- 1 | *.spec.js 2 | *.ts 3 | !*.d.ts 4 | *.spec.d.ts 5 | *.config.js 6 | coverage/ 7 | example/ 8 | tsconfig.json 9 | /.* 10 | -------------------------------------------------------------------------------- /validator-core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 0.5.1 4 | 5 | ### Patch Changes 6 | 7 | - 807feca: Update builds to output proper import paths 8 | 9 | All notable changes to this project will be documented in this file. 10 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 11 | 12 | # [0.5.0](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-core@0.5.0-alpha.1...hydra-validator-core@0.5.0) (2020-04-15) 13 | 14 | **Note:** Version bump only for package hydra-validator-core 15 | 16 | # [0.5.0-alpha.1](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-core@0.5.0-alpha.0...hydra-validator-core@0.5.0-alpha.1) (2020-04-15) 17 | 18 | **Note:** Version bump only for package hydra-validator-core 19 | 20 | # [0.5.0-alpha.0](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-core@0.4.0...hydra-validator-core@0.5.0-alpha.0) (2020-04-10) 21 | 22 | **Note:** Version bump only for package hydra-validator-core 23 | 24 | # [0.4.0](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-core@0.3.6...hydra-validator-core@0.4.0) (2020-03-17) 25 | 26 | **Note:** Version bump only for package hydra-validator-core 27 | 28 | ## [0.3.6](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-core@0.3.5...hydra-validator-core@0.3.6) (2020-01-23) 29 | 30 | **Note:** Version bump only for package hydra-validator-core 31 | 32 | ## [0.3.5](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-core@0.3.4...hydra-validator-core@0.3.5) (2019-12-08) 33 | 34 | **Note:** Version bump only for package hydra-validator-core 35 | 36 | ## [0.3.4](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-core@0.3.3...hydra-validator-core@0.3.4) (2019-08-29) 37 | 38 | **Note:** Version bump only for package hydra-validator-core 39 | 40 | ## [0.3.3](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-core@0.3.2...hydra-validator-core@0.3.3) (2019-08-22) 41 | 42 | **Note:** Version bump only for package hydra-validator-core 43 | 44 | ## [0.3.2](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-core@0.3.1...hydra-validator-core@0.3.2) (2019-08-19) 45 | 46 | **Note:** Version bump only for package hydra-validator-core 47 | 48 | ## [0.3.1](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-core@0.3.0...hydra-validator-core@0.3.1) (2019-07-31) 49 | 50 | **Note:** Version bump only for package hydra-validator-core 51 | 52 | # [0.3.0](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-core@0.2.0...hydra-validator-core@0.3.0) (2019-07-31) 53 | 54 | ### Features 55 | 56 | - **validator-core:** catch errors and don't block subsequent checks ([71a2ded](https://github.com/hypermedia-app/hydra-validator/commit/71a2ded)), closes [#26](https://github.com/hypermedia-app/hydra-validator/issues/26) 57 | - **validator-core:** include errors in run summary ([ed503c4](https://github.com/hypermedia-app/hydra-validator/commit/ed503c4)) 58 | - invocation step to handle headers and bodies ([3c3b878](https://github.com/hypermedia-app/hydra-validator/commit/3c3b878)) 59 | 60 | ### Reverts 61 | 62 | - roll back fetch hack ([a43d0d0](https://github.com/hypermedia-app/hydra-validator/commit/a43d0d0)), closes [#16](https://github.com/hypermedia-app/hydra-validator/issues/16) 63 | 64 | # 0.2.0 (2019-07-23) 65 | 66 | ### Features 67 | 68 | - **validator-core:** use custom context type on language level ([bb85a1e](https://github.com/hypermedia-app/hydra-validator/commit/bb85a1e)) 69 | - **validator-e2e:** update expectation and property steps ([10e6f82](https://github.com/hypermedia-app/hydra-validator/commit/10e6f82)) 70 | -------------------------------------------------------------------------------- /validator-core/README.md: -------------------------------------------------------------------------------- 1 | > # hydra-validator-core 2 | > Shared code for hydra validators 3 | 4 | ## Usage 5 | 6 | This package doesn't have to be installed explicitly, unless when creating 7 | a CLI plugin or runner. 8 | -------------------------------------------------------------------------------- /validator-core/index.ts: -------------------------------------------------------------------------------- 1 | export type ResultKind = 'success' | 'failure' | 'informational' | 'warning' | 'error' 2 | 3 | export interface IResult { 4 | description: string 5 | status: ResultKind 6 | } 7 | 8 | class ErrorResult implements IResult { 9 | public description: string; 10 | public status: ResultKind; 11 | public details: string | Error 12 | 13 | public constructor(description: string, details: string | Error) { 14 | this.status = 'error' 15 | this.description = description 16 | this.details = details 17 | } 18 | } 19 | 20 | type FailureKind = 'failed' | 'inconclusive' 21 | 22 | class Failure implements IResult { 23 | public details: string | Error 24 | public description: string; 25 | public status: ResultKind; 26 | 27 | public constructor(kind: FailureKind, description: string, details: string | Error = '') { 28 | this.description = description 29 | this.status = 'failure' 30 | this.details = details 31 | } 32 | } 33 | 34 | export class Result implements IResult { 35 | public description: string 36 | public status: ResultKind 37 | public details?: string 38 | 39 | protected constructor(descrition: string, status: ResultKind, details?: string) { 40 | this.description = descrition 41 | this.status = status 42 | this.details = details 43 | } 44 | 45 | public static Success(description = '', details?: string) { 46 | return new Result(description, 'success', details) 47 | } 48 | 49 | public static Failure(reason: string, details?: string | Error): IResult { 50 | return new Failure('failed', reason, details) 51 | } 52 | 53 | public static Warning(description: string, details?: string) { 54 | return new Result(description, 'warning', details) 55 | } 56 | 57 | public static Informational(description: string, details?: string) { 58 | return new Result(description, 'informational', details) 59 | } 60 | 61 | public static Error(description: string, details: string | Error = ''): IResult { 62 | return new ErrorResult(description, details) 63 | } 64 | } 65 | 66 | export interface Context { 67 | [s: string]: any 68 | } 69 | 70 | /** 71 | * Return type of check functions. Either results, or result will be reported 72 | */ 73 | export interface CheckResult { 74 | /** 75 | * Results to be reported 76 | */ 77 | result?: IResult 78 | /** 79 | * Results to be reported 80 | */ 81 | results?: IResult[] 82 | /** 83 | * Checks to add to queue 84 | */ 85 | // eslint-disable-next-line no-use-before-define 86 | nextChecks?: checkChain[] 87 | /** 88 | * If true, does not nest nextChecks 89 | */ 90 | sameLevel?: boolean 91 | } 92 | 93 | /** 94 | * Function delegate which runs actual check. It can be asynchronous 95 | */ 96 | export type checkChain = (this: T) => Promise> | CheckResult 97 | -------------------------------------------------------------------------------- /validator-core/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: [''], 3 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$', 4 | moduleFileExtensions: ['ts', 'tsx', 'js'], 5 | collectCoverage: true, 6 | collectCoverageFrom: ['**/*.ts'], 7 | } 8 | -------------------------------------------------------------------------------- /validator-core/namespace.ts: -------------------------------------------------------------------------------- 1 | import namespace from '@rdfjs/namespace' 2 | 3 | export const Hydra = namespace('http://www.w3.org/ns/hydra/core#') 4 | export const rdf = namespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#') 5 | -------------------------------------------------------------------------------- /validator-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hydra-validator-core", 3 | "version": "0.5.1", 4 | "description": "Shared package for hydra-validator", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "prepack": "tsc", 9 | "precommit": "lint-staged" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/hypermedia-app/hydra-validator.git" 14 | }, 15 | "keywords": [ 16 | "hydra", 17 | "hypermedia", 18 | "validator" 19 | ], 20 | "author": "Tomasz Pluskiewicz ", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/hypermedia-app/hydra-validator/issues" 24 | }, 25 | "homepage": "https://github.com/hypermedia-app/hydra-validator#readme", 26 | "devDependencies": { 27 | "@babel/plugin-proposal-class-properties": "^7.5.5", 28 | "@babel/plugin-proposal-object-rest-spread": "^7.5.5", 29 | "@babel/preset-env": "^7.5.5", 30 | "@babel/preset-typescript": "^7.3.3", 31 | "@types/jest": "^24.0.12", 32 | "@types/node": "^11.13.9", 33 | "@types/node-fetch": "^2.3.3", 34 | "@types/rdfjs__namespace": "^1.1.1", 35 | "isomorphic-fetch": "^2.2.1", 36 | "jest": "^24.7.1", 37 | "lint-staged": "^8.2.1", 38 | "minimist": ">=1.2.2" 39 | }, 40 | "dependencies": { 41 | "@rdfjs/namespace": "^1.1.0" 42 | }, 43 | "publishConfig": { 44 | "access": "public" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /validator-core/run-checks.spec.ts: -------------------------------------------------------------------------------- 1 | import { runChecks } from './run-checks' 2 | import { checkChain, Result } from '.' 3 | 4 | async function getResults(iter: AsyncIterableIterator) { 5 | const results: T[] = [] 6 | 7 | for await (const result of iter) { 8 | results.push(result) 9 | } 10 | 11 | return results 12 | } 13 | 14 | describe('run-checks', () => { 15 | test('yields an initial informational message', async () => { 16 | // given 17 | const check: checkChain = () => ({}) 18 | 19 | // when 20 | const [first] = await getResults(runChecks(check)) 21 | 22 | // expect 23 | expect(first.level).toBe(0) 24 | expect(first.result.status).toBe('informational') 25 | }) 26 | 27 | test('passes same context between subsequent steps', async () => { 28 | // given 29 | const secondCheck = jest.fn().mockResolvedValue({}) 30 | const firstCheck = jest.fn().mockResolvedValue({ 31 | nextChecks: [secondCheck], 32 | }) 33 | 34 | // when 35 | await getResults(runChecks(firstCheck)) 36 | 37 | // then 38 | expect(firstCheck.mock.instances[0]).toBe(secondCheck.mock.instances[0]) 39 | }) 40 | 41 | test('recursively calls all queued nextChecks', async () => { 42 | // given 43 | const spy = jest.fn().mockResolvedValue({}) 44 | const firstCheck = jest.fn().mockResolvedValue({ 45 | nextChecks: [ 46 | ...[1, 2, 3, 4, 5].map(() => spy), 47 | jest.fn().mockResolvedValue({ 48 | nextChecks: [spy], 49 | }), 50 | ], 51 | }) 52 | 53 | // when 54 | await getResults(runChecks(firstCheck)) 55 | 56 | // then 57 | expect(spy.mock.calls.length).toBe(6) 58 | }) 59 | 60 | test('yields single result before multiple results', async () => { 61 | // given 62 | const firstCheck = jest.fn().mockResolvedValue({ 63 | result: Result.Success('test'), 64 | results: [ 65 | Result.Success('test'), 66 | Result.Success('test'), 67 | ], 68 | }) 69 | 70 | // when 71 | const results = await getResults(runChecks(firstCheck)) 72 | 73 | // then 74 | expect(results.length).toBe(5) 75 | }) 76 | 77 | test('yields multiple results when result is not provided', async () => { 78 | // given 79 | const firstCheck = jest.fn().mockResolvedValue({ 80 | results: [ 81 | Result.Success('test'), 82 | Result.Success('test'), 83 | ], 84 | }) 85 | 86 | // when 87 | const results = await getResults(runChecks(firstCheck)) 88 | 89 | // then 90 | expect(results.length).toBe(4) 91 | }) 92 | 93 | test('yields failure summary when any check fails', async () => { 94 | // given 95 | const firstCheck = jest.fn().mockResolvedValue({ 96 | results: [ 97 | Result.Success('test'), 98 | Result.Failure('test', 'test'), 99 | ], 100 | }) 101 | 102 | // when 103 | const results = await getResults(runChecks(firstCheck)) 104 | 105 | // then 106 | expect(results.pop()!.result.status).toBe('failure') 107 | }) 108 | 109 | test('yields failure summary when any check errors', async () => { 110 | // given 111 | const firstCheck = jest.fn().mockResolvedValue({ 112 | results: [ 113 | Result.Success('test'), 114 | Result.Error('test', 'test'), 115 | ], 116 | }) 117 | 118 | // when 119 | const results = await getResults(runChecks(firstCheck)) 120 | 121 | // then 122 | expect(results.pop()!.result.status).toBe('failure') 123 | }) 124 | 125 | test('yields success summary when all check succeed', async () => { 126 | // given 127 | const firstCheck = jest.fn().mockResolvedValue({ 128 | results: [ 129 | Result.Success('test'), 130 | Result.Success('test'), 131 | ], 132 | }) 133 | 134 | // when 135 | const results = await getResults(runChecks(firstCheck)) 136 | 137 | // then 138 | expect(results.pop()!.result.status).toBe('success') 139 | }) 140 | 141 | test('yields warning summary when any check fails', async () => { 142 | // given 143 | const firstCheck = jest.fn().mockResolvedValue({ 144 | results: [ 145 | Result.Success('test'), 146 | Result.Warning('test'), 147 | ], 148 | }) 149 | 150 | // when 151 | const results = await getResults(runChecks(firstCheck)) 152 | 153 | // then 154 | expect(results.pop()!.result.status).toBe('warning') 155 | }) 156 | 157 | test('sums the number of results', async () => { 158 | // given 159 | const firstCheck = jest.fn().mockResolvedValue({ 160 | results: [ 161 | Result.Success('test'), 162 | Result.Success('test'), 163 | Result.Success('test'), 164 | Result.Success('test'), 165 | Result.Warning('test'), 166 | Result.Warning('test'), 167 | Result.Warning('test'), 168 | Result.Error('test'), 169 | Result.Failure('test'), 170 | ], 171 | }) 172 | 173 | // when 174 | const results = await getResults(runChecks(firstCheck)) 175 | 176 | // then 177 | const summary = results.pop() 178 | const result = summary!.result as Result 179 | expect(result.details).toMatch(/Success: 4/) 180 | expect(result.details).toMatch(/Warnings: 3/) 181 | expect(result.details).toMatch(/Failures: 1/) 182 | expect(result.details).toMatch(/Errors: 1/) 183 | }) 184 | 185 | test('bumps level for nextCheck', async () => { 186 | // given 187 | const spy = jest.fn().mockResolvedValue({ 188 | result: Result.Success('test'), 189 | }) 190 | const firstCheck = jest.fn().mockResolvedValue({ 191 | result: Result.Success('test'), 192 | nextChecks: [spy], 193 | }) 194 | 195 | // when 196 | const results = await getResults(runChecks(firstCheck)) 197 | 198 | // then 199 | expect(results[2].level).toBe(1) 200 | }) 201 | 202 | test('does not bump level for nextCheck when sameLevel is true', async () => { 203 | // given 204 | const spy = jest.fn().mockResolvedValue({ 205 | result: Result.Success('test'), 206 | }) 207 | const firstCheck = jest.fn().mockResolvedValue({ 208 | result: Result.Success('test'), 209 | nextChecks: [spy], 210 | sameLevel: true, 211 | }) 212 | 213 | // when 214 | const results = await getResults(runChecks(firstCheck)) 215 | 216 | // then 217 | expect(results[2].level).toBe(0) 218 | }) 219 | 220 | test('yields error when a check throws', async () => { 221 | // given 222 | const firstCheck = jest.fn().mockImplementation(() => { 223 | throw new Error('Check failed catastrophically') 224 | }) 225 | 226 | // when 227 | const results = await getResults(runChecks(firstCheck)) 228 | 229 | // then 230 | const { result } = results[1] as any 231 | expect(result.status).toBe('error') 232 | expect(result.details.message).toBe('Check failed catastrophically') 233 | }) 234 | 235 | test('keeps same level when a check throws', async () => { 236 | // given 237 | const throwingCheck = jest.fn().mockImplementation(() => { 238 | throw new Error('Check failed catastrophically') 239 | }) 240 | const secondCheck = jest.fn().mockResolvedValue({ 241 | result: Result.Success('second'), 242 | nextChecks: [throwingCheck], 243 | }) 244 | const firstCheck = jest.fn().mockResolvedValue({ 245 | result: Result.Success('first'), 246 | nextChecks: [secondCheck], 247 | }) 248 | 249 | // when 250 | const results = await getResults(runChecks(firstCheck)) 251 | 252 | // then 253 | const { level } = results[2]! 254 | expect(level).toBe(1) 255 | }) 256 | }) 257 | -------------------------------------------------------------------------------- /validator-core/run-checks.ts: -------------------------------------------------------------------------------- 1 | import { checkChain, Context, IResult, Result } from '.' 2 | 3 | function wrapCheck(check: checkChain, level: number) { 4 | return async function (ctx: Context) { 5 | const { result, results, nextChecks, sameLevel } = await check.call(ctx) 6 | 7 | const outResults: IResult[] = [] 8 | if (result) { 9 | outResults.push(result) 10 | } 11 | if (Array.isArray(results)) { 12 | outResults.push(...results) 13 | } 14 | 15 | return { 16 | level, 17 | results: outResults, 18 | nextChecks: nextChecks || [], 19 | bumpLevel: !sameLevel, 20 | } 21 | } 22 | } 23 | 24 | export async function * runChecks(firstCheck: checkChain) { 25 | const summary = { 26 | successes: 0, 27 | warnings: 0, 28 | failures: 0, 29 | errors: 0, 30 | } 31 | const context = { 32 | visitedUrls: [], 33 | } 34 | const checkQueue = [wrapCheck(firstCheck, 0)] 35 | 36 | yield { 37 | level: 0, 38 | result: Result.Informational('Analysis started...'), 39 | } 40 | 41 | let previousLevel = 0 42 | while (checkQueue.length > 0) { 43 | const currentCheck = checkQueue.splice(0, 1)[0] 44 | const { results, nextChecks, level, bumpLevel } = await currentCheck(context) 45 | .catch(reason => ({ 46 | results: [Result.Error('Unhandled error occurred in check', reason)], 47 | nextChecks: [] as checkChain[], 48 | level: previousLevel, 49 | bumpLevel: false, 50 | })) 51 | previousLevel = level 52 | 53 | for (const result of results) { 54 | switch (result.status) { 55 | case 'success': 56 | summary.successes++ 57 | break 58 | case 'failure': 59 | summary.failures++ 60 | break 61 | case 'warning': 62 | summary.warnings++ 63 | break 64 | case 'error': 65 | summary.errors++ 66 | break 67 | } 68 | 69 | yield { 70 | result, 71 | level, 72 | } 73 | } 74 | 75 | const wrapped = nextChecks.map(check => { 76 | let nextLevel = level 77 | if (results.length > 0 && bumpLevel) { 78 | nextLevel += 1 79 | } 80 | 81 | return wrapCheck(check, nextLevel) 82 | }) 83 | 84 | checkQueue.unshift(...wrapped) 85 | } 86 | 87 | let details = `Success: ${summary.successes}` 88 | let summaryResult = Result.Success('Analysis complete', details) 89 | 90 | if (summary.warnings > 0) { 91 | details = `${details}, Warnings: ${summary.warnings}` 92 | summaryResult = Result.Warning('Analysis complete with warnings', details) 93 | } 94 | 95 | if (summary.failures > 0) { 96 | details = `${details}, Failures: ${summary.failures}` 97 | summaryResult = Result.Failure('Analysis complete with errors', details) 98 | } 99 | 100 | if (summary.errors > 0) { 101 | details = `${details}, Errors: ${summary.errors}` 102 | summaryResult = Result.Failure('Analysis complete with errors', details) 103 | } 104 | 105 | yield { 106 | level: 0, 107 | result: summaryResult, 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /validator-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": [ 5 | "es2015", 6 | "dom" 7 | ], 8 | "strict": true, 9 | "declaration": true, 10 | "sourceMap": false, 11 | "esModuleInterop": true, 12 | "skipLibCheck": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /validator-e2e/.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | *.d.ts 3 | -------------------------------------------------------------------------------- /validator-e2e/.npmignore: -------------------------------------------------------------------------------- 1 | *.spec.js 2 | *.ts 3 | !*.d.ts 4 | *.spec.d.ts 5 | *.config.js 6 | coverage/ 7 | example/ 8 | tsconfig.json 9 | /.* 10 | -------------------------------------------------------------------------------- /validator-e2e/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 0.12.1 4 | 5 | ### Patch Changes 6 | 7 | - 807feca: Update builds to output proper import paths 8 | - Updated dependencies [807feca] 9 | - hydra-validator-core@0.5.1 10 | 11 | ## 0.12.0 12 | 13 | ### Minor Changes 14 | 15 | - 43da1a5: Publish as modules 16 | 17 | ## 0.11.2 18 | 19 | ### Patch Changes 20 | 21 | - 5ef2133: Set package type as ES Modules 22 | 23 | ## 0.11.1 24 | 25 | ### Patch Changes 26 | 27 | - 45aa529: Fix packaging (no JS) 28 | 29 | ## 0.11.0 30 | 31 | ### Minor Changes 32 | 33 | - 4f9a1d4: Updated alcaeus 34 | 35 | All notable changes to this project will be documented in this file. 36 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 37 | 38 | ## [0.10.7](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-e2e@0.10.6...hydra-validator-e2e@0.10.7) (2020-09-22) 39 | 40 | **Note:** Version bump only for package hydra-validator-e2e 41 | 42 | ## [0.10.6](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-e2e@0.10.5...hydra-validator-e2e@0.10.6) (2020-09-22) 43 | 44 | ### Bug Fixes 45 | 46 | - headers and update alcaeus ([e5d44b2](https://github.com/hypermedia-app/hydra-validator/commit/e5d44b2a4d6d190f4fb5c078c9dae6be7afd6a34)) 47 | 48 | ## [0.10.5](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-e2e@0.10.4...hydra-validator-e2e@0.10.5) (2020-05-04) 49 | 50 | **Note:** Version bump only for package hydra-validator-e2e 51 | 52 | ## [0.10.4](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-e2e@0.10.3...hydra-validator-e2e@0.10.4) (2020-05-02) 53 | 54 | ### Bug Fixes 55 | 56 | - **e2e:** latest alcaeus fixes managing resource state ([cb07289](https://github.com/hypermedia-app/hydra-validator/commit/cb07289d78df1080b2fad7457f4a856be3a666f6)) 57 | - **e2e:** literal was not logged properly on check fail ([03a9789](https://github.com/hypermedia-app/hydra-validator/commit/03a9789988481d6d095f4e8ad5b66d24d1961873)) 58 | 59 | ## [0.10.3](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-e2e@0.10.2...hydra-validator-e2e@0.10.3) (2020-04-25) 60 | 61 | **Note:** Version bump only for package hydra-validator-e2e 62 | 63 | ## [0.10.2](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-e2e@0.10.1...hydra-validator-e2e@0.10.2) (2020-04-24) 64 | 65 | **Note:** Version bump only for package hydra-validator-e2e 66 | 67 | ## [0.10.1](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-e2e@0.10.0...hydra-validator-e2e@0.10.1) (2020-04-20) 68 | 69 | **Note:** Version bump only for package hydra-validator-e2e 70 | 71 | # [0.10.0](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-e2e@0.10.0-alpha.1...hydra-validator-e2e@0.10.0) (2020-04-15) 72 | 73 | ### Bug Fixes 74 | 75 | - **e2e:** link should not fail fast when there is a status check ([29fc39c](https://github.com/hypermedia-app/hydra-validator/commit/29fc39c0923109b62ee2d2b07d5b15379b85ee41)) 76 | - **e2e:** minor fixes to debug messages ([2df03d4](https://github.com/hypermedia-app/hydra-validator/commit/2df03d4b99187e3e345098ae8b927837b27459e1)) 77 | 78 | # [0.10.0-alpha.1](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-e2e@0.10.0-alpha.0...hydra-validator-e2e@0.10.0-alpha.1) (2020-04-15) 79 | 80 | ### Bug Fixes 81 | 82 | - **e2e:** did not work with absolute paths ([81fecb9](https://github.com/hypermedia-app/hydra-validator/commit/81fecb93cd73b383a084f8bf018c5ddd76a13245)) 83 | - **e2e:** only fail links when they have child steps ([f97c08b](https://github.com/hypermedia-app/hydra-validator/commit/f97c08b4731f47eb67a86b02fe3c0ee6ca24cfbb)) 84 | - **e2e:** single object of property was treated like an array ([373e8fb](https://github.com/hypermedia-app/hydra-validator/commit/373e8fba2968d699ff175a8963309cc0d2bc4f5b)) 85 | 86 | ### Features 87 | 88 | - templated links ([61755d6](https://github.com/hypermedia-app/hydra-validator/commit/61755d62c7d69270c1194b8440732194c6e3e11e)) 89 | 90 | # [0.10.0-alpha.0](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-e2e@0.9.0...hydra-validator-e2e@0.10.0-alpha.0) (2020-04-10) 91 | 92 | ### Bug Fixes 93 | 94 | - ensure all "common parsers" are st up with alcaeus ([63dae3f](https://github.com/hypermedia-app/hydra-validator/commit/63dae3f027039fe8f165808efd2925d700846aca)) 95 | - scenario should fail when link fails to dereference ([9b7442e](https://github.com/hypermedia-app/hydra-validator/commit/9b7442e81003966afaf9c3355b05c8639f6ba27e)) 96 | 97 | # [0.9.0](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-e2e@0.8.3...hydra-validator-e2e@0.9.0) (2020-03-17) 98 | 99 | ### Features 100 | 101 | - it was not possible to check only one of multiple values ([6e2d811](https://github.com/hypermedia-app/hydra-validator/commit/6e2d811e948ea0c5f242940da1a1c759b9d8bf94)) 102 | 103 | ### BREAKING CHANGES 104 | 105 | - property step now only fails if all objects fail 106 | 107 | ## [0.8.3](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-e2e@0.8.2...hydra-validator-e2e@0.8.3) (2020-01-23) 108 | 109 | ### Bug Fixes 110 | 111 | - only count top-level steps towards strict run verification ([e20e00a](https://github.com/hypermedia-app/hydra-validator/commit/e20e00a)), closes [#70](https://github.com/hypermedia-app/hydra-validator/issues/70) 112 | 113 | ## [0.8.2](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-e2e@0.8.1...hydra-validator-e2e@0.8.2) (2019-12-08) 114 | 115 | **Note:** Version bump only for package hydra-validator-e2e 116 | 117 | ## [0.8.1](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-e2e@0.8.0...hydra-validator-e2e@0.8.1) (2019-12-07) 118 | 119 | **Note:** Version bump only for package hydra-validator-e2e 120 | 121 | # [0.8.0](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-e2e@0.7.0...hydra-validator-e2e@0.8.0) (2019-12-06) 122 | 123 | ### Bug Fixes 124 | 125 | - make the log on E2eContext optional ([40b72a8](https://github.com/hypermedia-app/hydra-validator/commit/40b72a8)) 126 | 127 | ### Features 128 | 129 | - apply default scenario headers when performing requests ([db260d8](https://github.com/hypermedia-app/hydra-validator/commit/db260d8)) 130 | 131 | # [0.7.0](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-e2e@0.6.0...hydra-validator-e2e@0.7.0) (2019-12-03) 132 | 133 | ### Features 134 | 135 | - append to entrypoint to base URI ([acb8fe1](https://github.com/hypermedia-app/hydra-validator/commit/acb8fe1)) 136 | 137 | # [0.6.0](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-e2e@0.5.1...hydra-validator-e2e@0.6.0) (2019-11-08) 138 | 139 | ### Features 140 | 141 | - add strict option to verify all steps have been visited ([#48](https://github.com/hypermedia-app/hydra-validator/issues/48)) ([34dc650](https://github.com/hypermedia-app/hydra-validator/commit/34dc650)) 142 | 143 | ## [0.5.1](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-e2e@0.5.0...hydra-validator-e2e@0.5.1) (2019-10-31) 144 | 145 | **Note:** Version bump only for package hydra-validator-e2e 146 | 147 | # [0.5.0](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-e2e@0.4.2...hydra-validator-e2e@0.5.0) (2019-10-11) 148 | 149 | ### Bug Fixes 150 | 151 | - handling literals for links and broken links ([39a67f7](https://github.com/hypermedia-app/hydra-validator/commit/39a67f7)), closes [#46](https://github.com/hypermedia-app/hydra-validator/issues/46) 152 | 153 | ### Features 154 | 155 | - **e2e:** add support for Expect Id statement ([f310d8c](https://github.com/hypermedia-app/hydra-validator/commit/f310d8c)) 156 | 157 | ## [0.4.2](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-e2e@0.4.1...hydra-validator-e2e@0.4.2) (2019-08-29) 158 | 159 | ### Bug Fixes 160 | 161 | - keep the link from being followed multiple times ([ae2bc37](https://github.com/hypermedia-app/hydra-validator/commit/ae2bc37)) 162 | 163 | ## [0.4.1](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-e2e@0.4.0...hydra-validator-e2e@0.4.1) (2019-08-29) 164 | 165 | ### Bug Fixes 166 | 167 | - fallback in case of not annotated links ([2068a5b](https://github.com/hypermedia-app/hydra-validator/commit/2068a5b)) 168 | - prevent non-string variable from being dereferneced ([ec6b31d](https://github.com/hypermedia-app/hydra-validator/commit/ec6b31d)) 169 | 170 | # [0.4.0](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-e2e@0.3.3...hydra-validator-e2e@0.4.0) (2019-08-22) 171 | 172 | ### Bug Fixes 173 | 174 | - don't log duplicate message when processing array of values ([009291b](https://github.com/hypermedia-app/hydra-validator/commit/009291b)) 175 | 176 | ### Features 177 | 178 | - add constraints to class, property and link blocks ([94d77ef](https://github.com/hypermedia-app/hydra-validator/commit/94d77ef)) 179 | 180 | ## [0.3.3](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-e2e@0.3.2...hydra-validator-e2e@0.3.3) (2019-08-19) 181 | 182 | ### Bug Fixes 183 | 184 | - invocation step must send all headers to the API ([279596d](https://github.com/hypermedia-app/hydra-validator/commit/279596d)) 185 | 186 | ## [0.3.2](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-e2e@0.3.1...hydra-validator-e2e@0.3.2) (2019-08-05) 187 | 188 | **Note:** Version bump only for package hydra-validator-e2e 189 | 190 | ## [0.3.1](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-e2e@0.3.0...hydra-validator-e2e@0.3.1) (2019-07-31) 191 | 192 | **Note:** Version bump only for package hydra-validator-e2e 193 | 194 | # [0.3.0](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-e2e@0.2.0...hydra-validator-e2e@0.3.0) (2019-07-31) 195 | 196 | ### Bug Fixes 197 | 198 | - property comparison with expected falsy values ([59af108](https://github.com/hypermedia-app/hydra-validator/commit/59af108)) 199 | - property statement should compare string representation ([64fe437](https://github.com/hypermedia-app/hydra-validator/commit/64fe437)) 200 | - silently ignore non-strict operation blocks ([4a03ff3](https://github.com/hypermedia-app/hydra-validator/commit/4a03ff3)) 201 | 202 | ### Features 203 | 204 | - **validator-e2e:** warn when representation cannot be figured out ([a8c0ff9](https://github.com/hypermedia-app/hydra-validator/commit/a8c0ff9)) 205 | - invocation step to handle headers and bodies ([3c3b878](https://github.com/hypermedia-app/hydra-validator/commit/3c3b878)) 206 | - operation step can be not strict ([b2310fe](https://github.com/hypermedia-app/hydra-validator/commit/b2310fe)) 207 | - property statement now handles rdf:type assertion ([331eaac](https://github.com/hypermedia-app/hydra-validator/commit/331eaac)) 208 | - run child checks on all values of link/property arrays ([a03c8f2](https://github.com/hypermedia-app/hydra-validator/commit/a03c8f2)) 209 | - update link step to current DSL state ([9e39a68](https://github.com/hypermedia-app/hydra-validator/commit/9e39a68)) 210 | 211 | # 0.2.0 (2019-07-23) 212 | 213 | ### Bug Fixes 214 | 215 | - **hydra-validator:** load docs file based on current working dir ([ee400ba](https://github.com/hypermedia-app/hydra-validator/commit/ee400ba)) 216 | - **validator-cli:** pass description and default value to commanderjs ([4939e43](https://github.com/hypermedia-app/hydra-validator/commit/4939e43)) 217 | - **validator-e2e:** operation id check against types ([5ec7f3a](https://github.com/hypermedia-app/hydra-validator/commit/5ec7f3a)) 218 | - **validator-e2e:** run class step ony when RDF type matches ([d273da8](https://github.com/hypermedia-app/hydra-validator/commit/d273da8)) 219 | 220 | ### Features 221 | 222 | - **validator-e2e:** first PoC of E2E runner ([ca9f95c](https://github.com/hypermedia-app/hydra-validator/commit/ca9f95c)) 223 | - **validator-e2e:** keep track of successfully executed steps ([a0a019e](https://github.com/hypermedia-app/hydra-validator/commit/a0a019e)) 224 | - **validator-e2e:** separate operation from invocation ([cb93b34](https://github.com/hypermedia-app/hydra-validator/commit/cb93b34)) 225 | - **validator-e2e:** update expectation and property steps ([10e6f82](https://github.com/hypermedia-app/hydra-validator/commit/10e6f82)) 226 | -------------------------------------------------------------------------------- /validator-e2e/README.md: -------------------------------------------------------------------------------- 1 | > # hydra-validator-e2e 2 | > End-to-end CLI plugin for testing Hydra APIs 3 | 4 | ## Installation 5 | 6 | ```shell 7 | npm i hydra-validator hydra-validator-e2e 8 | ``` 9 | 10 | ## Usage 11 | 12 | Installing the plugin adds a command to the runner: 13 | 14 | ``` 15 | > hydra-validator e2e --help 16 | 17 | Usage: e2e [options] 18 | 19 | Options: 20 | -d, --docs path to JSON containing test scenarios 21 | -h, --help output usage information 22 | ``` 23 | 24 | Example test run 25 | 26 | ``` 27 | hydra-validator e2e -d ./example/hydracg-movies-api.json https://hydra-movies.herokuapp.com/ 28 | ``` 29 | -------------------------------------------------------------------------------- /validator-e2e/babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-typescript" 4 | ], 5 | "plugins": [ 6 | [ 7 | "babel-plugin-add-import-extension", 8 | { 9 | "extension": "js" 10 | } 11 | ] 12 | ], 13 | "ignore": ["**/node_modules/"] 14 | } 15 | -------------------------------------------------------------------------------- /validator-e2e/example/data-cube-curation.hydra: -------------------------------------------------------------------------------- 1 | PREFIX api: 2 | PREFIX hydra: 3 | PREFIX schema: 4 | PREFIX csvw: 5 | 6 | With Class api:Entrypoint { 7 | Expect Property api:dataCubeProjects { 8 | Expect Operation api:CreateDataCubeProject 9 | } 10 | } 11 | 12 | With Operation api:CreateDataCubeProject { 13 | Invoke { 14 | Content-Type "text/turtle" 15 | 16 | ``` 17 | @prefix schema: . 18 | 19 | <> schema:name "BAFU UBD 28" . 20 | ``` 21 | } => { 22 | Expect Status 201 23 | Expect Header Location 24 | 25 | Expect Type api:DataCubeProject 26 | Expect Property schema:name "BAFU UBD 28" 27 | 28 | Expect Property api:DataCubeProject/sources { 29 | Expect Property hydra:totalItems 0 30 | } 31 | } 32 | } 33 | 34 | With Class api:DataCubeProject { 35 | With Property api:DataCubeProject/sources { 36 | Expect Operation api:DataCubeProject/PostSource 37 | } 38 | } 39 | 40 | With Link api:DataCubeProject/sources { 41 | Expect Status 200 42 | } 43 | 44 | With Operation api:DataCubeProject/PostSource { 45 | Invoke { 46 | Content-Type "text/csv" 47 | Content-Disposition 'attachment; filename="UBD0028.Daten_de.csv"' 48 | 49 | <<< "/Users/tomaszpluskiewicz/projects/zazuko/data-cube-curation/api/test/bodies/UBD0028.Daten_de.csv" 50 | } => { 51 | Expect Status 201 52 | Expect Type api:DataCubeSource 53 | 54 | Expect Property schema:name "UBD0028.Daten_de.csv" 55 | 56 | Expect Link api:DataCubeSource/csvwMetadata { 57 | Expect Header Content-Type "application/csvm+json" 58 | Expect Status 200 59 | 60 | Expect Property csvw:tableSchema { 61 | Expect Property csvw:columns { 62 | Expect Property csvw:suppressOutput true 63 | } 64 | 65 | With Property csvw:columns { 66 | When Property csvw:titles Equals "station_id" 67 | 68 | Expect Property csvw:propertyUrl "http://environment.data.admin.ch/ubd/28/station_id" 69 | } 70 | } 71 | } 72 | 73 | Expect Property api:DataCubeSource/columns { 74 | Expect Type hydra:Collection 75 | 76 | Expect Property hydra:totalItems 19 77 | Expect Property hydra:member { 78 | Expect Type api:DataCubeSourceColumn 79 | 80 | Expect Property api:DataCubeSourceColumn/title 81 | Expect Property api:DataCubeSourceColumn/mapped false 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /validator-e2e/example/flick-mis-traemli.hydra: -------------------------------------------------------------------------------- 1 | With Class { 2 | With Property { 3 | Expect Operation { 4 | Invoke { 5 | Expect Status 204 6 | Expect Header Location [uri] 7 | 8 | Follow [uri] 9 | } 10 | } 11 | } 12 | 13 | Expect Property 14 | 15 | Expect Link 16 | } 17 | 18 | With Class { 19 | Expect Property { 20 | With Operation { 21 | Invoke { 22 | Expect Status 200 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /validator-e2e/example/hydracg-movies.hydra: -------------------------------------------------------------------------------- 1 | With Class { 2 | Expect Property { 3 | Expect Operation { 4 | Invoke { 5 | Expect Status 201 6 | } 7 | } 8 | } 9 | 10 | Expect Link { 11 | Expect Status 200 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /validator-e2e/index.spec.ts: -------------------------------------------------------------------------------- 1 | import * as docsLoader from './lib/docsLoader' 2 | import * as createSteps from './lib/steps/factory' 3 | import { check } from './' 4 | import * as responseChecks from './lib/checkRunner' 5 | import { E2eContext } from './types' 6 | import { expect } from 'chai' 7 | import { describe, beforeEach, it } from 'mocha' 8 | import sinon from 'sinon' 9 | 10 | describe('validator-e2e', () => { 11 | let context: E2eContext 12 | 13 | beforeEach(() => { 14 | context = { 15 | scenarios: [], 16 | basePath: '', 17 | } 18 | 19 | sinon.restore() 20 | sinon.stub(responseChecks) 21 | }) 22 | 23 | describe('factory method', () => { 24 | it('throws if the docs file fails to load', async () => { 25 | // given 26 | sinon.stub(docsLoader, 'load').throws(new Error('test')) 27 | 28 | // when 29 | const { result } = await check('urn:irrelevant', { 30 | docs: '/no/such/file', 31 | cwd: '/', 32 | strict: false, 33 | }).call(context) 34 | 35 | // then 36 | expect(result!.status).to.eq('failure') 37 | }) 38 | 39 | it('sets base path', async () => { 40 | // given 41 | sinon.stub(docsLoader, 'load').callsFake(() => ({} as any)) 42 | sinon.stub(createSteps, 'default').returns({ steps: [{}, {}, {}] } as any) 43 | 44 | // when 45 | await check('urn:irrelevant', { 46 | docs: '/base/path/docs.api', 47 | cwd: '/', 48 | strict: false, 49 | }).call(context) 50 | 51 | // then 52 | expect(context.basePath).to.eq('/base/path') 53 | }) 54 | }) 55 | 56 | describe('check', () => { 57 | it('sets loaded scenarios to context', async () => { 58 | // given 59 | sinon.stub(docsLoader, 'load').returns({ 60 | steps: [], 61 | }) 62 | sinon.stub(createSteps, 'default').returns([{}, {}, {}] as any) 63 | 64 | // when 65 | await check('urn:irrelevant', { 66 | docs: '/no/such/file', 67 | cwd: '/', 68 | strict: false, 69 | }).call(context) 70 | 71 | // then 72 | expect(context.scenarios.length).to.eq(3) 73 | }) 74 | 75 | it('passes the loaded response to response checks', async () => { 76 | // given 77 | sinon.stub(docsLoader, 'load').returns({ 78 | steps: [], 79 | }) 80 | 81 | // when 82 | await check('urn:irrelevant', { 83 | docs: '/no/such/file', 84 | cwd: '/', 85 | strict: false, 86 | }).call(context) 87 | 88 | // then 89 | expect(responseChecks.getUrlRunner).to.have.been.calledWith('urn:irrelevant') 90 | }) 91 | 92 | it('appends entrypoint path to the base URL', async () => { 93 | // given 94 | sinon.stub(docsLoader, 'load').returns({ 95 | entrypoint: 'some/resource', 96 | steps: [], 97 | }) 98 | sinon.stub(createSteps, 'default').returns([{}, {}, {}] as any) 99 | 100 | // when 101 | await check('http://base.url/api/', { 102 | docs: '/no/such/file', 103 | cwd: '/', 104 | strict: false, 105 | }).call(context) 106 | 107 | // then 108 | expect(responseChecks.getUrlRunner).to.have.been.calledWith('http://base.url/api/some/resource') 109 | }) 110 | 111 | it('sets default headers to context', async () => { 112 | // given 113 | sinon.stub(docsLoader, 'load').returns({ 114 | defaultHeaders: { 115 | Authorization: ['Basic 12345=='], 116 | }, 117 | steps: [], 118 | }) 119 | sinon.stub(createSteps, 'default').returns([{}, {}, {}] as any) 120 | 121 | // when 122 | await check('http://base.url/api/', { 123 | docs: '/no/such/file', 124 | cwd: '/', 125 | strict: false, 126 | }).call(context) 127 | 128 | // then 129 | expect(context.headers?.get('Authorization')).to.eq('Basic 12345==') 130 | }) 131 | }) 132 | }) 133 | -------------------------------------------------------------------------------- /validator-e2e/index.ts: -------------------------------------------------------------------------------- 1 | import { checkChain, Result } from 'hydra-validator-core' 2 | import { resolve, dirname } from 'path' 3 | import { E2eOptions, E2eContext } from './types' 4 | import { getUrlRunner } from './lib/checkRunner' 5 | import { load, ScenarioJson } from './lib/docsLoader' 6 | import createSteps, { RuntimeStep } from './lib/steps/factory' 7 | import { verifyTopLevelBlocksExecuted } from './lib/strictRunVerification' 8 | import { buildHeaders } from './lib/headers' 9 | 10 | export function check(url: string, { docs, cwd, strict }: E2eOptions): checkChain { 11 | const docsPath = resolve(cwd, docs) 12 | let steps: RuntimeStep[] 13 | let resourcePath: string 14 | let scenario: ScenarioJson 15 | 16 | try { 17 | scenario = load(docsPath) 18 | steps = createSteps(scenario.steps) 19 | resourcePath = scenario.entrypoint || '' 20 | } catch (e) { 21 | return () => ({ 22 | result: Result.Failure('Failed to load test scenarios', e), 23 | }) 24 | } 25 | 26 | return async function tryFetch(this: E2eContext) { 27 | this.scenarios = steps 28 | this.basePath = dirname(docsPath) 29 | if (scenario.defaultHeaders) { 30 | this.headers = buildHeaders.call(this, scenario.defaultHeaders) 31 | } 32 | 33 | return { 34 | nextChecks: [ 35 | getUrlRunner(url + resourcePath), 36 | verifyTopLevelBlocksExecuted(strict, steps), 37 | ], 38 | sameLevel: true, 39 | } 40 | } 41 | } 42 | 43 | export const options = [ 44 | { 45 | flags: '-d, --docs ', 46 | description: 'path to JSON containing test scenarios', 47 | }, 48 | { 49 | flags: '--strict', 50 | description: 'Fail if not all steps are executed', 51 | defaultValue: false, 52 | }, 53 | ] 54 | -------------------------------------------------------------------------------- /validator-e2e/lib/checkRunner.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, beforeEach } from 'mocha' 2 | import { expect } from 'chai' 3 | import { Resource, HydraResponse } from 'alcaeus' 4 | import sinon from 'sinon' 5 | import { Hydra } from 'alcaeus/node' 6 | import { namedNode } from '@rdf-esm/data-model' 7 | import { RdfResource } from '@tpluscode/rdfine' 8 | import { getResponseRunner, getResourceRunner, getUrlRunner } from './checkRunner' 9 | import { E2eContext } from '../types' 10 | import { ScenarioStep } from './steps' 11 | import { ConstraintMock, StepSpy, StepStub } from './steps/stub' 12 | import { runAll } from './testHelpers' 13 | 14 | describe('processResponse', () => { 15 | let context: E2eContext 16 | let loadResource: sinon.SinonStub 17 | 18 | beforeEach(() => { 19 | context = { 20 | basePath: '', 21 | scenarios: [], 22 | } 23 | 24 | sinon.restore() 25 | loadResource = sinon.stub(Hydra, 'loadResource') 26 | }) 27 | 28 | describe('url runner', () => { 29 | it('fetches representation with alcaeus', async () => { 30 | // given 31 | const step: ScenarioStep = { 32 | children: [], 33 | constraints: [], 34 | } as any 35 | const runner = getUrlRunner('urn:resource:id', step) 36 | loadResource.resolves({ 37 | xhr: { 38 | url: 'x:y:z', 39 | }, 40 | }) 41 | 42 | // when 43 | await runner.call(context) 44 | 45 | // then 46 | expect(loadResource).to.have.been.calledWith('urn:resource:id') 47 | }) 48 | 49 | it('passes default headers to request', async () => { 50 | // given 51 | const step: ScenarioStep = { 52 | children: [], 53 | constraints: [], 54 | } as any 55 | const runner = getUrlRunner('urn:resource:id', step) 56 | loadResource.resolves({ 57 | xhr: { 58 | url: 'x:y:z', 59 | }, 60 | }) 61 | const headersInit = new Headers({ 62 | Authorization: 'Bearer jwt', 63 | }) 64 | context.headers = headersInit 65 | 66 | // when 67 | await runner.call(context) 68 | 69 | // then 70 | expect(loadResource).to.have.been.calledWith('urn:resource:id', headersInit) 71 | }) 72 | 73 | it('fails when request fails', async () => { 74 | // given 75 | loadResource.rejects(new Error('Failed to dereference link')) 76 | const runner = getUrlRunner('urn:resource:id', new StepStub('ignored')) 77 | 78 | // when 79 | const { result } = await runner.call(context) 80 | 81 | // then 82 | expect(result!.status).to.eq('error') 83 | }) 84 | 85 | it('can fail fast when request is not successful', async () => { 86 | // given 87 | loadResource.resolves({ 88 | response: { 89 | xhr: { 90 | ok: false, 91 | status: 404, 92 | statusText: 'Not Found', 93 | }, 94 | }, 95 | }) 96 | const runner = getUrlRunner('urn:resource:id', new StepStub('ignored'), true) 97 | 98 | // when 99 | const { result } = await runner.call(context) 100 | 101 | // then 102 | expect(result!.status).to.eq('failure') 103 | }) 104 | }) 105 | 106 | describe('response runner', () => { 107 | it('fails when the parameter is undefined', async () => { 108 | // given 109 | const step: ScenarioStep = { 110 | children: [], 111 | constraints: [], 112 | } as any 113 | const runner = getResponseRunner(undefined as any, step) 114 | 115 | // when 116 | const { result } = await runner.call(context) 117 | 118 | // then 119 | expect(result!.status).to.eq('failure') 120 | }) 121 | 122 | it('fails when the parameter is a literal', async () => { 123 | // given 124 | const step: ScenarioStep = { 125 | children: [], 126 | constraints: [], 127 | } as any 128 | const runner = getResponseRunner('not a link somehow' as any, step) 129 | 130 | // when 131 | const { result } = await runner.call(context) 132 | 133 | // then 134 | expect(result!.status).to.eq('failure') 135 | }) 136 | 137 | it('dereferences a resource', async () => { 138 | // given 139 | const step: ScenarioStep = { 140 | children: [], 141 | constraints: [], 142 | } as any 143 | const resource: Partial = { 144 | id: namedNode('foo'), 145 | } 146 | loadResource.resolves({ 147 | xhr: { 148 | url: 'x:y:z', 149 | }, 150 | }) 151 | const runner = getResponseRunner(resource as any, step) 152 | 153 | // when 154 | await runner.call(context) 155 | 156 | // then 157 | expect(Hydra.loadResource).to.have.been.calledWith('foo') 158 | }) 159 | 160 | it('does not perform request when passed a response object', async () => { 161 | // given 162 | const step: ScenarioStep = { 163 | children: [], 164 | constraints: [], 165 | } as any 166 | const response: HydraResponse = { 167 | xhr: { url: 'foo' }, 168 | } as any 169 | const runner = getResponseRunner(response, step) 170 | 171 | // when 172 | await runner.call(context) 173 | 174 | // then 175 | expect(Hydra.loadResource).not.to.have.been.called 176 | }) 177 | 178 | it('runs steps on representation', async () => { 179 | // given 180 | const spy = new StepSpy() 181 | const step: ScenarioStep = { 182 | children: [spy], 183 | constraints: [], 184 | } as any 185 | const topLevelStep = new StepSpy() 186 | const response: HydraResponse = { 187 | response: { xhr: { url: 'foo' } }, 188 | } as any 189 | const runner = getResponseRunner(response, step) 190 | context.scenarios.push(topLevelStep) 191 | 192 | // when 193 | await runAll(runner, context) 194 | 195 | // then 196 | expect(spy.runner).to.have.been.called 197 | expect(topLevelStep.runner).to.have.been.called 198 | }) 199 | 200 | it('does not run steps when constraint fails', async () => { 201 | // given 202 | const spy = new StepSpy() 203 | const step: ScenarioStep = { 204 | children: [spy], 205 | constraints: [new ConstraintMock(false, 'Response')], 206 | } as any 207 | const topLevelStep = new StepSpy() 208 | const response: HydraResponse = { 209 | xhr: { url: 'foo' }, 210 | } as any 211 | const runner = getResponseRunner(response, step) 212 | context.scenarios.push(topLevelStep) 213 | 214 | // when 215 | await runAll(runner, context) 216 | 217 | // then 218 | expect(spy.runner).not.to.have.been.called 219 | expect(topLevelStep.runner).not.to.have.been.called 220 | }) 221 | }) 222 | 223 | describe('resource runner', () => { 224 | it('runs steps on representation', async () => { 225 | // given 226 | const spy = new StepSpy() 227 | const step: ScenarioStep = { 228 | children: [spy], 229 | constraints: [], 230 | } as any 231 | const topLevelStep = new StepSpy() 232 | const resource: Resource = {} as any 233 | const runner = getResourceRunner(resource, step) 234 | context.scenarios.push(topLevelStep) 235 | 236 | // when 237 | await runAll(runner, context) 238 | 239 | // then 240 | expect(spy.runner).to.have.been.called 241 | expect(topLevelStep.runner).to.have.been.called 242 | }) 243 | 244 | it('does not run steps when constraint fails', async () => { 245 | // given 246 | const spy = new StepSpy() 247 | const step: ScenarioStep = { 248 | children: [spy], 249 | constraints: [new ConstraintMock(false, 'Representation')], 250 | } as any 251 | const topLevelStep = new StepSpy() 252 | const resource: Resource = {} as any 253 | const runner = getResourceRunner(resource, step) 254 | context.scenarios.push(topLevelStep) 255 | 256 | // when 257 | await runAll(runner, context) 258 | 259 | // then 260 | expect(spy.runner).not.to.have.been.called 261 | expect(topLevelStep.runner).not.to.have.been.called 262 | }) 263 | }) 264 | }) 265 | -------------------------------------------------------------------------------- /validator-e2e/lib/checkRunner.ts: -------------------------------------------------------------------------------- 1 | import { Resource, ResourceIndexer, HydraResponse } from 'alcaeus' 2 | import { Hydra } from 'alcaeus/node' 3 | import { E2eContext } from '../types' 4 | import { checkChain, CheckResult, Result } from 'hydra-validator-core' 5 | import { ScenarioStep } from './steps' 6 | import { Constraint, RepresentationConstraint, ResponseConstraint } from './steps/constraints/Constraint' 7 | import { NamedNode } from 'rdf-js' 8 | 9 | function processResource(resource: T, steps: ScenarioStep[], constraints: Constraint[]): CheckResult { 10 | const localContext = {} 11 | const nextChecks: checkChain[] = [] 12 | 13 | const resourceConstraints: RepresentationConstraint[] = constraints.filter(c => c.type === 'Representation') 14 | const allConstraintsSatisfied = resourceConstraints.every(c => c.satisfiedBy(resource as unknown as Resource & ResourceIndexer)) 15 | 16 | if (!allConstraintsSatisfied) { 17 | return { 18 | result: Result.Informational('Skipping representation steps due to scenario constraints'), 19 | } 20 | } 21 | 22 | for (const step of steps) { 23 | if (step.appliesTo(resource)) { 24 | nextChecks.push(step.getRunner(resource, localContext)) 25 | } 26 | } 27 | 28 | return { 29 | nextChecks, 30 | sameLevel: true, 31 | } 32 | } 33 | 34 | function processResponse(response: HydraResponse, steps: ScenarioStep[], constraints: Constraint[], failOnNegativeResponse: boolean): CheckResult { 35 | const localContext = {} 36 | 37 | const nextChecks: checkChain[] = [] 38 | const resource = response.representation?.root 39 | const xhr = response.response?.xhr 40 | 41 | if (!xhr) { 42 | return { 43 | result: Result.Error('No response to request'), 44 | sameLevel: true, 45 | } 46 | } 47 | 48 | if (!xhr?.ok && failOnNegativeResponse) { 49 | return { 50 | result: Result.Failure(`Failed to dereference resource ${xhr.url}. Response was ${xhr.status} ${xhr.statusText}`), 51 | sameLevel: true, 52 | } 53 | } 54 | 55 | const results = [ 56 | Result.Informational(`Fetched resource ${xhr.url}`), 57 | ] 58 | 59 | if (!xhr.ok) { 60 | results.push(Result.Warning(`Response was ${xhr.status} ${xhr.statusText}`)) 61 | } 62 | 63 | const responseConstraints: ResponseConstraint[] = constraints.filter(c => c.type === 'Response') 64 | const allConstraintsSatisfied = responseConstraints.every(c => c.satisfiedBy(response)) 65 | 66 | if (!allConstraintsSatisfied) { 67 | return { 68 | result: Result.Informational(`Skipping response from ${xhr.url} due to scenario constraints`), 69 | } 70 | } 71 | 72 | for (const step of steps) { 73 | if (step.appliesTo(xhr)) { 74 | nextChecks.push(step.getRunner(xhr, localContext)) 75 | } 76 | } 77 | 78 | if (!resource) { 79 | results.push(Result.Warning('Could not determine the resource representation')) 80 | } else { 81 | nextChecks.push(() => processResource(resource, steps, constraints)) 82 | } 83 | 84 | return { 85 | results, 86 | nextChecks, 87 | sameLevel: true, 88 | } 89 | } 90 | 91 | function dereferenceAndProcess(id: string | NamedNode, steps: ScenarioStep[], constraints: Constraint[], headers: HeadersInit | null, failOnNegativeResponse: boolean) { 92 | const uri: string = typeof id === 'string' ? id : id.value 93 | 94 | const loadResource = headers 95 | ? Hydra.loadResource(uri, headers) 96 | : Hydra.loadResource(uri) 97 | 98 | return loadResource 99 | .then(response => { 100 | return processResponse(response, steps, constraints, failOnNegativeResponse) 101 | }) 102 | .catch(e => ({ 103 | result: Result.Error(`Failed to dereference ${uri}`, e), 104 | })) 105 | } 106 | 107 | export function getResourceRunner( 108 | resource: T, 109 | currentStep: ScenarioStep) { 110 | return async function checkResource(this: E2eContext) { 111 | const steps = [...currentStep.children, ...this.scenarios].filter(s => s !== currentStep) 112 | return processResource(resource, steps, currentStep.constraints) 113 | } 114 | } 115 | 116 | export function getUrlRunner(id: string, currentStep?: ScenarioStep, failOnNegativeResponse = false) { 117 | const childSteps = currentStep ? currentStep.children : [] 118 | const constraints = currentStep ? currentStep.constraints : [] 119 | 120 | return async function checkResourceResponse(this: E2eContext) { 121 | const steps = [...childSteps, ...this.scenarios] 122 | 123 | return dereferenceAndProcess(id, steps, constraints, this.headers || null, failOnNegativeResponse) 124 | } 125 | } 126 | 127 | export function getResponseRunner( 128 | resourceOrResponse: Resource | HydraResponse, 129 | currentStep?: ScenarioStep, 130 | failOnNegativeResponse = false) { 131 | const childSteps = currentStep ? currentStep.children : [] 132 | const constraints = currentStep ? currentStep.constraints : [] 133 | 134 | return async function checkResourceResponse(this: E2eContext) { 135 | const steps = [...childSteps, ...this.scenarios] 136 | 137 | if (typeof resourceOrResponse === 'object') { 138 | if ('id' in resourceOrResponse) { 139 | if (resourceOrResponse.id.termType === 'BlankNode') { 140 | return { 141 | result: Result.Failure('Cannot dereference blank node identifier'), 142 | } 143 | } 144 | 145 | return dereferenceAndProcess(resourceOrResponse.id, steps, constraints, this.headers || null, failOnNegativeResponse) 146 | } 147 | return processResponse(resourceOrResponse, steps, constraints, failOnNegativeResponse) 148 | } 149 | 150 | return { 151 | result: Result.Failure(`Could not dereference resource. Value ${resourceOrResponse} does not appear to be a link`), 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /validator-e2e/lib/comparison.spec.ts: -------------------------------------------------------------------------------- 1 | import areEqual from './comparison' 2 | import { literal } from '@rdf-esm/data-model' 3 | import namespace from '@rdfjs/namespace' 4 | import { prefixes } from '@zazuko/rdf-vocabularies' 5 | import { Literal } from 'rdf-js' 6 | import { expect } from 'chai' 7 | import { describe, it } from 'mocha' 8 | 9 | const xsd = namespace(prefixes.xsd) 10 | 11 | describe('areEqual', () => { 12 | const equalPairs: [string | number | boolean, Literal][] = [ 13 | [0, literal('0', xsd.int)], 14 | [0, literal('0')], 15 | ['0', literal('0')], 16 | [false, literal('false', xsd.boolean)], 17 | [false, literal('false')], 18 | [true, literal('true')], 19 | ['foo', literal('foo')], 20 | ] 21 | 22 | equalPairs.forEach(pair => { 23 | it(`${typeof pair[0]} ${pair[0]} should equal ${typeof pair[1]} ${pair[1]}`, () => { 24 | expect(areEqual(pair[0], pair[1])).to.be.ok 25 | }) 26 | }) 27 | 28 | const unequalPairs: [string | number | boolean, Literal][] = [ 29 | [0, literal('1', xsd.int)], 30 | [0, literal('1')], 31 | [false, literal('true', xsd.boolean)], 32 | [false, literal('true')], 33 | [true, literal('false')], 34 | ['foo', literal('bar')], 35 | ] 36 | 37 | unequalPairs.forEach(pair => { 38 | it(`${typeof pair[0]} ${pair[0]} should not equal ${typeof pair[1]} ${pair[1]}`, () => { 39 | expect(areEqual(pair[0], pair[1])).not.to.be.ok 40 | }) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /validator-e2e/lib/comparison.ts: -------------------------------------------------------------------------------- 1 | import { Literal } from 'rdf-js' 2 | 3 | export default function areEqual(expected: unknown, actual: Literal) { 4 | if (typeof expected === 'boolean') { 5 | return expected.toString() === actual.value.toString() 6 | } 7 | 8 | // eslint-disable-next-line eqeqeq 9 | return expected == actual.value 10 | } 11 | -------------------------------------------------------------------------------- /validator-e2e/lib/docsLoader.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync, statSync } from 'fs' 2 | 3 | export interface ScenarioJson { 4 | entrypoint?: string 5 | defaultHeaders?: Record 6 | steps: unknown[] 7 | } 8 | 9 | export function load(docsPath: string): ScenarioJson { 10 | const docsFileStats = statSync(docsPath) 11 | if (!docsFileStats.isFile()) { 12 | throw new Error() 13 | } 14 | 15 | return JSON.parse(readFileSync(docsPath).toString()) 16 | } 17 | -------------------------------------------------------------------------------- /validator-e2e/lib/headers.ts: -------------------------------------------------------------------------------- 1 | import { E2eContext } from '../types' 2 | 3 | export function buildHeaders(this: E2eContext, headers: Record): Headers { 4 | return Object.entries(headers) 5 | .reduce((combined, [name, values]) => { 6 | if (Array.isArray(values) === false) { 7 | this.log && this.log.warning(`Skipping default header ${name}. Value was not the expected array`) 8 | return combined 9 | } 10 | 11 | values.forEach(value => { 12 | combined.append(name, value) 13 | }) 14 | 15 | return combined 16 | }, new Headers()) 17 | } 18 | -------------------------------------------------------------------------------- /validator-e2e/lib/steps/StepSpyMixin.spec.ts: -------------------------------------------------------------------------------- 1 | import { SpyMixin } from './StepSpyMixin' 2 | import { ScenarioStep } from './index' 3 | import { expect } from 'chai' 4 | import { describe, it } from 'mocha' 5 | 6 | class TestStep extends ScenarioStep { 7 | public constructor() { 8 | super([]) 9 | } 10 | 11 | protected appliesToInternal(): boolean { 12 | return true 13 | } 14 | 15 | public getRunner() { 16 | return () => ({}) 17 | } 18 | } 19 | 20 | class TestStepLike extends SpyMixin(TestStep) { 21 | } 22 | 23 | describe('StepSpyMixin', () => { 24 | it('does not mark as visited when runner is created', () => { 25 | // given 26 | const step = new TestStepLike() 27 | 28 | // when 29 | step.getRunner() 30 | 31 | // then 32 | expect(step.visited).to.eq(false) 33 | }) 34 | 35 | it('marks as when runner is invoked', async () => { 36 | // given 37 | const step = new TestStepLike() 38 | const runner = step.getRunner() 39 | 40 | // when 41 | await runner.call({}) 42 | 43 | // then 44 | expect(step.visited).to.eq(true) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /validator-e2e/lib/steps/StepSpyMixin.ts: -------------------------------------------------------------------------------- 1 | import { ScenarioStep } from './index' 2 | import { checkChain, Context } from 'hydra-validator-core' 3 | import { E2eContext } from '../../types' 4 | 5 | interface ScenarioStepImpl { 6 | getRunner(obj: T, localContext?: Context): checkChain 7 | } 8 | 9 | type StepConstructor = new (...args: any[]) => ScenarioStepImpl & ScenarioStep 10 | 11 | type ReturnConstructor = new (...args: any[]) => ScenarioStepImpl & { 12 | visited: boolean 13 | } 14 | 15 | export function SpyMixin>(Base: B): B & ReturnConstructor { 16 | abstract class StepSpyDecorator extends Base { 17 | private __visited = false 18 | 19 | public get visited() { 20 | return this.__visited 21 | } 22 | 23 | public getRunner(obj: T, localContext?: Context) { 24 | const realRunner = super.getRunner(obj, localContext) 25 | const step = this 26 | 27 | return function (this: E2eContext) { 28 | step.__visited = true 29 | return realRunner.call(this) 30 | } 31 | } 32 | } 33 | 34 | return StepSpyDecorator 35 | } 36 | -------------------------------------------------------------------------------- /validator-e2e/lib/steps/constraints/Constraint.ts: -------------------------------------------------------------------------------- 1 | import { Resource, ResourceIndexer, HydraResponse } from 'alcaeus' 2 | 3 | export type ConstraintOperator = 'eq' | 'gt' | 'ge' | 'lt' | 'le' | 'regex' | 'function' 4 | export type ConstraintType = 'Representation' | 'Response' | null 5 | 6 | export abstract class Constraint { 7 | private readonly __predicate: (actual: unknown) => boolean 8 | private readonly __negated: boolean 9 | 10 | public abstract get type(): ConstraintType 11 | 12 | public constructor(predicate: (actual: unknown) => boolean, negated: boolean) { 13 | this.__predicate = predicate 14 | this.__negated = negated 15 | } 16 | 17 | public satisfiedBy(subject: T): boolean { 18 | const value = this.getValue(subject) 19 | if (!this.sanityCheckValue(value)) { 20 | return false 21 | } 22 | 23 | const result = this.__predicate(value) 24 | const expected = !this.__negated 25 | 26 | return result === expected 27 | } 28 | 29 | protected abstract getValue(subject: T): unknown | null 30 | protected abstract sanityCheckValue(value: unknown): boolean 31 | } 32 | 33 | export abstract class ResponseConstraint extends Constraint { 34 | public get type(): ConstraintType { 35 | return 'Response' 36 | } 37 | } 38 | 39 | export abstract class RepresentationConstraint extends Constraint { 40 | public get type(): ConstraintType { 41 | return 'Representation' 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /validator-e2e/lib/steps/constraints/PropertyConstraint.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { describe, it, beforeEach } from 'mocha' 3 | import sinon from 'sinon' 4 | import cf, { AnyContext, AnyPointer } from 'clownface' 5 | import $rdf from 'rdf-ext' 6 | import { Hydra } from 'alcaeus/node' 7 | import { PropertyConstraint } from './PropertyConstraint' 8 | import { StepConstraintInit } from './index' 9 | import DatasetExt from 'rdf-ext/lib/Dataset' 10 | 11 | describe('PropertyConstraint', () => { 12 | const emptyInit: StepConstraintInit = {} as any 13 | let graph: AnyPointer 14 | 15 | beforeEach(() => { 16 | graph = cf({ dataset: $rdf.dataset() }) 17 | }) 18 | 19 | it('does not pass when property is undefined', () => { 20 | // given 21 | const predicate = sinon.stub() 22 | const property = 'http://example.com/prop' 23 | const init = { 24 | ...emptyInit, 25 | left: property, 26 | } 27 | const constraint = new PropertyConstraint(init, predicate, false) 28 | 29 | // when 30 | const result = constraint.satisfiedBy(Hydra.resources.factory.createEntity(graph.blankNode())) 31 | 32 | // then 33 | expect(result).to.eq(false) 34 | expect(predicate).not.to.have.been.called 35 | }) 36 | 37 | it('throws when property name is missing', () => { 38 | // given 39 | const predicate = sinon.stub() 40 | const init = { 41 | ...emptyInit, 42 | } 43 | 44 | // then 45 | expect(() => new PropertyConstraint(init, predicate, false)).to.throw() 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /validator-e2e/lib/steps/constraints/PropertyConstraint.ts: -------------------------------------------------------------------------------- 1 | import { RepresentationConstraint } from './Constraint' 2 | import { Resource, ResourceIndexer } from 'alcaeus' 3 | import { StepConstraintInit } from './index' 4 | 5 | export class PropertyConstraint extends RepresentationConstraint { 6 | private readonly __propertyName: string 7 | 8 | public constructor(init: StepConstraintInit, predicate: (value: any) => boolean, negated: boolean) { 9 | super(predicate, negated) 10 | 11 | if (!init.left) { 12 | throw new Error('Missing property name') 13 | } 14 | 15 | this.__propertyName = init.left 16 | } 17 | 18 | protected getValue(subject: Resource & ResourceIndexer) { 19 | return subject[this.__propertyName] 20 | } 21 | 22 | protected sanityCheckValue(value?: unknown): boolean { 23 | return !!value 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /validator-e2e/lib/steps/constraints/StatusConstraint.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { describe, it } from 'mocha' 3 | import sinon from 'sinon' 4 | import { StatusConstraint } from './StatusConstraint' 5 | 6 | describe('StatusConstraint', () => { 7 | it('does not pass when status is 0', () => { 8 | // given 9 | const predicate = sinon.stub() 10 | const constraint = new StatusConstraint(predicate, false) 11 | 12 | // when 13 | const result = constraint.satisfiedBy({ 14 | xhr: { 15 | status: 0, 16 | }, 17 | } as any) 18 | 19 | // then 20 | expect(result).to.eq(false) 21 | expect(predicate).not.to.have.been.called 22 | }) 23 | 24 | it('calls predicate when status code is within range', () => { 25 | // given 26 | const predicate = sinon.stub().callsFake(() => true) 27 | const constraint = new StatusConstraint(predicate, false) 28 | 29 | // when 30 | const result = constraint.satisfiedBy({ 31 | response: { 32 | xhr: { 33 | status: 303, 34 | }, 35 | }, 36 | } as any) 37 | 38 | // then 39 | expect(result).to.eq(true) 40 | expect(predicate).to.have.been.calledWith(303) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /validator-e2e/lib/steps/constraints/StatusConstraint.ts: -------------------------------------------------------------------------------- 1 | import { ResponseConstraint } from './Constraint' 2 | import { HydraResponse } from 'alcaeus' 3 | 4 | export class StatusConstraint extends ResponseConstraint { 5 | protected getValue(subject: HydraResponse): number { 6 | return subject.response?.xhr.status || 0 7 | } 8 | 9 | protected sanityCheckValue(value: unknown): boolean { 10 | return value !== 0 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /validator-e2e/lib/steps/constraints/conditions/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { factory } from './' 2 | import { StepConstraintInit } from '../' 3 | import { expect } from 'chai' 4 | import { describe, it } from 'mocha' 5 | 6 | describe('condition', () => { 7 | const emptyInit: StepConstraintInit = {} as any 8 | 9 | it('throws when condition is unrecognized', () => { 10 | // given 11 | const init = { ...emptyInit, operator: 'foo' } as any 12 | 13 | // then 14 | expect(() => factory(init)).to.throw() 15 | }) 16 | 17 | describe('eq', () => { 18 | it('returns true when string values are equal', () => { 19 | // given 20 | const init: StepConstraintInit = { 21 | ...emptyInit, 22 | operator: 'eq', 23 | right: 'expected', 24 | } 25 | 26 | // when 27 | const equality = factory(init) 28 | const result = equality('expected') 29 | 30 | // then 31 | expect(result).to.eq(true) 32 | }) 33 | }) 34 | 35 | describe('lt', () => { 36 | it('returns correct result', () => { 37 | // given 38 | const init: StepConstraintInit = { 39 | ...emptyInit, 40 | operator: 'lt', 41 | right: 100, 42 | } 43 | 44 | // when 45 | const equality = factory(init) 46 | const result = equality(99) 47 | 48 | // then 49 | expect(result).to.eq(true) 50 | }) 51 | }) 52 | 53 | describe('le', () => { 54 | it('returns correct result', () => { 55 | // given 56 | const init: StepConstraintInit = { 57 | ...emptyInit, 58 | operator: 'le', 59 | right: 100, 60 | } 61 | 62 | // when 63 | const equality = factory(init) 64 | const result = equality(100) 65 | 66 | // then 67 | expect(result).to.eq(true) 68 | }) 69 | }) 70 | 71 | describe('ge', () => { 72 | it('returns correct result', () => { 73 | // given 74 | const init: StepConstraintInit = { 75 | ...emptyInit, 76 | operator: 'ge', 77 | right: 100, 78 | } 79 | 80 | // when 81 | const equality = factory(init) 82 | const result = equality(100) 83 | 84 | // then 85 | expect(result).to.eq(true) 86 | }) 87 | }) 88 | 89 | describe('gt', () => { 90 | it('returns correct result', () => { 91 | // given 92 | const init: StepConstraintInit = { 93 | ...emptyInit, 94 | operator: 'gt', 95 | right: 100, 96 | } 97 | 98 | // when 99 | const equality = factory(init) 100 | const result = equality(101) 101 | 102 | // then 103 | expect(result).to.eq(true) 104 | }) 105 | }) 106 | 107 | describe('regex', () => { 108 | it('returns true for matching value', () => { 109 | // given 110 | const init: StepConstraintInit = { 111 | ...emptyInit, 112 | operator: 'regex', 113 | right: '^ex', 114 | } 115 | 116 | // when 117 | const regex = factory(init) 118 | const result = regex('expected') 119 | 120 | // then 121 | expect(result).to.eq(true) 122 | }) 123 | }) 124 | 125 | describe('custom', () => { 126 | it('returns true for matching value', () => { 127 | // given 128 | const init: StepConstraintInit = { 129 | ...emptyInit, 130 | operator: 'function', 131 | // eslint-disable-next-line no-template-curly-in-string 132 | right: 'name => `AS ${name.toUpperCase()}`', 133 | } 134 | 135 | // when 136 | const regex = factory(init) 137 | const result = regex('expected') 138 | 139 | // then 140 | expect(result).to.eq('AS EXPECTED') 141 | }) 142 | }) 143 | }) 144 | -------------------------------------------------------------------------------- /validator-e2e/lib/steps/constraints/conditions/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-new-func */ 2 | import { StepConstraintInit } from '../index' 3 | 4 | function equality(expected: unknown) { 5 | return function equalityPredicate(value: unknown) { 6 | return value === expected 7 | } 8 | } 9 | 10 | function inequality(operator: string, expected: unknown): () => boolean { 11 | return new Function('value', `return value ${operator} ${expected}`) as any // eslint-disable-line no-new-func 12 | } 13 | 14 | function regex(regex: RegExp) { 15 | return function regexPredicate(value: string) { 16 | return regex.test(value) 17 | } 18 | } 19 | 20 | export function factory(init: StepConstraintInit): (value: any) => boolean { 21 | const expected = init.right 22 | 23 | switch (init.operator) { 24 | case 'regex': 25 | return regex(new RegExp(init.right as string)) 26 | case 'eq': 27 | return equality(expected) 28 | case 'gt': 29 | return inequality('>', expected) 30 | case 'ge': 31 | return inequality('>=', expected) 32 | case 'lt': 33 | return inequality('<', expected) 34 | case 'le': 35 | return inequality('<=', expected) 36 | case 'function': 37 | return eval(`${init.right}`) // eslint-disable-line no-eval 38 | default: 39 | throw new Error(`Unexpected constraint operator ${init.operator}`) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /validator-e2e/lib/steps/constraints/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { describe, it, beforeEach, afterEach } from 'mocha' 3 | import sinon from 'sinon' 4 | import { factory, StepConstraintInit } from './' 5 | import * as conditions from './conditions/' 6 | import * as PC from './PropertyConstraint' 7 | import * as SC from './StatusConstraint' 8 | 9 | describe('factory', () => { 10 | const emptyInit: StepConstraintInit = {} as any 11 | 12 | beforeEach(() => { 13 | sinon.stub(conditions, 'factory').returns(() => true) 14 | sinon.stub(PC) 15 | sinon.stub(SC) 16 | }) 17 | 18 | afterEach(() => { 19 | sinon.restore() 20 | }) 21 | 22 | it('throws when type is unsupported', () => { 23 | // given 24 | const init = { 25 | ...emptyInit, 26 | constrain: 'Collection' as any, 27 | } 28 | 29 | // then 30 | expect(() => factory(init)).to.throw() 31 | }) 32 | 33 | it('throws when type is falsy', () => { 34 | // then 35 | expect(() => factory(emptyInit)).to.throw() 36 | }) 37 | 38 | it('calls conditionFactory', () => { 39 | // given 40 | const init: StepConstraintInit = { ...emptyInit, constrain: 'Property' } 41 | 42 | // when 43 | factory(init) 44 | 45 | // then 46 | expect(conditions.factory).to.have.been.calledWith(init) 47 | }) 48 | 49 | it('creates PropertyConstraint', () => { 50 | // given 51 | const init: StepConstraintInit = { 52 | ...emptyInit, 53 | constrain: 'Property', 54 | left: 'prop-name', 55 | } 56 | 57 | // when 58 | factory(init) 59 | 60 | // then 61 | expect(PC.PropertyConstraint).to.have.been.called 62 | }) 63 | 64 | it('calls StatusConstraint', () => { 65 | // given 66 | const init: StepConstraintInit = { ...emptyInit, constrain: 'Status' } 67 | 68 | // when 69 | factory(init) 70 | 71 | // then 72 | expect(SC.StatusConstraint).to.have.been.called 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /validator-e2e/lib/steps/constraints/index.ts: -------------------------------------------------------------------------------- 1 | import { Constraint, ConstraintOperator } from './Constraint' 2 | import { factory as createPredicate } from './conditions/index' 3 | import { StatusConstraint } from './StatusConstraint' 4 | import { PropertyConstraint } from './PropertyConstraint' 5 | 6 | export interface StepConstraintInit { 7 | constrain: 'Property' | 'Status' 8 | negated: boolean 9 | left?: string 10 | operator: ConstraintOperator 11 | right: unknown 12 | } 13 | 14 | export function factory(constraintInit: StepConstraintInit): Constraint { 15 | const predicate = createPredicate(constraintInit) 16 | 17 | switch (constraintInit.constrain) { 18 | case 'Status': 19 | return new StatusConstraint(predicate, constraintInit.negated) 20 | case 'Property': 21 | return new PropertyConstraint(constraintInit, predicate, constraintInit.negated) 22 | default: 23 | throw new Error(`Unexpected constraint ${constraintInit.constrain}`) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /validator-e2e/lib/steps/factory.ts: -------------------------------------------------------------------------------- 1 | import { ScenarioStep } from './index' 2 | import { ClassStep } from './representation' 3 | import { StepConstraintInit, factory as createConstraint } from './constraints/index' 4 | import { Constraint } from './constraints/Constraint' 5 | import { PropertyStep } from './representation/property' 6 | import { StatusStep } from './response/status' 7 | import { HeaderStep } from './response/header' 8 | import { InvocationStep } from './representation/operation/invocation' 9 | import { OperationStep } from './representation/operation' 10 | import { LinkStep } from './representation/link' 11 | import { FollowStep } from './representation/link/follow' 12 | import { IdentifierStep } from './representation/identifier' 13 | import { SpyMixin } from './StepSpyMixin' 14 | 15 | interface StepDescription { 16 | type: string 17 | children: StepDescription[] 18 | constraints?: StepConstraintInit[] 19 | } 20 | 21 | export type RuntimeStep = ScenarioStep & { 22 | visited: boolean 23 | } 24 | 25 | interface StepConstructor { 26 | new(stepInit: any, children: ScenarioStep[], constraints: Constraint[]): T 27 | } 28 | 29 | const stepConstructors = new Map>() 30 | function registerStep(name: string, ctor: StepConstructor) { 31 | stepConstructors.set(name, SpyMixin(ctor)) 32 | } 33 | 34 | registerStep('Class', ClassStep) 35 | registerStep('Link', LinkStep) 36 | registerStep('Property', PropertyStep) 37 | registerStep('ResponseStatus', StatusStep) 38 | registerStep('ResponseHeader', HeaderStep) 39 | registerStep('Invocation', InvocationStep) 40 | registerStep('Follow', FollowStep) 41 | registerStep('Operation', OperationStep) 42 | registerStep('Identifier', IdentifierStep) 43 | 44 | function create(step: StepDescription): RuntimeStep { 45 | const children = (step.children || []).map(create) 46 | const constraints = (step.constraints || []).map(createConstraint) 47 | 48 | const Step = stepConstructors.get(step.type) 49 | if (Step) { 50 | return new Step(step, children, constraints) 51 | } 52 | 53 | throw new Error(`Unexpected step ${step.type}`) 54 | } 55 | 56 | export default function (steps: any[]): RuntimeStep[] { 57 | return steps.map(step => create(step)) 58 | } 59 | -------------------------------------------------------------------------------- /validator-e2e/lib/steps/index.ts: -------------------------------------------------------------------------------- 1 | import { checkChain, Context } from 'hydra-validator-core' 2 | import { E2eContext } from '../../types' 3 | import { Constraint } from './constraints/Constraint' 4 | 5 | export abstract class ScenarioStep { 6 | public children: ScenarioStep[]; 7 | public constraints: Constraint[]; 8 | private __executed = false 9 | 10 | protected constructor(children: ScenarioStep[], constraints?: Constraint[]) { 11 | this.children = children 12 | this.constraints = constraints || [] 13 | } 14 | 15 | public get executed() { 16 | return this.__executed 17 | } 18 | 19 | protected markExecuted() { 20 | this.__executed = true 21 | } 22 | 23 | public appliesTo(obj: T): boolean { 24 | return !!obj && this.appliesToInternal(obj) 25 | } 26 | 27 | abstract getRunner(obj: T, localContext?: Context): checkChain; 28 | protected abstract appliesToInternal (obj: T): boolean 29 | } 30 | 31 | export abstract class ResponseStep extends ScenarioStep { 32 | protected appliesToInternal(obj: Response): boolean { 33 | return obj instanceof Response 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /validator-e2e/lib/steps/representation/identifier/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { namedNode } from '@rdf-esm/data-model' 2 | import { IdentifierStep } from './' 3 | import { E2eContext } from '../../../../types' 4 | import { describe, it } from 'mocha' 5 | import { expect } from 'chai' 6 | 7 | // jest.mock('../../../checkRunner') 8 | 9 | describe('Identifier', () => { 10 | let context: E2eContext 11 | 12 | it('applies to identified resource', () => { 13 | // given 14 | const step = new IdentifierStep({ 15 | value: 'http://foo/bar', 16 | }) 17 | 18 | // when 19 | const applies = step.appliesTo({ 20 | id: 'whatever', 21 | } as any) 22 | 23 | // then 24 | expect(applies).to.be.ok 25 | }) 26 | 27 | it('returns success when the resource identifier matches', async () => { 28 | // given 29 | const step = new IdentifierStep({ 30 | value: 'http://foo/bar', 31 | }) 32 | const resource = { 33 | id: namedNode('http://foo/bar'), 34 | } 35 | 36 | // when 37 | const execute = step.getRunner(resource as any) 38 | const { result } = await execute.call(context) 39 | 40 | // then 41 | expect(result!.status).to.eq('success') 42 | }) 43 | 44 | it('returns failure when the resource has different identifier', async () => { 45 | // given 46 | const step = new IdentifierStep({ 47 | value: 'http://foo/bar', 48 | }) 49 | const resource = { 50 | id: 'http://something/else', 51 | } 52 | 53 | // when 54 | const execute = step.getRunner(resource as any) 55 | const { result } = await execute.call(context) 56 | 57 | // then 58 | expect(result!.status).to.eq('failure') 59 | }) 60 | 61 | it('returns failure when resource is not an object', async () => { 62 | // given 63 | const step = new IdentifierStep({ 64 | value: 'http://foo/bar', 65 | }) 66 | const resource = 'foo bar' 67 | 68 | // when 69 | const execute = step.getRunner(resource as any) 70 | const { result } = await execute.call(context) 71 | 72 | // then 73 | expect(result!.status).to.eq('failure') 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /validator-e2e/lib/steps/representation/identifier/index.ts: -------------------------------------------------------------------------------- 1 | import { ScenarioStep } from '../../index' 2 | import { Resource } from 'alcaeus' 3 | import { E2eContext } from '../../../../types' 4 | import { checkChain, Result } from 'hydra-validator-core' 5 | import { NamedNode } from 'rdf-js' 6 | import { namedNode } from '@rdf-esm/data-model' 7 | 8 | interface IdentifierStepInit { 9 | value: string 10 | } 11 | 12 | export class IdentifierStep extends ScenarioStep { 13 | private readonly __identifier: NamedNode 14 | 15 | public constructor(init: IdentifierStepInit) { 16 | super([]) 17 | 18 | this.__identifier = namedNode(init.value) 19 | } 20 | 21 | protected appliesToInternal(): boolean { 22 | return true 23 | } 24 | 25 | public getRunner(obj: Resource): checkChain { 26 | const { __identifier } = this 27 | return function () { 28 | let result: Result 29 | 30 | if (typeof obj !== 'object') { 31 | result = Result.Failure(`Expected <${__identifier.value}> resource but the value is a ${typeof obj}`) 32 | } else if (__identifier.equals(obj.id)) { 33 | result = Result.Success(`Found expected resource identifier ${__identifier.value}`) 34 | } else { 35 | result = Result.Failure(`Expect resource <${__identifier.value}> but got <${obj.id.value}> instead.`) 36 | } 37 | 38 | return { 39 | result, 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /validator-e2e/lib/steps/representation/index.ts: -------------------------------------------------------------------------------- 1 | import { Resource } from 'alcaeus' 2 | import { E2eContext } from '../../../types' 3 | import { checkChain } from 'hydra-validator-core' 4 | import { ScenarioStep } from '../index' 5 | import { getResourceRunner } from '../../checkRunner' 6 | import { Constraint } from '../constraints/Constraint' 7 | 8 | interface ClassStepInit { 9 | classId: string 10 | } 11 | 12 | export class ClassStep extends ScenarioStep { 13 | public classId: string 14 | 15 | public constructor(init: ClassStepInit, children: ScenarioStep[], constraints: Constraint[]) { 16 | super(children, constraints) 17 | 18 | this.classId = init.classId 19 | } 20 | 21 | protected appliesToInternal(obj: Resource): boolean { 22 | return 'id' in obj && obj.types.has(this.classId) 23 | } 24 | 25 | public getRunner(resource: Resource): checkChain { 26 | const step = this 27 | 28 | return function checkRepresentation() { 29 | return { 30 | nextChecks: [getResourceRunner(resource, step)], 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /validator-e2e/lib/steps/representation/link/follow.spec.ts: -------------------------------------------------------------------------------- 1 | import * as checkRunner from '../../../checkRunner' 2 | import { E2eContext } from '../../../../types' 3 | import { FollowStep } from './follow' 4 | import { describe, it, beforeEach } from 'mocha' 5 | import { expect } from 'chai' 6 | import sinon from 'sinon' 7 | 8 | describe('follow', () => { 9 | let context: E2eContext & any 10 | let getUrlRunner: sinon.SinonStub 11 | beforeEach(() => { 12 | context = { 13 | scenarios: [], 14 | } 15 | sinon.reset() 16 | getUrlRunner = sinon.stub(checkRunner, 'getUrlRunner') 17 | }) 18 | 19 | describe('[variable]', () => { 20 | it('fetches it when value is a string', async () => { 21 | // given 22 | const step = new FollowStep({ 23 | variable: 'url', 24 | }, []) 25 | context.url = 'http://example.com/' 26 | 27 | // when 28 | const execute = step.getRunner(null, context) 29 | await execute.call(context) 30 | 31 | // then 32 | expect(getUrlRunner) 33 | .to.have.been.calledWith('http://example.com/', step) 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /validator-e2e/lib/steps/representation/link/follow.ts: -------------------------------------------------------------------------------- 1 | import { E2eContext } from '../../../../types' 2 | import { Context, checkChain, IResult, Result } from 'hydra-validator-core' 3 | import { getUrlRunner } from '../../../checkRunner' 4 | import { ScenarioStep } from '../../index' 5 | 6 | interface FollowStepInit { 7 | variable: string 8 | } 9 | 10 | export class FollowStep extends ScenarioStep { 11 | public variable: string 12 | 13 | public constructor(init: FollowStepInit, children: ScenarioStep[]) { 14 | super(children) 15 | 16 | this.variable = init.variable 17 | } 18 | 19 | public getRunner(obj: unknown, scope: Context) { 20 | const step = this 21 | return async function checkLink() { 22 | if (step.executed) { 23 | return {} 24 | } 25 | 26 | const resourceId: string | unknown = scope[step.variable] 27 | 28 | if (typeof resourceId !== 'string') { 29 | return { 30 | result: Result.Error(`Cannot fetch resource. Value of variable ${step.variable} must be a string`), 31 | } 32 | } 33 | 34 | const result: IResult = resourceId 35 | ? Result.Informational(`Fetching resource ${resourceId}`) 36 | : Result.Failure(`Variable ${step.variable} not found`) 37 | 38 | const nextChecks: checkChain[] = [] 39 | if (result.status !== 'failure') { 40 | nextChecks.push(getUrlRunner(resourceId, step)) 41 | 42 | step.markExecuted() 43 | } 44 | 45 | return { 46 | result, 47 | nextChecks, 48 | } 49 | } 50 | } 51 | 52 | protected appliesToInternal(): boolean { 53 | return true 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /validator-e2e/lib/steps/representation/link/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { namedNode } from '@rdf-esm/data-model' 2 | import { IriTemplate, Resource } from 'alcaeus' 3 | import * as checkRunner from '../../../checkRunner' 4 | import { LinkStep } from './' 5 | import { E2eContext } from '../../../../types' 6 | import { RecursivePartial } from '../../../testHelpers' 7 | import { StepSpy } from '../../stub' 8 | import { StatusStep } from '../../response/status' 9 | import { expect } from 'chai' 10 | import { describe, it, beforeEach } from 'mocha' 11 | import sinon from 'sinon' 12 | import { GraphPointer } from 'clownface' 13 | import { schema } from '@tpluscode/rdf-ns-builders' 14 | 15 | describe('link', () => { 16 | let context: E2eContext & any 17 | let getResponseRunner: sinon.SinonStub 18 | let getUrlRunner: sinon.SinonStub 19 | 20 | beforeEach(() => { 21 | context = { 22 | scenarios: [], 23 | } 24 | sinon.restore() 25 | getResponseRunner = sinon.stub(checkRunner, 'getResponseRunner') 26 | getUrlRunner = sinon.stub(checkRunner, 'getUrlRunner') 27 | }) 28 | 29 | it('applies to identified resource', () => { 30 | // given 31 | const step = new LinkStep({ 32 | rel: 'self', 33 | strict: false, 34 | }, [], []) 35 | 36 | // when 37 | const applies = step.appliesTo({ 38 | id: 'whatever', 39 | } as any) 40 | 41 | // then 42 | expect(applies).to.be.ok 43 | }) 44 | 45 | describe('statement', () => { 46 | it('when not strict and link not found, returns no result', async () => { 47 | // given 48 | const step = new LinkStep({ 49 | rel: 'self', 50 | strict: false, 51 | }, [], []) 52 | const resource = { 53 | getLinks: () => [], 54 | getArray: () => [], 55 | } 56 | 57 | // when 58 | const execute = step.getRunner(resource as any) 59 | const { result } = await execute.call(context) 60 | 61 | // then 62 | expect(result).to.be.undefined 63 | }) 64 | 65 | it('when strict and link not found, returns failure', async () => { 66 | // given 67 | const step = new LinkStep({ 68 | rel: 'self', 69 | strict: true, 70 | }, [], []) 71 | const resource = { 72 | id: namedNode('foo'), 73 | getLinks: () => [], 74 | getArray: () => [], 75 | } 76 | 77 | // when 78 | const execute = step.getRunner(resource as any) 79 | const { result } = await execute.call(context) 80 | 81 | // then 82 | expect(result!.status).to.eq('failure') 83 | }) 84 | 85 | it('when strict and link not found but exists on resource; returns warning, follows link', async () => { 86 | // given 87 | const step = new LinkStep({ 88 | rel: 'urn:not:link', 89 | strict: true, 90 | }, [], []) 91 | const linked = { id: 'urn:resource:linked' } 92 | const resource = { 93 | getLinks: () => [], 94 | getArray: sinon.stub().returns([linked]), 95 | 'urn:not:link': linked, 96 | } 97 | 98 | // when 99 | const execute = step.getRunner(resource as any) 100 | const { result } = await execute.call(context) 101 | 102 | // then 103 | expect(result!.status).to.eq('warning') 104 | expect(getResponseRunner).to.have.been.calledWith(linked, step, false) 105 | }) 106 | 107 | it('dereferences linked resources', async () => { 108 | // given 109 | const step = new LinkStep({ 110 | rel: 'urn:link:rel', 111 | strict: true, 112 | }, [], []) 113 | const resource: RecursivePartial = { 114 | getLinks: () => [ 115 | { 116 | supportedProperty: { 117 | id: namedNode('urn:link:rel'), 118 | }, 119 | resources: [ 120 | { id: 'urn:resource:one' }, 121 | { id: 'urn:resource:two' }, 122 | ], 123 | }, 124 | ], 125 | getArray: () => [], 126 | } 127 | 128 | // when 129 | const execute = step.getRunner(resource as any) 130 | const { nextChecks } = await execute.call(context) 131 | 132 | // then 133 | expect(nextChecks).to.have.length(2) 134 | expect(getResponseRunner).to.have.been.calledWith({ id: 'urn:resource:one' }, step, false) 135 | expect(getResponseRunner).to.have.been.calledWith({ id: 'urn:resource:two' }, step, false) 136 | }) 137 | }) 138 | 139 | describe('block', () => { 140 | it('dereferences populated template', async () => { 141 | // given 142 | const step = new LinkStep({ 143 | rel: 'urn:link:rel', 144 | strict: false, 145 | variables: [ 146 | { key: 'http://schema.org/tag', value: 'foo' }, 147 | { key: 'http://schema.org/tag', value: 'bar' }, 148 | { key: 'http://schema.org/title', value: 'baz' }, 149 | ], 150 | }, [new StepSpy()], []) 151 | const template: Partial = { 152 | expand: sinon.stub().returns('filled-in-template'), 153 | } 154 | const resource: RecursivePartial = { 155 | getLinks: () => [ 156 | { 157 | supportedProperty: { 158 | id: namedNode('urn:link:rel'), 159 | }, 160 | resources: [ 161 | template, 162 | ], 163 | }, 164 | ], 165 | getArray: () => [], 166 | } 167 | 168 | // when 169 | const execute = step.getRunner(resource as any) 170 | await execute.call(context) 171 | 172 | // then 173 | expect(template.expand).to.have.been.calledWithMatch((variables: GraphPointer) => { 174 | expect(variables.out(schema.tag).values).to.include.all.members(['foo', 'bar']) 175 | expect(variables.out(schema.title).value).to.eq('baz') 176 | return true 177 | }) 178 | expect(getUrlRunner).to.have.been.calledWith('filled-in-template', step, true) 179 | }) 180 | 181 | it('does not fail child step when they include a StatusStep', async () => { 182 | // given 183 | const step = new LinkStep({ 184 | rel: 'urn:link:rel', 185 | strict: false, 186 | variables: [ 187 | { key: 'http://schema.org/tag', value: 'foo' }, 188 | { key: 'http://schema.org/tag', value: 'bar' }, 189 | { key: 'http://schema.org/title', value: 'baz' }, 190 | ], 191 | }, [new StatusStep({ code: 400 })], []) 192 | const template: Partial = { 193 | expand: sinon.stub().returns('filled-in-template'), 194 | } 195 | const resource: RecursivePartial = { 196 | getLinks: () => [ 197 | { 198 | supportedProperty: { 199 | id: namedNode('urn:link:rel'), 200 | }, 201 | resources: [ 202 | template, 203 | ], 204 | }, 205 | ], 206 | getArray: () => [], 207 | } 208 | 209 | // when 210 | const execute = step.getRunner(resource as any) 211 | await execute.call(context) 212 | 213 | // then 214 | expect(template.expand).to.have.been.calledWithMatch((variables: GraphPointer) => { 215 | expect(variables.out(schema.tag).values).to.include.all.members(['foo', 'bar']) 216 | expect(variables.out(schema.title).value).to.eq('baz') 217 | return true 218 | }) 219 | expect(getUrlRunner).to.have.been.calledWith('filled-in-template', step, false) 220 | }) 221 | }) 222 | }) 223 | -------------------------------------------------------------------------------- /validator-e2e/lib/steps/representation/link/index.ts: -------------------------------------------------------------------------------- 1 | import { IriTemplate, Resource } from 'alcaeus' 2 | import { checkChain, Result } from 'hydra-validator-core' 3 | import { getResponseRunner, getUrlRunner } from '../../../checkRunner' 4 | import { ScenarioStep } from '../../index' 5 | import { Constraint } from '../../constraints/Constraint' 6 | import { E2eContext } from '../../../../types' 7 | import { namedNode } from '@rdf-esm/data-model' 8 | import { NamedNode } from 'rdf-js' 9 | import { StatusStep } from '../../response/status' 10 | import clownface from 'clownface' 11 | import $rdf from 'rdf-ext' 12 | 13 | interface TemplateVariable { 14 | key: string 15 | value: string 16 | } 17 | 18 | interface LinkStepInit { 19 | rel: string 20 | strict: boolean 21 | variables?: TemplateVariable[] 22 | } 23 | 24 | function reduceVariables(variables: Record, variable: TemplateVariable) { 25 | const value = variables[variable.key] 26 | 27 | if (!value) { 28 | return { 29 | ...variables, 30 | [variable.key]: variable.value, 31 | } 32 | } 33 | 34 | if (Array.isArray(value)) { 35 | return { 36 | ...variables, 37 | [variable.key]: [...value, variable.value], 38 | } 39 | } 40 | 41 | return { 42 | ...variables, 43 | ...variables, 44 | [variable.key]: [value, variable.value], 45 | } 46 | } 47 | 48 | export class LinkStep extends ScenarioStep { 49 | private relation: NamedNode 50 | private strict: boolean 51 | private variables: Record 52 | 53 | public constructor(init: LinkStepInit, children: ScenarioStep[], constraints: Constraint[]) { 54 | super(children, constraints) 55 | 56 | this.relation = namedNode(init.rel) 57 | this.strict = init.strict 58 | this.variables = init.variables?.reduce(reduceVariables, {}) || {} 59 | } 60 | 61 | protected appliesToInternal(obj: Resource): boolean { 62 | return 'id' in obj 63 | } 64 | 65 | public getRunner(resource: Resource): checkChain { 66 | const step = this 67 | 68 | return async function checkLink() { 69 | if (step.executed) { 70 | return {} 71 | } 72 | 73 | const linkValue = resource.getLinks() 74 | .find(link => link.supportedProperty.id.equals(step.relation)) 75 | 76 | // found supportedProperty which is a hydra:Link 77 | if (linkValue) { 78 | step.markExecuted() 79 | 80 | return { 81 | result: Result.Informational(`Stepping into link ${step.relation.value}`), 82 | nextChecks: linkValue.resources.map(resource => step.__dereferenceLinkedResource(resource as any, step)), 83 | } 84 | } 85 | 86 | // the resource may have a matching key, but not a supportedProperty hydra:Links 87 | const potentialLinks = resource.getArray(step.relation) 88 | 89 | if (potentialLinks.length > 0) { 90 | step.markExecuted() 91 | 92 | return { 93 | result: Result.Warning( 94 | `Stepping into link ${step.relation.value}`, 95 | `Resources found but ${step.relation.value} is not a SupportedProperty of hydra:Link type.`), 96 | nextChecks: potentialLinks.map(resource => step.__dereferenceLinkedResource(resource, step)), 97 | } 98 | } 99 | 100 | if (step.strict) { 101 | return { 102 | result: Result.Failure(`No resources found on resource ${resource.id.value} linked with ${step.relation.value}`), 103 | } 104 | } 105 | 106 | return {} 107 | } 108 | } 109 | 110 | private __dereferenceLinkedResource(resource: Resource | IriTemplate, step: this) { 111 | const failOnNegativeResponse = step.children.length > 0 && !step.children.some(child => child instanceof StatusStep) 112 | 113 | if ('expand' in resource) { 114 | const variables = [...Object.entries(step.variables)].reduce((pointer, [predicate, objects]) => { 115 | return pointer.addOut($rdf.namedNode(predicate), objects) 116 | }, clownface({ dataset: $rdf.dataset() }).blankNode()) 117 | return getUrlRunner(resource.expand(variables), step, failOnNegativeResponse) 118 | } 119 | 120 | return getResponseRunner(resource, step, failOnNegativeResponse) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /validator-e2e/lib/steps/representation/operation/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, beforeEach } from 'mocha' 2 | import { expect } from 'chai' 3 | import { blankNode, namedNode } from '@rdf-esm/data-model' 4 | import { E2eContext } from '../../../../types' 5 | import { OperationStep } from './index' 6 | import { StepStub } from '../../stub' 7 | import { runAll } from '../../../testHelpers' 8 | 9 | describe('Operation block', () => { 10 | let context: E2eContext & any 11 | beforeEach(() => { 12 | context = { 13 | scenarios: [], 14 | } 15 | }) 16 | 17 | it('when strict and operation is not found returns failure', async () => { 18 | // given 19 | const operationStep = new OperationStep({ 20 | strict: true, 21 | operationId: 'not-found', 22 | }, []) 23 | const resource = { 24 | operations: [], 25 | } 26 | 27 | // when 28 | const execute = operationStep.getRunner(resource as any) 29 | const result = await execute.call(context) 30 | 31 | // then 32 | expect(result.result!.status).to.eq('failure') 33 | }) 34 | 35 | it('when not strict and operation is not found returns no result', async () => { 36 | // given 37 | const operationStep = new OperationStep({ 38 | strict: false, 39 | operationId: 'not-found', 40 | }, []) 41 | const resource = { 42 | operations: [], 43 | } 44 | 45 | // when 46 | const execute = operationStep.getRunner(resource as any) 47 | const result = await execute.call(context) 48 | 49 | // then 50 | expect(result.result).to.be.undefined 51 | }) 52 | 53 | it('returns informational when operation is found matching SupportedOperation id', async () => { 54 | // given 55 | const operationStep = new OperationStep({ 56 | strict: true, 57 | operationId: 'the-operation', 58 | }, []) 59 | const resource: any = { 60 | operations: [ 61 | { 62 | id: namedNode('the-operation'), 63 | }, 64 | ], 65 | } 66 | 67 | // when 68 | const execute = operationStep.getRunner(resource) 69 | const result = await execute.call(context) 70 | 71 | // then 72 | expect(result.result!.status).to.eq('informational') 73 | }) 74 | 75 | it('returns informational when operation is found matching Operation type', async () => { 76 | // given 77 | const operationStep = new OperationStep({ 78 | strict: true, 79 | operationId: 'the-operation', 80 | }, []) 81 | const resource: any = { 82 | operations: [ 83 | { 84 | id: blankNode(), 85 | types: { 86 | has: () => true, 87 | }, 88 | }, 89 | ], 90 | } 91 | 92 | // when 93 | const execute = operationStep.getRunner(resource) 94 | const result = await execute.call(context) 95 | 96 | // then 97 | expect(result.result!.status).to.eq('informational') 98 | }) 99 | 100 | it('enqueues top-level steps', async () => { 101 | // given 102 | const operation = {} 103 | const operationStep = new OperationStep({ 104 | strict: true, 105 | operationId: 'the-operation', 106 | }, []) 107 | const resource = { 108 | operations: { 109 | find: () => operation, 110 | }, 111 | } 112 | context.scenarios.push(new StepStub('topLevel')) 113 | 114 | // when 115 | const execute = operationStep.getRunner(resource as any) 116 | const result = await runAll(execute, context) 117 | 118 | // then 119 | expect(result.checkNames).to.include('topLevel') 120 | }) 121 | 122 | it('enqueues child steps before top-level steps', async () => { 123 | // given 124 | const operation = {} 125 | const operationStep = new OperationStep({ 126 | strict: true, 127 | operationId: 'the-operation', 128 | }, [ 129 | new StepStub('child'), 130 | ]) 131 | const resource = { 132 | operations: { 133 | find: () => operation, 134 | }, 135 | } 136 | context.scenarios.push(new StepStub('topLevel')) 137 | 138 | // when 139 | const execute = operationStep.getRunner(resource as any) 140 | const result = await runAll(execute, context) 141 | 142 | // then 143 | expect(result.checkNames).to.include('child') 144 | expect(result.checkNames).to.include('topLevel') 145 | expect(result.checkNames.indexOf('child')).to.be.lessThan(result.checkNames.indexOf('topLevel')) 146 | }) 147 | }) 148 | -------------------------------------------------------------------------------- /validator-e2e/lib/steps/representation/operation/index.ts: -------------------------------------------------------------------------------- 1 | import { E2eContext } from '../../../../types' 2 | import { checkChain, Result } from 'hydra-validator-core' 3 | import { ScenarioStep } from '../../index' 4 | import { getResourceRunner } from '../../../checkRunner' 5 | import { Resource, ResourceIdentifier } from 'alcaeus' 6 | import { namedNode } from '@rdf-esm/data-model' 7 | 8 | interface OperationStepInit { 9 | operationId: string 10 | strict: boolean 11 | } 12 | 13 | export class OperationStep extends ScenarioStep { 14 | public operationId: ResourceIdentifier; 15 | public strict: boolean; 16 | 17 | public constructor(init: OperationStepInit, children: ScenarioStep[]) { 18 | super(children) 19 | this.operationId = namedNode(init.operationId) 20 | this.strict = init.strict 21 | } 22 | 23 | protected appliesToInternal(obj: Resource): boolean { 24 | return 'operations' in obj 25 | } 26 | 27 | public getRunner(resource: Resource): checkChain { 28 | const step = this 29 | return async function invokeOperation(this: E2eContext) { 30 | const operation = resource.operations.find(op => op.id.equals(step.operationId) || op.types.has(step.operationId)) 31 | if (!operation) { 32 | const message = `Operation ${step.operationId.value} not found` 33 | if (step.strict) { 34 | return { 35 | result: Result.Failure(message), 36 | } 37 | } 38 | 39 | // TODO: add a 'debug' result for diagnostic purposes 40 | return {} 41 | } 42 | 43 | if (step.executed) { 44 | return {} 45 | } 46 | 47 | step.markExecuted() 48 | 49 | return { 50 | result: Result.Informational(`Found operation '${operation.title}'`), 51 | nextChecks: [getResourceRunner(operation, step)], 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /validator-e2e/lib/steps/representation/operation/invocation.spec.ts: -------------------------------------------------------------------------------- 1 | import { E2eContext } from '../../../../types' 2 | import { InvocationStep } from './invocation' 3 | import { StepStub } from '../../stub' 4 | import * as checkRunner from '../../../checkRunner' 5 | import 'isomorphic-fetch' 6 | import { describe, it, beforeEach } from 'mocha' 7 | import { expect } from 'chai' 8 | import sinon from 'sinon' 9 | 10 | describe('Invoke block', () => { 11 | let context: E2eContext 12 | let readFileSync: sinon.SinonStub 13 | let getResponseRunner: sinon.SinonStub 14 | 15 | beforeEach(() => { 16 | context = { 17 | scenarios: [], 18 | basePath: '/some/path/with/tests', 19 | } 20 | 21 | sinon.restore() 22 | readFileSync = sinon.stub() 23 | getResponseRunner = sinon.stub(checkRunner, 'getResponseRunner') 24 | }) 25 | 26 | it('invokes operation with provided headers', async () => { 27 | // given 28 | const step = new InvocationStep({ 29 | body: 'Test', 30 | headers: { 31 | 'Content-Type': 'text/csv', 32 | accept: 'application/rdf+xml', 33 | }, 34 | }, []) 35 | const operation = { 36 | invoke: sinon.stub(), 37 | } 38 | 39 | // when 40 | const execute = step.getRunner(operation as any) 41 | await execute.call(context) 42 | 43 | // then 44 | expect(operation.invoke).to.have.been.calledWith( 45 | 'Test', new Headers({ 46 | 'content-type': 'text/csv', 47 | accept: 'application/rdf+xml', 48 | })) 49 | }) 50 | 51 | it('invokes operation with default headers', async () => { 52 | // given 53 | const step = new InvocationStep({ 54 | body: 'Test', 55 | }, []) 56 | const operation = { 57 | invoke: sinon.stub(), 58 | } 59 | context.headers = new Headers({ 60 | 'Content-Type': 'text/csv', 61 | accept: 'application/rdf+xml', 62 | }) 63 | 64 | // when 65 | const execute = step.getRunner(operation as any) 66 | await execute.call(context) 67 | 68 | // then 69 | expect(operation.invoke).to.have.been.calledWith( 70 | 'Test', new Headers({ 71 | 'content-type': 'text/csv', 72 | accept: 'application/rdf+xml', 73 | })) 74 | }) 75 | 76 | it('invokes operation with default headers merged with invocation headers', async () => { 77 | // given 78 | const step = new InvocationStep({ 79 | body: 'Test', 80 | headers: { 81 | 'User-Agent': 'curl', 82 | 'Content-Type': 'text/csv', 83 | }, 84 | }, []) 85 | const operation = { 86 | invoke: sinon.stub(), 87 | } 88 | context.headers = new Headers({ 89 | 'Content-Type': 'application/ld+json', 90 | accept: 'application/rdf+xml', 91 | }) 92 | 93 | // when 94 | const execute = step.getRunner(operation as any) 95 | await execute.call(context) 96 | 97 | // then 98 | expect(operation.invoke).to.have.been.calledWith( 99 | 'Test', new Headers({ 100 | 'user-agent': 'curl', 101 | 'content-type': 'text/csv', 102 | accept: 'application/rdf+xml', 103 | })) 104 | }) 105 | 106 | it('resolves relative path for referenced body', async () => { 107 | // given 108 | const step = new InvocationStep({ 109 | body: { 110 | path: '../../body/data.csv', 111 | }, 112 | headers: { 113 | 'Content-Type': 'text/csv', 114 | }, 115 | fs: { readFileSync }, 116 | }, []) 117 | const operation = { 118 | invoke: sinon.stub(), 119 | } 120 | 121 | // when 122 | const execute = step.getRunner(operation as any) 123 | await execute.call(context) 124 | 125 | // then 126 | expect(readFileSync).to.have.been.calledWith('/some/path/body/data.csv') 127 | }) 128 | 129 | it('returns error when body file fails to load', async () => { 130 | // given 131 | const step = new InvocationStep({ 132 | body: { 133 | path: '../../body/data.csv', 134 | }, 135 | headers: { 136 | 'Content-Type': 'text/csv', 137 | }, 138 | }, []) 139 | const operation = { 140 | invoke: sinon.stub(), 141 | } 142 | readFileSync.throws(() => { 143 | throw new Error('Fail to open file') 144 | }) 145 | 146 | // when 147 | const execute = step.getRunner(operation as any) 148 | const { result } = await execute.call(context) 149 | 150 | // then 151 | expect(result!.status).to.eq('error') 152 | }) 153 | 154 | it('enqueues child steps and top level steps', async () => { 155 | // given 156 | const step = new InvocationStep({ 157 | body: 'Test', 158 | headers: { 159 | 'Content-Type': 'text/csv', 160 | }, 161 | }, [ 162 | new StepStub('child'), 163 | ]) 164 | const operation = { 165 | invoke: sinon.stub(), 166 | } 167 | const response = {} 168 | operation.invoke.returns(response) 169 | 170 | // when 171 | const execute = step.getRunner(operation as any) 172 | await execute.call(context) 173 | 174 | // then 175 | expect(getResponseRunner).to.have.been.calledWith(response, step) 176 | }) 177 | 178 | it('invokes operation with empty body when not given', async () => { 179 | // given 180 | const step = new InvocationStep({}, []) 181 | const operation = { 182 | invoke: sinon.stub(), 183 | } 184 | 185 | // when 186 | const execute = step.getRunner(operation as any) 187 | await execute.call(context) 188 | 189 | // then 190 | expect(operation.invoke).to.have.been.calledWith('', new Headers({})) 191 | }) 192 | }) 193 | -------------------------------------------------------------------------------- /validator-e2e/lib/steps/representation/operation/invocation.ts: -------------------------------------------------------------------------------- 1 | import { RuntimeOperation } from 'alcaeus' 2 | import { E2eContext } from '../../../../types' 3 | import { checkChain, Result } from 'hydra-validator-core' 4 | import { getResponseRunner } from '../../../checkRunner' 5 | import { ScenarioStep } from '../../index' 6 | import * as fs from 'fs' 7 | import { resolve } from 'path' 8 | import 'isomorphic-fetch' 9 | 10 | interface InvocationStepInit { 11 | body?: string | { path: string } 12 | headers?: { [key: string]: string } 13 | fs?: Pick 14 | } 15 | 16 | export class InvocationStep extends ScenarioStep { 17 | private body?: (basePath: string) => BodyInit; 18 | private headers: Map; 19 | 20 | public constructor({ body, headers, fs: { readFileSync } = fs }: InvocationStepInit, children: ScenarioStep[]) { 21 | super(children) 22 | 23 | this.headers = Object.entries((headers || {})) 24 | .reduce((headers, kv) => { 25 | headers.set(kv[0].toLowerCase(), kv[1]) 26 | return headers 27 | }, new Map()) 28 | 29 | if (typeof body === 'string') { 30 | this.body = () => body 31 | } else if (body && body.path) { 32 | const path = body.path 33 | this.body = (basePath) => readFileSync(resolve(basePath, path)) 34 | } 35 | } 36 | 37 | public getRunner(operation: RuntimeOperation): checkChain { 38 | const step = this 39 | return async function (this: E2eContext) { 40 | if (step.executed) { 41 | return {} 42 | } 43 | 44 | let body: BodyInit = '' 45 | if (step.body) { 46 | try { 47 | body = await step.body(this.basePath) 48 | } catch (e) { 49 | return { 50 | result: Result.Error('Failed to load body', e), 51 | } 52 | } 53 | } 54 | const headers = [...step.headers.entries()].reduce((headers, [header, value]) => { 55 | headers.set(header, value) 56 | return headers 57 | }, new Headers()) 58 | 59 | if (this.headers) { 60 | this.headers.forEach((value, name) => { 61 | if (!headers.has(name)) { 62 | headers.append(name, value) 63 | } 64 | }) 65 | } 66 | 67 | const response = await operation.invoke(body, headers) 68 | 69 | step.markExecuted() 70 | 71 | return { 72 | result: Result.Informational(`Invoked operation '${operation.title}'`), 73 | nextChecks: [getResponseRunner(response, step)], 74 | } 75 | } 76 | } 77 | 78 | protected appliesToInternal(): boolean { 79 | return true 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /validator-e2e/lib/steps/representation/property/index.ts: -------------------------------------------------------------------------------- 1 | import { Literal, NamedNode } from 'rdf-js' 2 | import { namedNode } from '@rdf-esm/data-model' 3 | import { Resource, ResourceIndexer } from 'alcaeus' 4 | import { E2eContext } from '../../../../types' 5 | import { checkChain, CheckResult, Result, IResult } from 'hydra-validator-core' 6 | import { ScenarioStep } from '../../index' 7 | import { expand } from '@zazuko/rdf-vocabularies' 8 | import areEqual from '../../../comparison' 9 | import { getResourceRunner } from '../../../checkRunner' 10 | import { Constraint } from '../../constraints/Constraint' 11 | 12 | interface PropertyStepInit { 13 | propertyId: string 14 | value?: unknown 15 | strict: boolean 16 | } 17 | 18 | async function flattenResults(root: CheckResult, context: E2eContext): Promise { 19 | let results: IResult[] = [] 20 | if (root.results) { 21 | results = root.results 22 | } else if (root.result) { 23 | results = [root.result] 24 | } 25 | 26 | if (!root.nextChecks || root.nextChecks.length === 0) { 27 | return results 28 | } 29 | 30 | const remainingChecks = [...root.nextChecks] 31 | while (remainingChecks.length > 0) { 32 | const child = remainingChecks.splice(0, 1)[0] 33 | const childResult = await child.call(context) 34 | 35 | results = [...results, ...await flattenResults(childResult, context)] 36 | } 37 | 38 | return results 39 | } 40 | 41 | export class PropertyStep extends ScenarioStep { 42 | public propertyId: NamedNode 43 | public strict: boolean 44 | public expectedValue?: unknown 45 | 46 | public constructor(init: PropertyStepInit, children: ScenarioStep[], constraints: Constraint[]) { 47 | super(children, constraints) 48 | 49 | this.propertyId = namedNode(init.propertyId) 50 | this.strict = init.strict 51 | this.expectedValue = init.value 52 | } 53 | 54 | protected appliesToInternal(obj: Resource): boolean { 55 | return typeof obj === 'object' && 'id' in obj 56 | } 57 | 58 | public getRunner(resource: Resource & ResourceIndexer): checkChain { 59 | const step = this 60 | 61 | if (step.children.length > 0 && !!step.expectedValue) { 62 | return function (): CheckResult { 63 | return { 64 | result: Result.Error("Property step cannot mix both 'value' and child steps"), 65 | } 66 | } 67 | } 68 | 69 | if (step.constraints.length > 0 && !!step.expectedValue) { 70 | return function (): CheckResult { 71 | return { 72 | result: Result.Error('Property statement cannot have constraints'), 73 | } 74 | } 75 | } 76 | 77 | return function () { 78 | if (step.propertyId.value === expand('rdf:type')) { 79 | return step.__executeRdfTypeStatement(resource) 80 | } 81 | 82 | const value = resource[step.propertyId.value] 83 | if (!value) { 84 | return step.__getMissingPropertyResult(resource) 85 | } 86 | 87 | return step.__checkValues(value as any, this) 88 | } 89 | } 90 | 91 | private __checkValues(value: (Resource | Literal) | (Resource | Literal)[], context: E2eContext, arrayItem = false): Promise> | CheckResult { 92 | if (Array.isArray(value)) { 93 | if (value.length === 0) { 94 | return { 95 | result: Result.Warning(`Found empty array for property ${this.propertyId.value}`), 96 | } 97 | } 98 | 99 | return this.__executeArray(value, context) 100 | } 101 | 102 | if (typeof this.expectedValue !== 'undefined' && this.expectedValue !== null) { 103 | return this.__executeStatement(value) 104 | } 105 | 106 | if (!this.children || this.children.length === 0) { 107 | return { 108 | result: Result.Success(`Found expected property ${this.propertyId.value}`), 109 | } 110 | } 111 | 112 | return this.__executeBlock(value as Resource, arrayItem) 113 | } 114 | 115 | private __executeRdfTypeStatement(resource: Resource) { 116 | if (!this.strict) { 117 | return { result: Result.Error('Expect Type statement must be strict') } 118 | } 119 | 120 | let result 121 | const hasType = resource.types.has(this.expectedValue as string) 122 | if (hasType) { 123 | result = Result.Success(`Found type ${this.expectedValue}`) 124 | } else { 125 | result = Result.Failure(`Resource ${resource.id.value} does not have expected RDF type ${this.expectedValue}`) 126 | } 127 | 128 | return { result } 129 | } 130 | 131 | private __getMissingPropertyResult(resource: Resource) { 132 | let result 133 | if (this.strict) { 134 | result = Result.Failure(`Property ${this.propertyId.value} missing on resource ${resource.id.value}`) 135 | } else { 136 | result = Result.Informational(`Skipping missing property ${this.propertyId.value}`) 137 | } 138 | 139 | return { result } 140 | } 141 | 142 | private __executeStatement(value: Resource | Literal): CheckResult { 143 | if ('id' in value) { 144 | return { 145 | result: Result.Failure(`Expected ${this.propertyId.value} to be literal but found resource ${value.id.value}`), 146 | } 147 | } 148 | 149 | if (areEqual(this.expectedValue, value)) { 150 | return { 151 | result: Result.Success(`Found ${this.propertyId.value} property with expected value`), 152 | } 153 | } 154 | 155 | return { 156 | result: Result.Failure(`Expected ${this.propertyId.value} to equal ${this.expectedValue} but found ${value.value}`), 157 | } 158 | } 159 | 160 | private __executeBlock(value: Resource, arrayItem: boolean): CheckResult { 161 | const result = Result.Informational(`Stepping into property ${this.propertyId.value}`) 162 | const nextChecks = [getResourceRunner(value, this)] 163 | 164 | if (arrayItem) { 165 | return { nextChecks } 166 | } 167 | 168 | return { result, nextChecks } 169 | } 170 | 171 | private async __executeArray(array: (Literal | Resource)[], context: E2eContext) { 172 | let anyFailed = false 173 | 174 | for (const value of array) { 175 | const childResult = await this.__checkValues(value, context, true) 176 | const allResults = await flattenResults(childResult, context) 177 | 178 | const noSuccessResults = allResults.every(r => r.status !== 'success') 179 | const someFailed = allResults.some(r => r.status === 'failure' || r.status === 'error') 180 | 181 | if (someFailed || noSuccessResults) { 182 | anyFailed = anyFailed || someFailed 183 | continue 184 | } 185 | 186 | return { 187 | result: Result.Success(`Found ${this.propertyId.value} property matching expected criteria`), 188 | } 189 | } 190 | 191 | if (!anyFailed) { 192 | return { 193 | result: Result.Informational(`No object of ${this.propertyId.value} property but no steps failed. Have all object been excluded by constraints?`), 194 | } 195 | } 196 | 197 | return { 198 | result: Result.Failure(`No object of ${this.propertyId.value} property was found matching the criteria`), 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /validator-e2e/lib/steps/response/header.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, beforeEach } from 'mocha' 2 | import { expect } from 'chai' 3 | import { HeaderStep } from './header' 4 | import { E2eContext } from '../../../types' 5 | import 'isomorphic-fetch' 6 | 7 | describe('header statement', () => { 8 | let context: E2eContext & any 9 | 10 | beforeEach(() => { 11 | context = { 12 | scenarios: [], 13 | } 14 | }) 15 | 16 | it('returns success when no value is given and header is present', async () => { 17 | // given 18 | const step = new HeaderStep({ 19 | header: 'Location', 20 | }) 21 | const response = new Response(null, { 22 | headers: { 23 | Location: 'http://example.com', 24 | }, 25 | }) 26 | 27 | // when 28 | const execute = step.getRunner(response, context) 29 | const result = execute.call(context) 30 | 31 | // then 32 | expect(result.result!.status).to.eq('success') 33 | }) 34 | 35 | it('returns failure when header is present but value does not match', async () => { 36 | // given 37 | const step = new HeaderStep({ 38 | header: 'Location', 39 | value: 'http://example.org', 40 | }) 41 | const response = new Response(null, { 42 | headers: { 43 | Location: 'http://example.com', 44 | }, 45 | }) 46 | 47 | // when 48 | const execute = step.getRunner(response, context) 49 | const result = execute.call(context) 50 | 51 | // then 52 | expect(result.result!.status).to.eq('failure') 53 | }) 54 | 55 | it('returns success when header value matches expected', async () => { 56 | // given 57 | const step = new HeaderStep({ 58 | header: 'Location', 59 | value: 'http://example.org', 60 | }) 61 | const response = new Response(null, { 62 | headers: { 63 | Location: 'http://example.org', 64 | }, 65 | }) 66 | 67 | // when 68 | const execute = step.getRunner(response, context) 69 | const result = execute.call(context) 70 | 71 | // then 72 | expect(result.result!.status).to.eq('success') 73 | }) 74 | 75 | it('returns success when header value matches pattern', async () => { 76 | // given 77 | const step = new HeaderStep({ 78 | header: 'Location', 79 | pattern: '^http:\\/\\/example\\.', 80 | }) 81 | const response = new Response(null, { 82 | headers: { 83 | Location: 'http://example.org', 84 | }, 85 | }) 86 | 87 | // when 88 | const execute = step.getRunner(response, context) 89 | const result = execute.call(context) 90 | 91 | // then 92 | expect(result.result!.status).to.eq('success') 93 | }) 94 | 95 | it('returns failure when header value does not matches pattern', async () => { 96 | // given 97 | const step = new HeaderStep({ 98 | header: 'Location', 99 | pattern: 'com$', 100 | }) 101 | const response = new Response(null, { 102 | headers: { 103 | Location: 'http://example.org', 104 | }, 105 | }) 106 | 107 | // when 108 | const execute = step.getRunner(response, context) 109 | const result = execute.call(context) 110 | 111 | // then 112 | expect(result.result!.status).to.eq('failure') 113 | }) 114 | 115 | it('returns error when pattern is not a well formed regex', async () => { 116 | // given 117 | const step = new HeaderStep({ 118 | header: 'Location', 119 | pattern: '(unclosed', 120 | }) 121 | const response = new Response(null, { 122 | headers: { 123 | Location: 'http://example.org', 124 | }, 125 | }) 126 | 127 | // when 128 | const execute = step.getRunner(response, context) 129 | const result = execute.call(context) 130 | 131 | // then 132 | expect(result.result!.status).to.eq('error') 133 | }) 134 | 135 | it('returns failure when header is not found', async () => { 136 | // given 137 | const step = new HeaderStep({ 138 | header: 'Location', 139 | }) 140 | const response = new Response(null, { }) 141 | 142 | // when 143 | const execute = step.getRunner(response, context) 144 | const result = execute.call(context) 145 | 146 | // then 147 | expect(result.result!.status).to.eq('failure') 148 | }) 149 | 150 | it('sets to context when `captureAs` is used', () => { 151 | // given 152 | const step = new HeaderStep({ 153 | header: 'Location', 154 | captureAs: 'url', 155 | }) 156 | const response = new Response(null, { 157 | headers: { 158 | Location: 'http://example.com', 159 | }, 160 | }) 161 | 162 | // when 163 | const execute = step.getRunner(response, context) 164 | execute.call(context) 165 | 166 | // then 167 | expect(context.url).to.eq('http://example.com') 168 | }) 169 | }) 170 | -------------------------------------------------------------------------------- /validator-e2e/lib/steps/response/header.ts: -------------------------------------------------------------------------------- 1 | import { IResult, Result, Context } from 'hydra-validator-core' 2 | import { ResponseStep } from '../index' 3 | import escapeStringRegexp from 'escape-string-regexp' 4 | 5 | interface ExpectationStepInit { 6 | header: string 7 | captureAs?: string 8 | value?: string 9 | pattern?: string 10 | } 11 | 12 | export class HeaderStep extends ResponseStep { 13 | public header: string; 14 | public captureAs: string | null; 15 | public pattern: string | null; 16 | 17 | public constructor(step: ExpectationStepInit) { 18 | super([]) 19 | 20 | this.header = step.header 21 | this.captureAs = step.captureAs || null 22 | this.pattern = step.pattern || null 23 | if (step.value) { 24 | this.pattern = `^${escapeStringRegexp(step.value)}$` 25 | } 26 | } 27 | 28 | public getRunner(response: Response, scope: Context) { 29 | const expectation = this 30 | let regex: RegExp 31 | 32 | try { 33 | if (expectation.pattern) { 34 | regex = new RegExp(expectation.pattern) 35 | } 36 | } catch (e) { 37 | return () => ({ result: Result.Error('Regular expression is not valid', e) }) 38 | } 39 | 40 | return function () { 41 | let result: IResult 42 | 43 | if (!response.headers.has(expectation.header)) { 44 | return { 45 | result: Result.Failure(`Expected to find response header ${expectation.header}`), 46 | } 47 | } 48 | result = Result.Success(`Found '${expectation.header}' header`) 49 | 50 | if (expectation.captureAs) { 51 | scope[expectation.captureAs] = response.headers.get(expectation.header) 52 | } 53 | 54 | if (regex) { 55 | const value = response.headers.get(expectation.header) 56 | if (!value || !regex.test(value)) { 57 | result = Result.Failure(`Expected header ${expectation.header} to match ${regex} but found '${response.headers.get(expectation.header)}'`) 58 | } 59 | } 60 | 61 | return { result } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /validator-e2e/lib/steps/response/status.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, beforeEach } from 'mocha' 2 | import { expect } from 'chai' 3 | import { StatusStep } from './status' 4 | import { E2eContext } from '../../../types' 5 | import 'isomorphic-fetch' 6 | 7 | describe('status statement', () => { 8 | let context: E2eContext & any 9 | 10 | beforeEach(() => { 11 | context = { 12 | scenarios: [], 13 | } 14 | }) 15 | 16 | it('returns success when status code matches that expected', async () => { 17 | // given 18 | const step = new StatusStep({ 19 | code: 204, 20 | }) 21 | const response = new Response(null, { 22 | status: 204, 23 | }) 24 | 25 | // when 26 | const execute = step.getRunner(response) 27 | const result = execute.call(context) 28 | 29 | // then 30 | expect(result.result!.status).to.eq('success') 31 | }) 32 | 33 | it('returns failure when status code does not match', async () => { 34 | // given 35 | const step = new StatusStep({ 36 | code: 404, 37 | }) 38 | const response = new Response(null, { 39 | status: 204, 40 | }) 41 | 42 | // when 43 | const execute = step.getRunner(response) 44 | const result = execute.call(context) 45 | 46 | // then 47 | expect(result.result!.status).to.eq('failure') 48 | }) 49 | 50 | it('returns error when status code is not given', async () => { 51 | // given 52 | const step = new StatusStep({ 53 | code: null, 54 | } as any) 55 | const response = new Response(null, { 56 | status: 204, 57 | }) 58 | 59 | // when 60 | const execute = step.getRunner(response) 61 | const result = execute.call(context) 62 | 63 | // then 64 | expect(result.result!.status).to.eq('error') 65 | }) 66 | 67 | it('returns error when status code is not a number', async () => { 68 | // given 69 | const step = new StatusStep({ 70 | code: 'xyz', 71 | } as any) 72 | const response = new Response(null, { 73 | status: 204, 74 | }) 75 | 76 | // when 77 | const execute = step.getRunner(response) 78 | const result = execute.call(context) 79 | 80 | // then 81 | expect(result.result!.status).to.eq('error') 82 | }) 83 | 84 | it('returns error when status code is not a valid number', async () => { 85 | // given 86 | const step = new StatusStep({ 87 | code: '699', 88 | } as any) 89 | const response = new Response(null, { 90 | status: 204, 91 | }) 92 | 93 | // when 94 | const execute = step.getRunner(response) 95 | const result = execute.call(context) 96 | 97 | // then 98 | expect(result.result!.status).to.eq('error') 99 | }) 100 | }) 101 | -------------------------------------------------------------------------------- /validator-e2e/lib/steps/response/status.ts: -------------------------------------------------------------------------------- 1 | import { Result } from 'hydra-validator-core' 2 | import { ResponseStep } from '../index' 3 | 4 | interface ExpectationStepInit { 5 | code: number 6 | } 7 | 8 | export class StatusStep extends ResponseStep { 9 | public code: number; 10 | 11 | public constructor(step: ExpectationStepInit) { 12 | super([]) 13 | 14 | this.code = step.code 15 | } 16 | 17 | public getRunner(response: Response) { 18 | const expectation = this 19 | if (typeof expectation.code !== 'number' || 20 | expectation.code < 100 || 21 | expectation.code >= 600) { 22 | return () => ({ 23 | result: Result.Error( 24 | 'Step parameter is not a valid status code', 25 | 'Status code must be an integer between 100 and 599'), 26 | }) 27 | } 28 | 29 | return function () { 30 | const result = response.status === expectation.code 31 | ? Result.Success(`Status code '${expectation.code}'`) 32 | : Result.Failure(`Expected status code ${expectation.code} but got ${response.status}`) 33 | 34 | return { result } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /validator-e2e/lib/steps/stub.ts: -------------------------------------------------------------------------------- 1 | import { ScenarioStep } from './index' 2 | import { checkChain } from 'hydra-validator-core' 3 | import { Constraint } from './constraints/Constraint' 4 | // eslint-disable-next-line import/no-extraneous-dependencies 5 | import sinon from 'sinon' 6 | 7 | export class StepStub extends ScenarioStep { 8 | private readonly runner: checkChain 9 | private readonly executions: string[] 10 | private readonly name: string 11 | public visited = false 12 | 13 | public constructor(name: string, executions: string[] = []) { 14 | super([]) 15 | 16 | this.name = name 17 | this.executions = executions 18 | // eslint-disable-next-line no-new-func 19 | this.runner = new Function( 20 | `return function ${name}() { return {} }`, 21 | )() 22 | } 23 | 24 | protected appliesToInternal(): boolean { 25 | return true 26 | } 27 | 28 | public getRunner() { 29 | this.executions.push(this.name) 30 | return this.runner 31 | } 32 | } 33 | 34 | export class StepSpy extends ScenarioStep { 35 | public readonly runner: sinon.SinonStub 36 | public readonly realAppliesTo: sinon.SinonStub 37 | 38 | public constructor() { 39 | super([]) 40 | this.realAppliesTo = sinon.stub().returns(true) 41 | this.runner = sinon.stub().returns(() => ({ })) 42 | } 43 | 44 | public appliesTo(obj: unknown): boolean { 45 | return this.realAppliesTo(obj) 46 | } 47 | 48 | public getRunner() { 49 | return this.runner 50 | } 51 | 52 | protected appliesToInternal(): boolean { 53 | throw new Error('Not implemented') 54 | } 55 | } 56 | 57 | export class ConstraintMock extends Constraint { 58 | public type: 'Representation' | 'Response' | null 59 | 60 | public constructor(mockResult = true, type?: 'Representation' | 'Response') { 61 | super(() => mockResult, false) 62 | 63 | this.type = type || null 64 | } 65 | 66 | protected getValue(subject: unknown): unknown { 67 | return undefined 68 | } 69 | 70 | protected sanityCheckValue(value: unknown): boolean { 71 | return true 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /validator-e2e/lib/strictRunVerification.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, beforeEach } from 'mocha' 2 | import { expect } from 'chai' 3 | import { E2eContext } from '../types' 4 | import { verifyTopLevelBlocksExecuted } from './strictRunVerification' 5 | import { StepStub } from './steps/stub' 6 | 7 | function createVisitedStub(name: string) { 8 | const stub = new StepStub(name) 9 | stub.visited = true 10 | return stub 11 | } 12 | 13 | describe('strictRunVerification', () => { 14 | let context: E2eContext 15 | beforeEach(() => { 16 | context = { 17 | basePath: '', 18 | scenarios: [], 19 | } 20 | }) 21 | 22 | describe('strict', () => { 23 | it('returns informational when all steps are visited', async () => { 24 | // given 25 | const steps = [ 26 | createVisitedStub('foo'), 27 | createVisitedStub('bar'), 28 | ] 29 | const runner = verifyTopLevelBlocksExecuted(true, steps) 30 | 31 | // when 32 | const { result } = await runner.call(context) 33 | 34 | // then 35 | expect(result!.status).to.eq('informational') 36 | }) 37 | 38 | it('returns failure when some steps are not visited', async () => { 39 | // given 40 | const steps = [ 41 | createVisitedStub('foo'), 42 | new StepStub('bar'), 43 | ] 44 | const runner = verifyTopLevelBlocksExecuted(true, steps) 45 | 46 | // when 47 | const { result } = await runner.call(context) 48 | 49 | // then 50 | expect(result!.status).to.eq('failure') 51 | }) 52 | 53 | describe('not strict', () => { 54 | it('returns informational when all steps are visited', async () => { 55 | // given 56 | const steps = [ 57 | createVisitedStub('foo'), 58 | createVisitedStub('bar'), 59 | ] 60 | const runner = verifyTopLevelBlocksExecuted(false, steps) 61 | 62 | // when 63 | const { result } = await runner.call(context) 64 | 65 | // then 66 | expect(result!.status).to.eq('informational') 67 | }) 68 | 69 | it('returns failure when no steps were visited', async () => { 70 | // given 71 | const steps = [ 72 | new StepStub('foo'), 73 | new StepStub('bar'), 74 | ] 75 | const runner = verifyTopLevelBlocksExecuted(false, steps) 76 | 77 | // when 78 | const { result } = await runner.call(context) 79 | 80 | // then 81 | expect(result!.status).to.eq('failure') 82 | }) 83 | 84 | it('returns warning when some steps were not visited', async () => { 85 | // given 86 | const steps = [ 87 | new StepStub('foo'), 88 | createVisitedStub('bar'), 89 | ] 90 | const runner = verifyTopLevelBlocksExecuted(false, steps) 91 | 92 | // when 93 | const { result } = await runner.call(context) 94 | 95 | // then 96 | expect(result!.status).to.eq('warning') 97 | }) 98 | }) 99 | }) 100 | }) 101 | -------------------------------------------------------------------------------- /validator-e2e/lib/strictRunVerification.ts: -------------------------------------------------------------------------------- 1 | import { E2eContext } from '../types' 2 | import { checkChain, CheckResult, Result } from 'hydra-validator-core' 3 | import { RuntimeStep } from './steps/factory' 4 | 5 | interface Counts { 6 | total: number 7 | visited: number 8 | } 9 | 10 | function getCounts(counts: Counts, step: RuntimeStep) { 11 | counts.total += 1 12 | counts.visited += step.visited ? 1 : 0 13 | 14 | return counts 15 | } 16 | 17 | export function verifyTopLevelBlocksExecuted(strict: boolean, steps: RuntimeStep[]): checkChain { 18 | return function runStrictVerification(this: E2eContext): CheckResult { 19 | const { visited, total } = steps.reduce(getCounts, { 20 | total: 0, 21 | visited: 0, 22 | }) 23 | 24 | const summary = `Executed ${visited} out of ${total} top-level blocks.` 25 | 26 | if (strict && visited < total) { 27 | return { 28 | result: Result.Failure(summary, 'Strict mode requires that all steps are executed'), 29 | } 30 | } 31 | 32 | if (visited === 0) { 33 | return { 34 | result: Result.Failure(summary, 'At least one step should have been executed in non-strict mode'), 35 | } 36 | } 37 | 38 | if (visited < total) { 39 | return { 40 | result: Result.Warning(summary), 41 | } 42 | } 43 | 44 | return { 45 | result: Result.Informational(summary), 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /validator-e2e/lib/testHelpers.ts: -------------------------------------------------------------------------------- 1 | import { E2eContext } from '../types' 2 | import { checkChain, IResult } from 'hydra-validator-core' 3 | 4 | interface Summary { 5 | successes: number 6 | failures: number 7 | checkNames: string[] 8 | } 9 | 10 | export type RecursivePartial = { 11 | [P in keyof T]?: RecursivePartial; 12 | }; 13 | 14 | export async function runAll(chain: checkChain, context: E2eContext = { scenarios: [], basePath: '' }): Promise { 15 | const checkNames: string[] = [] 16 | 17 | let results: IResult[] = [] 18 | let queue = [chain] 19 | 20 | while (queue.length > 0) { 21 | const check = queue.splice(0, 1)[0] 22 | checkNames.push(check.name) 23 | const checkOutcome = await check.call(context) 24 | 25 | let outcomeResults: IResult[] = [] 26 | let outcomeNextChecks: checkChain[] = [] 27 | if (checkOutcome) { 28 | if (checkOutcome.result) { 29 | outcomeResults = [checkOutcome.result] 30 | } else { 31 | outcomeResults = checkOutcome.results || [] 32 | } 33 | 34 | outcomeNextChecks = checkOutcome.nextChecks ? checkOutcome.nextChecks : [] 35 | } 36 | 37 | results = [...results, ...outcomeResults] 38 | queue = [...queue, ...outcomeNextChecks] 39 | } 40 | 41 | const response = results as any 42 | 43 | response.successes = results.filter(r => r.status === 'success').length 44 | response.failures = results.filter(r => r.status === 'failure').length 45 | response.checkNames = checkNames 46 | 47 | return response 48 | } 49 | -------------------------------------------------------------------------------- /validator-e2e/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hydra-validator-e2e", 3 | "version": "0.12.1", 4 | "description": "A set of tools to execute E2E tests on a Hydra API", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "prepack": "babel . --out-dir . --extensions .ts", 9 | "example:compile": "hypertest-compiler example" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/hypermedia-app/hydra-validator.git" 14 | }, 15 | "keywords": [ 16 | "hydra", 17 | "hypermedia", 18 | "validator", 19 | "e2e", 20 | "tests" 21 | ], 22 | "author": "Tomasz Pluskiewicz ", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/hypermedia-app/hydra-validator/issues" 26 | }, 27 | "homepage": "https://github.com/hypermedia-app/hydra-validator#readme", 28 | "devDependencies": { 29 | "@babel/cli": "^7.13.0", 30 | "@babel/preset-typescript": "^7.12.17", 31 | "@rdfjs/namespace": "^1.1.0", 32 | "@tpluscode/rdfine": "^0.5.23", 33 | "@types/clownface": "^1", 34 | "@types/matchdep": "^2.0.0", 35 | "@types/node": "^14.14.31", 36 | "@types/rdf-dataset-indexed": "^0.4.4", 37 | "@types/rdf-ext": "^1.3.8", 38 | "@types/rdf-js": "^4", 39 | "@types/rdfjs__namespace": "^1.1.1", 40 | "babel-plugin-add-import-extension": "^1.4.4", 41 | "chai": "^4.3.0", 42 | "mocha": "^8.3.0", 43 | "minimist": ">=1.2.2", 44 | "sinon": "^9.2.4" 45 | }, 46 | "dependencies": { 47 | "@hydrofoil/hypertest": "^0.7.2", 48 | "@rdf-esm/data-model": "^0.5.4", 49 | "@tpluscode/rdf-ns-builders": "^0.4.0", 50 | "@zazuko/rdf-vocabularies": "^2019.12.23", 51 | "alcaeus": "^1.1.2", 52 | "clownface": "^1.0.0", 53 | "escape-string-regexp": "^2.0.0", 54 | "hydra-validator-core": "^0.5.1", 55 | "isomorphic-fetch": "^2.2.1", 56 | "rdf-ext": "^1.3.1" 57 | }, 58 | "publishConfig": { 59 | "access": "public" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /validator-e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ES2020", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "noImplicitAny": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /validator-e2e/types.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'hydra-validator-core' 2 | import { ScenarioStep } from './lib/steps' 3 | import type { Loggers } from 'hydra-validator' 4 | 5 | export interface E2eOptions { 6 | docs: string 7 | cwd: string 8 | strict: boolean 9 | } 10 | 11 | export interface E2eContext extends Context { 12 | scenarios: ScenarioStep[] 13 | basePath: string 14 | headers?: Headers 15 | log?: Loggers 16 | } 17 | -------------------------------------------------------------------------------- /validator-ui/.browserslistrc: -------------------------------------------------------------------------------- 1 | last 2 Chrome major versions 2 | last 2 ChromeAndroid major versions 3 | last 2 Edge major versions 4 | last 2 Firefox major versions 5 | last 2 Safari major versions 6 | last 2 iOS major versions -------------------------------------------------------------------------------- /validator-ui/.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | *.d.ts 3 | -------------------------------------------------------------------------------- /validator-ui/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@tpluscode" 4 | ], 5 | "env": { 6 | "browser": true 7 | }, 8 | "parserOptions": { 9 | "project": "./tsconfig.json" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /validator-ui/.lintstagedrc.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | "*.{js,ts}": 3 | - eslint --fix --quiet 4 | - git add 5 | -------------------------------------------------------------------------------- /validator-ui/.npmignore: -------------------------------------------------------------------------------- 1 | ## editors 2 | /.idea 3 | /.vscode 4 | 5 | ## system files 6 | .DS_Store 7 | 8 | # code coverage folders 9 | coverage/ 10 | 11 | ## npm 12 | node_modules 13 | npm-debug.log 14 | 15 | ## temp folders 16 | /.tmp/ 17 | 18 | ## build output 19 | dist 20 | build-stats.json 21 | -------------------------------------------------------------------------------- /validator-ui/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.4.1](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-ui@0.4.0...hydra-validator-ui@0.4.1) (2020-09-22) 7 | 8 | **Note:** Version bump only for package hydra-validator-ui 9 | 10 | 11 | 12 | 13 | 14 | # [0.4.0](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-ui@0.4.0-alpha.1...hydra-validator-ui@0.4.0) (2020-04-15) 15 | 16 | **Note:** Version bump only for package hydra-validator-ui 17 | 18 | 19 | 20 | 21 | 22 | # [0.4.0-alpha.1](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-ui@0.4.0-alpha.0...hydra-validator-ui@0.4.0-alpha.1) (2020-04-15) 23 | 24 | **Note:** Version bump only for package hydra-validator-ui 25 | 26 | 27 | 28 | 29 | 30 | # [0.4.0-alpha.0](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-ui@0.3.0...hydra-validator-ui@0.4.0-alpha.0) (2020-04-10) 31 | 32 | **Note:** Version bump only for package hydra-validator-ui 33 | 34 | 35 | 36 | 37 | 38 | # [0.3.0](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-ui@0.2.7...hydra-validator-ui@0.3.0) (2020-03-17) 39 | 40 | **Note:** Version bump only for package hydra-validator-ui 41 | 42 | 43 | 44 | 45 | 46 | ## [0.2.7](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-ui@0.2.6...hydra-validator-ui@0.2.7) (2020-01-23) 47 | 48 | **Note:** Version bump only for package hydra-validator-ui 49 | 50 | 51 | 52 | 53 | 54 | ## [0.2.6](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-ui@0.2.5...hydra-validator-ui@0.2.6) (2019-12-08) 55 | 56 | **Note:** Version bump only for package hydra-validator-ui 57 | 58 | 59 | 60 | 61 | 62 | ## [0.2.5](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-ui@0.2.4...hydra-validator-ui@0.2.5) (2019-12-07) 63 | 64 | **Note:** Version bump only for package hydra-validator-ui 65 | 66 | 67 | 68 | 69 | 70 | ## [0.2.4](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-ui@0.2.3...hydra-validator-ui@0.2.4) (2019-08-29) 71 | 72 | **Note:** Version bump only for package hydra-validator-ui 73 | 74 | 75 | 76 | 77 | 78 | ## [0.2.3](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-ui@0.2.2...hydra-validator-ui@0.2.3) (2019-08-22) 79 | 80 | **Note:** Version bump only for package hydra-validator-ui 81 | 82 | 83 | 84 | 85 | 86 | ## [0.2.2](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-ui@0.2.1...hydra-validator-ui@0.2.2) (2019-08-19) 87 | 88 | **Note:** Version bump only for package hydra-validator-ui 89 | 90 | 91 | 92 | 93 | 94 | ## [0.2.1](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-ui@0.2.0...hydra-validator-ui@0.2.1) (2019-07-31) 95 | 96 | **Note:** Version bump only for package hydra-validator-ui 97 | 98 | 99 | 100 | 101 | 102 | # [0.2.0](https://github.com/hypermedia-app/hydra-validator/compare/hydra-validator-ui@0.1.1...hydra-validator-ui@0.2.0) (2019-07-31) 103 | 104 | 105 | ### Features 106 | 107 | * **validator-ui:** handle errors on web UI ([645ea1d](https://github.com/hypermedia-app/hydra-validator/commit/645ea1d)) 108 | 109 | 110 | ### Reverts 111 | 112 | * roll back fetch hack ([a43d0d0](https://github.com/hypermedia-app/hydra-validator/commit/a43d0d0)), closes [#16](https://github.com/hypermedia-app/hydra-validator/issues/16) 113 | 114 | 115 | 116 | 117 | 118 | ## 0.1.1 (2019-07-23) 119 | 120 | 121 | 122 | # 0.1.0 (2019-06-18) 123 | 124 | 125 | 126 | ## 0.0.3 (2019-05-06) 127 | 128 | 129 | 130 | ## 0.0.2 (2019-05-06) 131 | 132 | 133 | 134 | ## 0.0.1 (2019-05-06) 135 | 136 | **Note:** Version bump only for package hydra-validator-ui 137 | 138 | 139 | 140 | 141 | 142 | # [0.1.0](https://github.com/hypermedia-app/hydra-validator/compare/v0.0.3...v0.1.0) (2019-06-18) 143 | 144 | **Note:** Version bump only for package hydra-validator-ui 145 | -------------------------------------------------------------------------------- /validator-ui/README.md: -------------------------------------------------------------------------------- 1 | > ## Hydra Analyser Web UI [![Built with open-wc recommendations](https://img.shields.io/badge/built%20with-open--wc-blue.svg)](https://github.com/open-wc) 2 | > Static analysis of API Documentation and resources 3 | 4 | ## Usage 5 | 6 | To check any endpoint for Hydra controls and their correctness go to https://analyse.hypermedia.app and paste an URL 7 | to the textbox and press ENTER. 8 | 9 | The website will dereference that resource and linked API Documentation (if any) and try to check it against the implemented 10 | rules. 11 | 12 | For the online version to work, the API must be served over HTTPS and [CORS must be enabled](https://enable-cors.org) on the server. 13 | 14 | ## Local environment 15 | 16 | To run locally: 17 | 18 | ```sh 19 | lerna bootstrap 20 | npm run start 21 | ``` 22 | -------------------------------------------------------------------------------- /validator-ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 19 | 20 | 21 | Hydra analyser 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /validator-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hydra-validator-ui", 3 | "license": "MIT", 4 | "scripts": { 5 | "start": "webpack-dev-server --mode development --open", 6 | "start:dev:legacy": "webpack-dev-server --mode development --legacy", 7 | "start:prod": "http-server dist/ -o", 8 | "build": "webpack --mode production", 9 | "build:stats": "webpack --mode production --profile --json > bundle-stats.json", 10 | "precommit": "lint-staged" 11 | }, 12 | "dependencies": { 13 | "@hydrofoil/hydrofoil-paper-shell": "^0.1.3", 14 | "@hydrofoil/hydrofoil-shell": "^0.1.1", 15 | "@lit-any/lit-any": "^0.8.1", 16 | "@polymer/app-layout": "^3.0.2", 17 | "@polymer/iron-icon": "^3.0.1", 18 | "@polymer/iron-icons": "^3.0.1", 19 | "@polymer/paper-icon-button": "^3.0.2", 20 | "@polymer/paper-input": "^3.0.2", 21 | "@polymer/paper-item": "^3.0.1", 22 | "hydra-validator-analyse": "^0.3.1", 23 | "hydra-validator-core": "^0.5.0", 24 | "ld-navigation": "^0.5.2", 25 | "lit-element": "^2.0.1", 26 | "lit-html": "^1.0.0" 27 | }, 28 | "devDependencies": { 29 | "@open-wc/building-webpack": "^1.3.5", 30 | "http-server": "^0.11.1", 31 | "lint-staged": "^8.2.1", 32 | "minimist": ">=1.2.2", 33 | "owc-dev-server": "^0.3.0", 34 | "serialize-javascript": ">=2.1.1", 35 | "ts-loader": "^8.0.17", 36 | "webpack-cli": "^3.2.3", 37 | "webpack-dev-server": "^3.1.14", 38 | "webpack-merge": "^4.2.1" 39 | }, 40 | "private": true, 41 | "version": "0.4.1" 42 | } 43 | -------------------------------------------------------------------------------- /validator-ui/src/hydra-validator-ui.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit-element' 2 | import './validator-shell' 3 | import * as item from './views/icon-items' 4 | 5 | class HydraValidatorUi extends LitElement { 6 | static get styles() { 7 | return css` 8 | validator-shell { 9 | max-width: 750px; 10 | margin: 0 auto; 11 | }` 12 | } 13 | 14 | render() { 15 | return html` 16 | 17 |

Hydra validator

18 |

This page let's authors run a set of tests against their Hydra server.

19 | 20 |

Usage

21 |

Type or paste an URL to verify in the address bar above and press ENTER

22 | 23 |

The results

24 |

Results will be presented as a simple list. Each has of four possible outcomes:

25 | 26 |
27 | ${item.success('Successful check. All good!')} 28 | ${item.information('Additional piece of information to help understand the results')} 29 | ${item.warning('Non-critical issue. It may pose problems if not handled correctly by clients')} 30 | ${item.failure('A failed check', 'More details shown on second line')} 31 |
32 |
33 | ` 34 | } 35 | } 36 | 37 | customElements.define('hydra-validator-ui', HydraValidatorUi) 38 | -------------------------------------------------------------------------------- /validator-ui/src/validator-shell.ts: -------------------------------------------------------------------------------- 1 | import { HydrofoilShell } from '@hydrofoil/hydrofoil-shell/hydrofoil-shell' 2 | import '@hydrofoil/hydrofoil-paper-shell/hydrofoil-address-bar' 3 | import '@polymer/paper-input/paper-input' 4 | import '@polymer/iron-icon/iron-icon' 5 | import '@polymer/iron-icons/iron-icons' 6 | import '@polymer/paper-icon-button/paper-icon-button' 7 | import '@polymer/app-layout/app-toolbar/app-toolbar' 8 | import '@polymer/app-layout/app-header/app-header' 9 | import '@polymer/app-layout/app-header-layout/app-header-layout' 10 | import { html } from 'lit-html' 11 | import fireNavigation from 'ld-navigation/fireNavigation' 12 | import './views' 13 | import { css } from 'lit-element' 14 | 15 | function navigate(this: any, e) { 16 | fireNavigation(this, e.target.url) 17 | } 18 | 19 | export default class ValidatorShell extends HydrofoilShell { 20 | constructor() { 21 | super() 22 | this.url = '' 23 | } 24 | 25 | static get styles() { 26 | return css` 27 | app-toolbar, ::slotted(app-toolbar) { 28 | background: var(--paper-indigo-400); 29 | color: white; 30 | }` 31 | } 32 | 33 | home() { 34 | this.url = '' 35 | this.state = 'ready' 36 | } 37 | 38 | renderTop() { 39 | return html` 40 | 41 | 42 | 43 | 44 | 49 | 50 | 51 | 52 | ` 53 | } 54 | 55 | renderMain() { 56 | if (this.state === 'ready') { 57 | return html`${this.renderTop()}` 58 | } 59 | 60 | if (this.state === 'error') { 61 | return html`${this.renderTop()} ${this.lastError}` 62 | } 63 | 64 | return html` 65 | ${this.renderTop()} 66 | ${super.renderMain()}` 67 | } 68 | 69 | async loadResourceInternal(url) { 70 | const [{ runChecks }, firstCheck] = await Promise.all([ 71 | import('hydra-validator-core/run-checks'), 72 | import('hydra-validator-analyse/checks/url-resolvable'), 73 | ]) 74 | 75 | return runChecks(firstCheck.default(url)) 76 | } 77 | } 78 | 79 | customElements.define('validator-shell', ValidatorShell) 80 | -------------------------------------------------------------------------------- /validator-ui/src/views/icon-items.ts: -------------------------------------------------------------------------------- 1 | import { html } from 'lit-html' 2 | import { ifDefined } from 'lit-html/directives/if-defined' 3 | 4 | function item({ icon, color, description, details }) { 5 | import('@polymer/paper-item/paper-icon-item') 6 | import('@polymer/iron-icon/iron-icon') 7 | import('@polymer/iron-icons/iron-icons') 8 | import('@polymer/paper-item/paper-item-body') 9 | 10 | return html` 11 | 12 | 13 | 14 |
${description}
15 | ${details ? html`
${details}
` : ''} 16 |
17 |
` 18 | } 19 | 20 | export function warning(description, details?) { 21 | return item({ 22 | description, 23 | details, 24 | color: 'orange', 25 | icon: 'report-problem', 26 | }) 27 | } 28 | 29 | export function failure(description, details) { 30 | return item({ 31 | description, 32 | details, 33 | icon: 'clear', 34 | color: 'red', 35 | }) 36 | } 37 | 38 | export function success(description, details?) { 39 | return item({ 40 | description, 41 | details, 42 | icon: 'check', 43 | color: 'darkgreen', 44 | }) 45 | } 46 | 47 | export function information(description, details?) { 48 | return item({ 49 | description, 50 | details, 51 | icon: 'lightbulb-outline', 52 | color: 'darkblue', 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /validator-ui/src/views/index.ts: -------------------------------------------------------------------------------- 1 | import ViewTemplates from '@lit-any/lit-any/views' 2 | import { asyncAppend } from 'lit-html/directives/async-append' 3 | import { html } from 'lit-html' 4 | import * as item from './icon-items' 5 | 6 | ViewTemplates.default.when 7 | .scopeMatches('hydrofoil-shell') 8 | .renders((model, next) => { 9 | return html` 10 |
11 | ${asyncAppend(model, v => next(v, 'result'))} 12 |
` 13 | }) 14 | 15 | ViewTemplates.default.when 16 | .scopeMatches('result') 17 | .valueMatches(v => v.result && (v.result.status !== 'failure' && v.result.status !== 'error')) 18 | .renders(check => { 19 | switch (check.result.status) { 20 | case 'informational': 21 | return item.information(check.result.description, check.result.details) 22 | case 'success': 23 | return item.success(check.result.description, check.result.details) 24 | default: 25 | return item.warning(check.result.description, check.result.details) 26 | } 27 | }) 28 | 29 | ViewTemplates.default.when 30 | .scopeMatches('result') 31 | .valueMatches(v => v.result && (v.result.status === 'failure' || v.result.status === 'error')) 32 | .renders(check => item.failure(check.result.description, check.result.details)) 33 | -------------------------------------------------------------------------------- /validator-ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "target": "esnext", 6 | "allowJs": true, 7 | "moduleResolution": "node", 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "noImplicitAny": false, 11 | "noEmit": false 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /validator-ui/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path') 2 | const merge = require('webpack-merge') 3 | const createDefaultConfig = require('@open-wc/building-webpack/modern-config') 4 | 5 | // If you don't need IE11 support, use the modern-config instead 6 | // import createDefaultConfig from '@open-wc/building-webpack/modern-config'; 7 | 8 | const config = createDefaultConfig({ 9 | input: resolve(__dirname, './index.html') 10 | }) 11 | 12 | module.exports = merge(config, { 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.tsx?$/, 17 | use: 'ts-loader', 18 | exclude: /node_modules/ 19 | } 20 | ] 21 | }, 22 | resolve: { 23 | extensions: [ '.tsx', '.ts', '.js' ] 24 | } 25 | }) 26 | --------------------------------------------------------------------------------