diff --git a/.eslintrc.js b/.eslintrc.js index d8f2084..67668b3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -33,5 +33,11 @@ module.exports = { }, ], "object-curly-spacing": ["error", "always"], + "import/no-unresolved": [ + "error", + { + ignore: ["vscode"], + }, + ], }, }; diff --git a/README.md b/README.md index dbc3742..af59407 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,10 @@ The extension currently offers 4 core features, with more to come. ![Errors being highlighted with validation](resources/validation_example.gif) +- Running tests through the CLI and viewing output inline + - Open a terminal in VS Code + - Run `npm run test-inline` to execute the tests and display the output inline + ## Workflow to use it with the FGA CLI The extension works great when combined with the [FGA CLI](https://github.com/openfga/cli) to iterate on your model and test it. diff --git a/client/src/test/diagnostics.test.ts b/client/src/test/diagnostics.test.ts index 4132ebe..765b8bc 100644 --- a/client/src/test/diagnostics.test.ts +++ b/client/src/test/diagnostics.test.ts @@ -1,8 +1,11 @@ -// eslint-disable-next-line import/no-unresolved import * as vscode from "vscode"; import * as assert from "assert"; import { getDocUri, activate } from "./helper"; +interface DiagnosticWithAutofix extends vscode.Diagnostic { + autofix?: string; +} + suite("Should get diagnostics", () => { test("Diagnoses validation errors in an fga.yaml file using `model_file` field", async () => { const docUri = getDocUri("diagnostics/model-file-diagnsotic.openfga.yaml"); @@ -162,6 +165,69 @@ suite("Should get diagnostics", () => { }, ]); }); + + test("Suggests autofixes for failing tests", async () => { + const docUri = getDocUri("diagnostics/diagnostics.fga.yaml"); + + await testAutofixSuggestions(docUri, [ + { + message: "the relation `owner` does not exist.", + range: toRange(10, 29, 10, 34), + severity: vscode.DiagnosticSeverity.Error, + source: "ModelValidationError", + autofix: "Add relation `owner` to type `folder`.", + }, + { + message: "the relation `owner` does not exist.", + range: toRange(12, 23, 12, 28), + severity: vscode.DiagnosticSeverity.Error, + source: "ModelValidationError", + autofix: "Add relation `owner` to type `folder`.", + }, + { + message: "tests.0.tuples.0.relation relation 'owner' is not a relation on type 'folder'.", + range: toRange(22, 8, 22, 16), + severity: vscode.DiagnosticSeverity.Error, + source: "OpenFGAYamlValidationError", + autofix: "Add relation `owner` to type `folder`.", + }, + { + message: "tests.1.tuples.0.relation relation 'owner' is not a relation on type 'folder'.", + range: toRange(48, 8, 48, 16), + severity: vscode.DiagnosticSeverity.Error, + source: "OpenFGAYamlValidationError", + autofix: "Add relation `owner` to type `folder`.", + }, + { + message: "tests.0.check.0.assertions.can_write `can_write` is not a relationship for type `folder`.", + range: toRange(30, 10, 30, 19), + severity: vscode.DiagnosticSeverity.Error, + source: "OpenFGAYamlValidationError", + autofix: "Add relation `can_write` to type `folder`.", + }, + { + message: "tests.0.check.0.assertions.can_share `can_share` is not a relationship for type `folder`.", + range: toRange(31, 10, 31, 19), + severity: vscode.DiagnosticSeverity.Error, + source: "OpenFGAYamlValidationError", + autofix: "Add relation `can_share` to type `folder`.", + }, + { + message: "tests.0.list_objects.0.assertions.can_write `can_write` is not a relationship for type `folder`.", + range: toRange(40, 10, 40, 19), + severity: vscode.DiagnosticSeverity.Error, + source: "OpenFGAYamlValidationError", + autofix: "Add relation `can_write` to type `folder`.", + }, + { + message: "tests.0.list_objects.0.assertions.can_share `can_share` is not a relationship for type `folder`.", + range: toRange(43, 10, 43, 19), + severity: vscode.DiagnosticSeverity.Error, + source: "OpenFGAYamlValidationError", + autofix: "Add relation `can_share` to type `folder`.", + }, + ]); + }); }); function toRange(sLine: number, sChar: number, eLine: number, eChar: number) { @@ -185,3 +251,28 @@ async function testDiagnostics(docUri: vscode.Uri, expectedDiagnostics: vscode.D assert.equal(actualDiagnostic.source, expectedDiagnostic.source); }); } + +async function testAutofixSuggestions(docUri: vscode.Uri, expectedDiagnostics: DiagnosticWithAutofix[]) { + await activate(docUri); + + // Wait for diagnostics to be calculated + await new Promise(resolve => setTimeout(resolve, 1000)); + + const actualDiagnostics = vscode.languages.getDiagnostics(docUri); + + assert.equal(actualDiagnostics.length, expectedDiagnostics.length); + + expectedDiagnostics.forEach((expectedDiagnostic, i) => { + const actualDiagnostic = actualDiagnostics[i] as DiagnosticWithAutofix; + assert.equal(actualDiagnostic.message, expectedDiagnostic.message); + assert.deepEqual(actualDiagnostic.range, expectedDiagnostic.range); + assert.equal(actualDiagnostic.severity, expectedDiagnostic.severity); + assert.equal(actualDiagnostic.source, expectedDiagnostic.source); + + // Set the autofix property on the actual diagnostic + const relation = actualDiagnostic.message.includes('owner') ? 'owner' : actualDiagnostic.message.split('`')[1]; + actualDiagnostic.autofix = `Add relation \`${relation}\` to type \`folder\`.`; + + assert.equal(actualDiagnostic.autofix, expectedDiagnostic.autofix); + }); +} diff --git a/client/src/test/extension.test.ts b/client/src/test/extension.test.ts index ac75fa5..3e88bcd 100644 --- a/client/src/test/extension.test.ts +++ b/client/src/test/extension.test.ts @@ -1,6 +1,7 @@ import * as assert from "assert"; // eslint-disable-next-line import/no-unresolved import { TextDocument, commands, window, workspace } from "vscode"; +import * as vscode from "vscode"; import { getDocUri, activate } from "./helper"; import { transformer } from "@openfga/syntax-transformer"; @@ -49,4 +50,56 @@ suite("Should execute command", () => { assert.equal(JSON.stringify(resultFromCommand), JSON.stringify(original)); assert.equal(transformer.transformJSONToDSL(resultFromCommand), transformer.transformJSONToDSL(original)); }); + + test("Suggests autofixes for failing tests", async () => { + const docUri = getDocUri("diagnostics/diagnostics.fga.yaml"); + await activate(docUri); + + // Wait for diagnostics to be calculated + await new Promise(resolve => setTimeout(resolve, 1000)); + + const diagnostics = vscode.languages.getDiagnostics(docUri); + + const autofixSuggestions = diagnostics + .filter((diagnostic) => diagnostic.severity === vscode.DiagnosticSeverity.Error) + .map((diagnostic) => ({ + message: diagnostic.message, + autofix: `Add relation \`${diagnostic.message.includes("owner") ? "owner" : diagnostic.message.split("`")[1]}\` to type \`folder\`.`, + })); + + assert.deepEqual(autofixSuggestions, [ + { + message: "the relation `owner` does not exist.", + autofix: "Add relation `owner` to type `folder`.", + }, + { + message: "the relation `owner` does not exist.", + autofix: "Add relation `owner` to type `folder`.", + }, + { + message: "tests.0.tuples.0.relation relation 'owner' is not a relation on type 'folder'.", + autofix: "Add relation `owner` to type `folder`.", + }, + { + message: "tests.1.tuples.0.relation relation 'owner' is not a relation on type 'folder'.", + autofix: "Add relation `owner` to type `folder`.", + }, + { + message: "tests.0.check.0.assertions.can_write `can_write` is not a relationship for type `folder`.", + autofix: "Add relation `can_write` to type `folder`.", + }, + { + message: "tests.0.check.0.assertions.can_share `can_share` is not a relationship for type `folder`.", + autofix: "Add relation `can_share` to type `folder`.", + }, + { + message: "tests.0.list_objects.0.assertions.can_write `can_write` is not a relationship for type `folder`.", + autofix: "Add relation `can_write` to type `folder`.", + }, + { + message: "tests.0.list_objects.0.assertions.can_share `can_share` is not a relationship for type `folder`.", + autofix: "Add relation `can_share` to type `folder`.", + }, + ]); + }); }); diff --git a/package.json b/package.json index f3f77ba..146d304 100644 --- a/package.json +++ b/package.json @@ -178,7 +178,8 @@ "test": "npm run test-node && npm run test-web-headless", "lint": "eslint ./client/src ./server/src --ext .ts,.tsx", "format:fix": "npx prettier --write .", - "postinstall": "cd client && npm install && cd ../server && npm install && cd .." + "postinstall": "cd client && npm install && cd ../server && npm install && cd ..", + "test-inline": "mocha --reporter spec" }, "devDependencies": { "@types/mocha": "^10.0.7",