From 47522a1dcbc0827c10b48d56a94468a699d32cf2 Mon Sep 17 00:00:00 2001 From: Pauline Didier Date: Wed, 4 Sep 2024 12:32:20 +0200 Subject: [PATCH 1/4] feat: :tada: add basic validator --- api/package.json | 4 +- api/src/app.ts | 4 + .../resources/gram/v1/token/delete.spec.ts | 1 - .../resources/gram/v1/validation/router.ts | 11 ++ .../gram/v1/validation/validateModel.spec.ts | 183 ++++++++++++++++++ .../gram/v1/validation/validateModel.ts | 43 ++++ api/src/test-util/model.ts | 7 +- config/default.ts | 3 + .../static/StaticValidationProvider.ts | 141 ++++++++++++++ core/src/Bootstrapper.ts | 6 + core/src/bootstrap.ts | 2 + core/src/config/GramConfiguration.ts | 2 + core/src/data/dal.ts | 4 + core/src/validation/ValidationHandler.ts | 59 ++++++ 14 files changed, 464 insertions(+), 6 deletions(-) create mode 100644 api/src/resources/gram/v1/validation/router.ts create mode 100644 api/src/resources/gram/v1/validation/validateModel.spec.ts create mode 100644 api/src/resources/gram/v1/validation/validateModel.ts create mode 100644 config/providers/static/StaticValidationProvider.ts create mode 100644 core/src/validation/ValidationHandler.ts diff --git a/api/package.json b/api/package.json index 421aa7f8..e63cf37f 100644 --- a/api/package.json +++ b/api/package.json @@ -12,7 +12,7 @@ "lint-fix": "prettier -l src/ --write", "debug": "NODE_ENV=development node --loader ts-node/esm --inspect=127.0.0.1 src/index.js", "test": "NODE_OPTIONS=--experimental-vm-modules NODE_ENV=test jest --detectOpenHandles --no-cache --runInBand --logHeapUsage --forceExit", - "jest": "NODE_OPTIONS=--experimental-vm-modules NODE_ENV=test jest --detectOpenHandles --no-cache" + "test-validate": "NODE_OPTIONS=--experimental-vm-modules NODE_ENV=test jest --detectOpenHandles --no-cache ./src/resources/gram/v1/validation/validateModel.spec.ts" }, "main": "dist/index.js", "repository": { @@ -68,4 +68,4 @@ "ts-node": "^10.9.1", "tsconfig-paths": "^4.1.2" } -} +} \ No newline at end of file diff --git a/api/src/app.ts b/api/src/app.ts index b32f0448..23638b57 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -41,6 +41,7 @@ import { initSentry } from "./util/sentry.js"; import { userRouter } from "./resources/gram/v1/user/router.js"; import { searchRouter } from "./resources/gram/v1/search/router.js"; import { systemsRouter } from "./resources/gram/v1/systems/router.js"; +import { validationRouter } from "./resources/gram/v1/validation/router.js"; export async function createApp(dal: DataAccessLayer) { // Start constructing the app. @@ -222,6 +223,9 @@ export async function createApp(dal: DataAccessLayer) { errorWrap(searchClasses(dal.ccHandler)) ); + // Model Validation + authenticatedRoutes.use("/validate", validationRouter(dal)); + // Report Routes authenticatedRoutes.get( "/reports/system-compliance", diff --git a/api/src/resources/gram/v1/token/delete.spec.ts b/api/src/resources/gram/v1/token/delete.spec.ts index d8730588..a9933e2c 100644 --- a/api/src/resources/gram/v1/token/delete.spec.ts +++ b/api/src/resources/gram/v1/token/delete.spec.ts @@ -1,6 +1,5 @@ import request from "supertest"; import { createTestApp } from "../../../../test-util/app.js"; -import { jest } from "@jest/globals"; describe("token.delete", () => { let app: any; diff --git a/api/src/resources/gram/v1/validation/router.ts b/api/src/resources/gram/v1/validation/router.ts new file mode 100644 index 00000000..c598aab4 --- /dev/null +++ b/api/src/resources/gram/v1/validation/router.ts @@ -0,0 +1,11 @@ +import { DataAccessLayer } from "@gram/core/dist/data/dal.js"; +import express from "express"; +import { errorWrap } from "../../../../util/errorHandler.js"; +import { validateModel } from "./validateModel.js"; + +export function validationRouter(dal: DataAccessLayer): express.Router { + const router = express.Router(); + + router.get("/:id", errorWrap(validateModel(dal))); + return router; +} diff --git a/api/src/resources/gram/v1/validation/validateModel.spec.ts b/api/src/resources/gram/v1/validation/validateModel.spec.ts new file mode 100644 index 00000000..361d6e37 --- /dev/null +++ b/api/src/resources/gram/v1/validation/validateModel.spec.ts @@ -0,0 +1,183 @@ +import request from "supertest"; +import { jest } from "@jest/globals"; +import { createTestApp } from "../../../../test-util/app.js"; +import { sampleUserToken } from "../../../../test-util/sampleTokens.js"; +import { ModelDataService } from "@gram/core/dist/data/models/ModelDataService.js"; +import { sampleOwnedSystem } from "../../../../test-util/sampleOwnedSystem.js"; +import Model, { Component } from "@gram/core/dist/data/models/Model.js"; +import { createSampleModel } from "../../../../test-util/model.js"; +import { DataAccessLayer } from "@gram/core/dist/data/dal.js"; +import exp from "constants"; +import { + ModelValidationRule, + ValidationResult, + ValidationRule, +} from "@gram/core/dist/validation/ValidationHandler.js"; + +describe("validateModel", () => { + let app: any; + let token: string; + let getById: any; + let modelService: ModelDataService; + let dal: DataAccessLayer; + + beforeAll(async () => { + ({ app, dal } = await createTestApp()); + token = await sampleUserToken(); + modelService = dal.modelService; + getById = jest.spyOn(modelService, "getById"); + }); + + it("should return 401 on un-authenticated request", async () => { + const res = await request(app).get("/api/v1/validate/12323"); + expect(res.status).toBe(401); + }); + it("should return 401 when using invalid user token", async () => { + const res = await request(app) + .get("/api/v1/validate/12323") + .set("Authorization", "invalidtoken"); + expect(res.status).toBe(401); + }); + it("should return 400 if the model id is invalid or unknown", async () => { + const invalidmodelId = "12323"; + const res = await request(app) + .get("/api/v1/validate/" + invalidmodelId) + .set("Authorization", token); + expect(res.status).toBe(400); + }); + + it("should return 200 if the model id is valid", async () => { + const validModelId = await createSampleModel(dal); + + getById.mockImplementation(async () => { + const model = new Model(validModelId, "some-version", "some-owner"); + return model; + }); + + const res = await request(app) + .get("/api/v1/validate/" + validModelId) + .set("Authorization", token); + expect(res.status).toBe(200); + }); + + it("should return a list of validation results", async () => { + const validModelId = "1daf8acc-a691-408d-802c-46e360d2f427"; + getById.mockImplementation(async () => { + const model = new Model("some-system-id", "some-version", "some-owner"); + model.id = validModelId; + model.data = { + dataFlows: [], + components: [ + { + x: 222, + y: 350, + id: "5e8e6021-9455-4157-a026-c7be218b1019", + name: "Process", + type: "proc", + }, + ], + }; + return model; + }); + + const res = await request(app) + .get("/api/v1/validate/" + validModelId) + .set("Authorization", token); + expect(res.status).toBe(200); + expect(res.body.id).toBe(validModelId); + expect(res.body.total).toBe(res.body.results.length); + expect(res.body.results).toBeDefined(); + + // expect (res.body.data) + }); + + it("should return a list of model validation results only if model is empty", async () => { + const validModelId = "1daf8acc-a691-408d-802c-46e360d2f427"; + getById.mockImplementation(async () => { + const model = new Model("some-system-id", "some-version", "some-owner"); + model.id = validModelId; + model.data = { + dataFlows: [], + components: [], + }; + return model; + }); + + const res = await request(app) + .get("/api/v1/validate/" + validModelId) + .set("Authorization", token); + const results = res.body.results; + console.log("empty model", { results }); + + expect(res.status).toBe(200); + expect(results.length).not.toBe(0); + expect(res.body.id).toBe(validModelId); + expect(res.body.total).toBe(results.length); + expect(Array.isArray(results)).toBeTruthy(); + expect( + results.every((element: ValidationResult) => element.type === "model") + ).toBeTruthy(); + expect( + results.some((element: ValidationResult) => element.type === "component") + ).toBeFalsy(); + }); + + it("should return a list of component and model validation results if model has components", async () => { + const validModelId = "1daf8acc-a691-408d-802c-46e360d2f427"; + getById.mockImplementation(async () => { + const model = new Model("some-system-id", "some-version", "some-owner"); + model.id = validModelId; + model.data = { + dataFlows: [], + components: [ + { + x: 222, + y: 350, + id: "5e8e6021-9455-4157-a026-c7be218b1019", + name: "Process", + type: "proc", + }, + { + x: 222, + y: 350, + id: "5e8e7021-9455-4157-z026-c7be218b1019", + name: "Process", + type: "proc", + }, + ], + }; + return model; + }); + + const res = await request(app) + .get("/api/v1/validate/" + validModelId) + .set("Authorization", token); + const results = res.body.results; + + expect(res.status).toBe(200); + expect(results.length).not.toBe(0); + expect(res.body.id).toBe(validModelId); + expect(res.body.total).toBe(results.length); + expect(Array.isArray(results)).toBeTruthy(); + + expect( + results.some( + (element: ValidationResult) => + element.type === "component" && + element.elementId === "5e8e6021-9455-4157-a026-c7be218b1019" + ) + ).toBeTruthy(); + + expect( + results.some( + (element: ValidationResult) => + element.type === "component" && + element.elementId === "5e8e7021-9455-4157-z026-c7be218b1019" + ) + ).toBeTruthy(); + + expect( + results.some((element: ValidationResult) => element.type === "model") + ).toBeTruthy(); + }); +}); diff --git a/api/src/resources/gram/v1/validation/validateModel.ts b/api/src/resources/gram/v1/validation/validateModel.ts new file mode 100644 index 00000000..4de2c3d3 --- /dev/null +++ b/api/src/resources/gram/v1/validation/validateModel.ts @@ -0,0 +1,43 @@ +/** + * GET /api/v1/validate/{id} + * @exports {function} handler + */ +import { DataAccessLayer } from "@gram/core/dist/data/dal.js"; +import Model from "@gram/core/dist/data/models/Model.js"; +import { Request, Response } from "express"; + +export function validateModel(dal: DataAccessLayer) { + return async (req: Request, res: Response) => { + const modelId = req.params.id; + let model: Model | null = null; + // Get the model from the DAL + try { + model = await dal.modelService.getById(modelId); + + if (!model) { + return res.sendStatus(400); + } + } catch (error) { + return res.sendStatus(400); + } + // Validate the model + try { + // Use validation service to validate the model + // Return the validation results + model id + console.log( + "dal.validationHandler.validationProviders", + dal.validationHandler.validationProviders + ); + + const validationResults = await dal.validationHandler.validate(model); + + return res.json({ + id: modelId, + total: validationResults.length, + results: validationResults, + }); + } catch (error) { + return res.sendStatus(400); + } + }; +} diff --git a/api/src/test-util/model.ts b/api/src/test-util/model.ts index 5873f9b1..cbcd143b 100644 --- a/api/src/test-util/model.ts +++ b/api/src/test-util/model.ts @@ -1,13 +1,14 @@ import { DataAccessLayer } from "@gram/core/dist/data/dal.js"; -import Model from "@gram/core/dist/data/models/Model.js"; +import Model, { ModelData } from "@gram/core/dist/data/models/Model.js"; import { sampleOwnedSystem } from "./sampleOwnedSystem.js"; export async function createSampleModel( dal: DataAccessLayer, - owner: string = "root" + owner: string = "root", + data: ModelData = { components: [], dataFlows: [] } ) { const model = new Model(sampleOwnedSystem.id, "some-version", owner); - model.data = { components: [], dataFlows: [] }; + model.data = data; const modelId = await dal.modelService.create(model); return modelId; } diff --git a/config/default.ts b/config/default.ts index 15d7b460..3b5a1cc9 100644 --- a/config/default.ts +++ b/config/default.ts @@ -26,6 +26,7 @@ import { StaticUserProvider } from "./providers/static/StaticUserProvider.js"; import { Team } from "@gram/core/dist/auth/models/Team.js"; import { StaticTeamProvider } from "./providers/static/StaticTeamProvider.js"; import { StrideSuggestionProvider } from "@gram/stride"; +import { StaticValidationProvider } from "./providers/static/StaticValidationProvider.js"; export const defaultConfig: GramConfiguration = { appPort: 8080, @@ -180,6 +181,7 @@ export const defaultConfig: GramConfiguration = { const systemProvider = new StaticSystemProvider(sampleSystems); const teamProvider = new StaticTeamProvider(sampleTeams, teamMap); + const staticValidationProvider = new StaticValidationProvider(); return { assetFolders: [ @@ -220,6 +222,7 @@ export const defaultConfig: GramConfiguration = { teamProvider, // completely optional dal.modelService, ], + validationProviders: [staticValidationProvider], }; }, }; diff --git a/config/providers/static/StaticValidationProvider.ts b/config/providers/static/StaticValidationProvider.ts new file mode 100644 index 00000000..07f672c1 --- /dev/null +++ b/config/providers/static/StaticValidationProvider.ts @@ -0,0 +1,141 @@ +import Model from "@gram/core/dist/data/models/Model.js"; +import { + ComponentValidationRule, + ModelValidationRule, + ValidationProvider, + ValidationResult, + ValidationRule, +} from "@gram/core/dist/validation/ValidationHandler.js"; +import log4js from "log4js"; + +const log = log4js.getLogger("staticValidationProvider"); + +const validationRules: ValidationRule[] = [ + { + type: "component", + name: "should have a name", + affectedType: ["proc", "ee", "ds", "tf"], + test: (component, _) => component.name.trim() !== "", + messageTrue: "Component has a name", + messageFalse: "Component does not have a name", + }, + { + type: "component", + name: "should have a description", + affectedType: ["proc", "ee", "ds", "tf"], + test: (component, _) => + component.description ? component.description.trim() !== "" : false, + messageTrue: "Component has a description", + messageFalse: "Component does not have a description", + }, + { + type: "component", + name: "should have a long enough description", + affectedType: ["proc", "ee", "ds", "tf"], + conditionalRules: [["should have a description", true]], + test: (component, _) => + component.description ? component.description.length > 100 : false, + messageTrue: "Component has a long enough description", + messageFalse: + "Component's description should be at least 100, to be descriptive enough", + }, + { + type: "component", + name: "should have at least one tech stack", + affectedType: ["proc", "ds", "tf"], + test: (component, _) => + component.classes ? component.classes.length > 0 : false, + messageTrue: "Component has at least one tech stack", + messageFalse: "Component does not have any tech stack", + }, + { + type: "component", + name: "should have at least one dataflow", + affectedType: ["proc", "ds", "tf", "ee"], + test: (component, dataflows) => { + if (dataflows.length === 0) { + return false; + } + return dataflows.some( + (dataflow) => + dataflow.endComponent.id === component.id || + dataflow.startComponent.id === component.id + ); + }, + messageTrue: "Component has at least one dataflow", + messageFalse: "Component does not have any dataflow", + }, + { + type: "model", + name: "should have at least one component", + affectedType: [], + test: (model) => model.data.components.length > 0, + messageTrue: "Model has at least one component", + messageFalse: "Model is empty", + }, +]; + +function isComponentValidation( + rule: ValidationRule +): rule is ComponentValidationRule { + return rule.type === "component"; +} + +function isModelValidation(rule: ValidationRule): rule is ModelValidationRule { + return rule.type === "model"; +} +export class StaticValidationProvider implements ValidationProvider { + async validate(model: Model): Promise { + log.info("StaticValidationProvider is called"); + if (!model) { + return [ + { + type: "model", + ruleName: "should have at least one component", + testResult: false, + message: "Model is empty", + }, + ]; + } + + const components = model.data.components; + const dataFlows = model.data.dataFlows; + const componentRules = validationRules.filter( + (rule) => rule.type === "component" + ); + const modelRules = validationRules.filter((rule) => rule.type === "model"); + const results: ValidationResult[] = []; + + // Validate components + + for (const component of components) { + for (const rule of componentRules) { + if (!isComponentValidation(rule)) { + continue; + } + const testResult = rule.test(component, dataFlows); + results.push({ + type: rule.type, + elementId: component.id, + ruleName: rule.name, + testResult: testResult, + message: testResult ? rule.messageTrue : rule.messageFalse, + }); + } + } + + for (const rule of modelRules) { + if (!isModelValidation(rule)) { + continue; + } + const testResult = rule.test(model); + results.push({ + type: rule.type, + ruleName: rule.name, + testResult: testResult, + message: testResult ? rule.messageTrue : rule.messageFalse, + }); + } + return results; + } +} diff --git a/core/src/Bootstrapper.ts b/core/src/Bootstrapper.ts index 4d487e4c..e63d09fa 100644 --- a/core/src/Bootstrapper.ts +++ b/core/src/Bootstrapper.ts @@ -18,6 +18,7 @@ import { getPool, migratePlugin } from "./plugins/data.js"; import pg from "pg"; import { TeamProvider } from "./auth/TeamProvider.js"; import { SearchProvider } from "./search/SearchHandler.js"; +import { ValidationProvider } from "./validation/ValidationHandler.js"; /* Could create a temporary directory instead */ export const AssetDir = "assets"; @@ -145,6 +146,11 @@ export class Bootstrapper { this.dal.reviewerHandler.setReviewerProvider(reviewerProvider); } + setValidationProvider(validationProdiver: ValidationProvider): void { + this.log.info(`Set Validation Provider: ${validationProdiver}`); + this.dal.validationHandler.register(validationProdiver); + } + compileAssets() { const files = readdirSync(AssetDir); this.log.info("Clearing asset symlinks"); diff --git a/core/src/bootstrap.ts b/core/src/bootstrap.ts index 6afffe39..9bcf4786 100644 --- a/core/src/bootstrap.ts +++ b/core/src/bootstrap.ts @@ -53,6 +53,8 @@ export async function bootstrap(): Promise { providers.assetFolders?.forEach((af) => bt.registerAssets(af.name, af.folderPath) ); + + providers.validationProviders?.forEach((vp) => bt.setValidationProvider(vp)); bt.compileAssets(); return dal; diff --git a/core/src/config/GramConfiguration.ts b/core/src/config/GramConfiguration.ts index e8c561b8..ff717c27 100644 --- a/core/src/config/GramConfiguration.ts +++ b/core/src/config/GramConfiguration.ts @@ -14,6 +14,7 @@ import type { Migration } from "../data/Migration.js"; import type { TeamProvider } from "../auth/TeamProvider.js"; import type { ActionItemExporter } from "../action-items/ActionItemExporter.js"; import type { SearchProvider } from "../search/SearchHandler.js"; +import { ValidationProvider } from "../validation/ValidationHandler.js"; export interface Providers { /** @@ -36,6 +37,7 @@ export interface Providers { teamProvider?: TeamProvider; actionItemExporters?: ActionItemExporter[]; searchProviders?: SearchProvider[]; + validationProviders?: ValidationProvider[]; } export interface GramConfiguration { diff --git a/core/src/data/dal.ts b/core/src/data/dal.ts index b44c8805..ddadf588 100644 --- a/core/src/data/dal.ts +++ b/core/src/data/dal.ts @@ -28,6 +28,8 @@ import { ActionItemHandler } from "../action-items/ActionItemHandler.js"; import { LinkDataService } from "./links/LinkDataService.js"; import { SearchHandler } from "../search/SearchHandler.js"; +import { ValidationHandler } from "../validation/ValidationHandler.js"; + /** * Class that carries access to all DataServices, useful for passing dependencies. */ @@ -57,6 +59,7 @@ export class DataAccessLayer { teamHandler: TeamHandler; actionItemHandler: ActionItemHandler; searchHandler: SearchHandler; + validationHandler: ValidationHandler; get authzProvider(): AuthzProvider { return authzProvider; @@ -80,6 +83,7 @@ export class DataAccessLayer { this.userHandler = new UserHandler(); this.reviewerHandler = new ReviewerHandler(); this.searchHandler = new SearchHandler(); + this.validationHandler = new ValidationHandler(); // Initialize Data Services this.modelService = new ModelDataService(this); diff --git a/core/src/validation/ValidationHandler.ts b/core/src/validation/ValidationHandler.ts new file mode 100644 index 00000000..d7708454 --- /dev/null +++ b/core/src/validation/ValidationHandler.ts @@ -0,0 +1,59 @@ +import Model, { Component, DataFlow } from "../data/models/Model.js"; + +export interface ValidationResult { + type: "component" | "resource" | "model"; + elementId?: string; + ruleName: string; + testResult: boolean; + message: string; +} + +export interface ValidationProvider { + validate(model: Model): Promise; +} + +export interface ComponentValidationRule { + type: "component"; + name: string; + affectedType: ("proc" | "ee" | "ds" | "tf")[]; + conditionalRules?: [string, boolean][]; // ["name of the rule", "result of the test"] + test: (component: Component, dataflows: DataFlow[]) => boolean; // Return true if the model follows the rules + messageTrue: string; + messageFalse: string; +} + +export interface ModelValidationRule { + type: "model"; + name: string; + affectedType: []; + conditionalRules?: [string, boolean][]; // ["name of the rule", "result of the test"] + test: (model: Model) => boolean; // Return true if the model follows the rules + messageTrue: string; + messageFalse: string; +} + +export type ValidationRule = ComponentValidationRule | ModelValidationRule; + +export class ValidationHandler { + validationProviders: ValidationProvider[]; + + constructor() { + this.validationProviders = []; + } + + register(provider: ValidationProvider): void { + this.validationProviders.push(provider); + } + + async validate(model: Model): Promise { + return [ + ...(await Promise.all( + this.validationProviders.map( + async (provider): Promise => ({ + ...(await provider.validate(model)), + }) + ) + )), + ].flat(); + } +} From 9308c158462a19daebddc96633321d8a78006ff8 Mon Sep 17 00:00:00 2001 From: Pauline Didier Date: Fri, 6 Sep 2024 17:05:53 +0200 Subject: [PATCH 2/4] test: :test_tube: test basic validation --- .../gram/v1/validation/validateModel.spec.ts | 22 ++- .../gram/v1/validation/validateModel.ts | 5 +- api/src/test-util/testConfig.ts | 2 + app/src/components/login/Login.spec.js | 2 +- config/jest.config.ts | 23 +++ config/package.json | 5 +- .../static/StaticValidationProvider.spec.ts | 176 ++++++++++++++++++ .../static/StaticValidationProvider.ts | 9 +- core/src/test-util/testConfig.ts | 1 + core/src/validation/ValidationHandler.ts | 5 +- 10 files changed, 229 insertions(+), 21 deletions(-) create mode 100644 config/jest.config.ts create mode 100644 config/providers/static/StaticValidationProvider.spec.ts diff --git a/api/src/resources/gram/v1/validation/validateModel.spec.ts b/api/src/resources/gram/v1/validation/validateModel.spec.ts index 361d6e37..3f2f23eb 100644 --- a/api/src/resources/gram/v1/validation/validateModel.spec.ts +++ b/api/src/resources/gram/v1/validation/validateModel.spec.ts @@ -3,16 +3,14 @@ import { jest } from "@jest/globals"; import { createTestApp } from "../../../../test-util/app.js"; import { sampleUserToken } from "../../../../test-util/sampleTokens.js"; import { ModelDataService } from "@gram/core/dist/data/models/ModelDataService.js"; -import { sampleOwnedSystem } from "../../../../test-util/sampleOwnedSystem.js"; -import Model, { Component } from "@gram/core/dist/data/models/Model.js"; +import Model from "@gram/core/dist/data/models/Model.js"; import { createSampleModel } from "../../../../test-util/model.js"; import { DataAccessLayer } from "@gram/core/dist/data/dal.js"; -import exp from "constants"; import { - ModelValidationRule, + ValidationProvider, ValidationResult, - ValidationRule, } from "@gram/core/dist/validation/ValidationHandler.js"; +import { log } from "console"; describe("validateModel", () => { let app: any; @@ -28,6 +26,16 @@ describe("validateModel", () => { getById = jest.spyOn(modelService, "getById"); }); + it("should register StaticValidationHandler", () => { + const validationProviderList = dal.validationHandler.validationProviders; + expect(validationProviderList.length).toBeGreaterThan(0); + expect( + validationProviderList.find((element: ValidationProvider) => { + return element.name === "StaticValidationProvider"; + }) + ).toBeTruthy(); + }); + it("should return 401 on un-authenticated request", async () => { const res = await request(app).get("/api/v1/validate/12323"); expect(res.status).toBe(401); @@ -106,14 +114,16 @@ describe("validateModel", () => { const res = await request(app) .get("/api/v1/validate/" + validModelId) .set("Authorization", token); + console.log("body", res.body.results); + const results = res.body.results; - console.log("empty model", { results }); expect(res.status).toBe(200); expect(results.length).not.toBe(0); expect(res.body.id).toBe(validModelId); expect(res.body.total).toBe(results.length); expect(Array.isArray(results)).toBeTruthy(); + expect( results.every((element: ValidationResult) => element.type === "model") ).toBeTruthy(); diff --git a/api/src/resources/gram/v1/validation/validateModel.ts b/api/src/resources/gram/v1/validation/validateModel.ts index 4de2c3d3..5c335d95 100644 --- a/api/src/resources/gram/v1/validation/validateModel.ts +++ b/api/src/resources/gram/v1/validation/validateModel.ts @@ -24,12 +24,9 @@ export function validateModel(dal: DataAccessLayer) { try { // Use validation service to validate the model // Return the validation results + model id - console.log( - "dal.validationHandler.validationProviders", - dal.validationHandler.validationProviders - ); const validationResults = await dal.validationHandler.validate(model); + console.log("Results from static validation", validationResults); return res.json({ id: modelId, diff --git a/api/src/test-util/testConfig.ts b/api/src/test-util/testConfig.ts index 213ab840..233375aa 100644 --- a/api/src/test-util/testConfig.ts +++ b/api/src/test-util/testConfig.ts @@ -12,6 +12,7 @@ import { testReviewerProvider } from "./sampleReviewer.js"; import { TestTeamProvider } from "./TestTeamProvider.js"; import { testUserProvider } from "./sampleUser.js"; import { testSystemProvider } from "./system.js"; +import { StaticValidationProvider } from "@gram/config/dist/providers/static/StaticValidationProvider.js"; export const testConfig: GramConfiguration = { appPort: 8080, @@ -101,6 +102,7 @@ export const testConfig: GramConfiguration = { systemProvider: testSystemProvider, suggestionSources: [], teamProvider: new TestTeamProvider(), + validationProviders: [new StaticValidationProvider()], }; }, }; diff --git a/app/src/components/login/Login.spec.js b/app/src/components/login/Login.spec.js index 0b46a36e..178ca22a 100644 --- a/app/src/components/login/Login.spec.js +++ b/app/src/components/login/Login.spec.js @@ -20,7 +20,7 @@ const wrappedRender = (login) => ); -describe.skip("Login", () => { +describe("Login", () => { it("should render successfully", () => { const login = { signInRequired: true, diff --git a/config/jest.config.ts b/config/jest.config.ts new file mode 100644 index 00000000..195b6cf8 --- /dev/null +++ b/config/jest.config.ts @@ -0,0 +1,23 @@ +import type { Config } from "@jest/types"; + +const config: Config.InitialOptions = { + preset: "ts-jest/presets/default-esm", + extensionsToTreatAsEsm: [".ts"], + testEnvironment: "node", + verbose: true, + transform: { + "^.+\\.tsx?$": [ + "ts-jest", + { + tsconfig: "./tsconfig.json", + useESM: true, + }, + ], + }, + moduleNameMapper: { + "^(\\.{1,2}/.*)\\.js$": "$1", + }, + coverageProvider: "v8", + transformIgnorePatterns: ["/node_modules/"], +}; +export default config; diff --git a/config/package.json b/config/package.json index cd825360..cd4b9c69 100644 --- a/config/package.json +++ b/config/package.json @@ -9,7 +9,8 @@ "scripts": { "build": "tsc -p tsconfig.build.json", "lint": "prettier --check .", - "lint-fix": "prettier -l . --write" + "lint-fix": "prettier -l . --write", + "test": "NODE_OPTIONS=--experimental-vm-modules NODE_ENV=test jest --detectOpenHandles --no-cache --runInBand --logHeapUsage --forceExit" }, "repository": { "type": "git", @@ -43,4 +44,4 @@ "ts-node": "^10.9.1", "tsconfig-paths": "^4.1.2" } -} +} \ No newline at end of file diff --git a/config/providers/static/StaticValidationProvider.spec.ts b/config/providers/static/StaticValidationProvider.spec.ts new file mode 100644 index 00000000..1b95618c --- /dev/null +++ b/config/providers/static/StaticValidationProvider.spec.ts @@ -0,0 +1,176 @@ +import Model from "@gram/core/dist/data/models/Model.js"; +import { StaticValidationProvider } from "./StaticValidationProvider.js"; +import { ValidationProvider } from "@gram/core/dist/validation/ValidationHandler.js"; +describe("StaticValidationProvider", () => { + let staticValidationProvider: ValidationProvider; + + beforeAll(async () => { + staticValidationProvider = new StaticValidationProvider(); + }); + + it("should return an array", async () => { + const model = new Model("some-system-id", "some-version", "some-owner"); + const result = await staticValidationProvider.validate(model); + expect(Array.isArray(result)).toBe(true); + }); + + it("should return one result for an empty model", async () => { + const model = new Model("some-system-id", "some-version", "some-owner"); + const resultList = await staticValidationProvider.validate(model); + expect(resultList.length).toBe(1); + const result = resultList[0]; + expect(result.type).toBe("model"); + expect(result.ruleName).toBe("should have at least one component"); + expect(result.testResult).toBe(false); + expect(result.message).toBe("Model is empty"); + }); + + it("should have validation results for each component", async () => { + const model = new Model("some-system-id", "some-version", "some-owner"); + model.data = { + dataFlows: [], + components: [ + { + x: 338.046875, + y: 450, + id: "e8edd886-84fa-4c2b-aef9-b2724eab08c8", + name: "Process", + type: "proc", + }, + { + x: 736.046875, + y: 299, + id: "b66e03fd-36dc-4813-9d6b-2af4eb35a66e", + name: "External entity", + type: "ee", + }, + { + x: 755.046875, + y: 613, + id: "e7bdc6a9-169a-40fc-8831-543db611ff6a", + name: "Data Store", + type: "ds", + }, + { + x: 383.046875, + y: 804, + id: "1645da1a-142b-4567-a2af-1e6ea76181b0", + name: "Trust Boundary", + type: "tb", + width: 300, + height: 150, + }, + ], + }; + + const resultList = await staticValidationProvider.validate(model); + expect(resultList.length).toBeGreaterThan(4); + expect( + resultList.some( + (result) => + result.type === "component" && + result.elementId === "e8edd886-84fa-4c2b-aef9-b2724eab08c8" + ) + ).toBe(true); + expect( + resultList.some( + (result) => + result.type === "component" && + result.elementId === "b66e03fd-36dc-4813-9d6b-2af4eb35a66e" + ) + ).toBe(true); + expect( + resultList.some( + (result) => + result.type === "component" && + result.elementId === "e7bdc6a9-169a-40fc-8831-543db611ff6a" + ) + ).toBe(true); + expect( + resultList.some( + (result) => + result.type === "component" && + result.elementId === "1645da1a-142b-4567-a2af-1e6ea76181b0" + ) + ).toBe(true); + }); + + describe("StaticValidationProvider: validation rules", () => { + it("should have all testResult true for a valid component", async () => { + const model = new Model("some-system-id", "some-version", "some-owner"); + model.data = { + dataFlows: [ + { + id: "6092a7a7-4693-4352-9229-68793464fde4", + points: [338.046875, 450, 651.046875, 655], + endComponent: { + id: "b66e03fd-36dc-4813-9d6b-2af4eb35a66e", + }, + startComponent: { + id: "e8edd886-84fa-4c2b-aef9-b2724eab08c8", + }, + bidirectional: false, + }, + ], + components: [ + { + x: 338.046875, + y: 450, + id: "e8edd886-84fa-4c2b-aef9-b2724eab08c8", + name: "Process", + type: "proc", + classes: [ + { + id: "bb3a425c-f89c-4b2e-a375-5437e1140b89", + icon: "/assets/klarna/klarna.png", + name: "Klarna", + componentType: "any", + }, + ], + description: + "This is an amazing description, it describes the process in detail and why it is important", + }, + { + x: 649.046875, + y: 598, + id: "b66e03fd-36dc-4813-9d6b-2af4eb35a66e", + name: "External entity", + type: "ee", + }, + ], + }; + + const resultList = await staticValidationProvider.validate(model); + const validComponentResults = resultList.filter( + (result) => result.elementId === "e8edd886-84fa-4c2b-aef9-b2724eab08c8" + ); + + expect( + validComponentResults.every((result) => result.testResult === true) + ).toBe(true); + }); + + it("should have all testResult false for invalid component", async () => { + const model = new Model("some-system-id", "some-version", "some-owner"); + model.data = { + dataFlows: [], + components: [ + { + x: 649.046875, + y: 598, + id: "b66e03fd-36dc-4813-9d6b-2af4eb35a66e", + name: "", + type: "ee", + }, + ], + }; + const result = await staticValidationProvider.validate(model); + const invalidComponentResults = result.filter( + (result) => result.elementId === "b66e03fd-36dc-4813-9d6b-2af4eb35a66e" + ); + expect( + invalidComponentResults.every((result) => result.testResult === false) + ).toBe(true); + }); + }); +}); diff --git a/config/providers/static/StaticValidationProvider.ts b/config/providers/static/StaticValidationProvider.ts index 07f672c1..e469aa48 100644 --- a/config/providers/static/StaticValidationProvider.ts +++ b/config/providers/static/StaticValidationProvider.ts @@ -6,9 +6,6 @@ import { ValidationResult, ValidationRule, } from "@gram/core/dist/validation/ValidationHandler.js"; -import log4js from "log4js"; - -const log = log4js.getLogger("staticValidationProvider"); const validationRules: ValidationRule[] = [ { @@ -34,10 +31,10 @@ const validationRules: ValidationRule[] = [ affectedType: ["proc", "ee", "ds", "tf"], conditionalRules: [["should have a description", true]], test: (component, _) => - component.description ? component.description.length > 100 : false, + component.description ? component.description.length > 50 : false, messageTrue: "Component has a long enough description", messageFalse: - "Component's description should be at least 100, to be descriptive enough", + "Component's description should be at least 50, to be descriptive enough", }, { type: "component", @@ -85,8 +82,8 @@ function isModelValidation(rule: ValidationRule): rule is ModelValidationRule { return rule.type === "model"; } export class StaticValidationProvider implements ValidationProvider { + name: string = "StaticValidationProvider"; async validate(model: Model): Promise { - log.info("StaticValidationProvider is called"); if (!model) { return [ { diff --git a/core/src/test-util/testConfig.ts b/core/src/test-util/testConfig.ts index 588520a8..130aecf9 100644 --- a/core/src/test-util/testConfig.ts +++ b/core/src/test-util/testConfig.ts @@ -86,6 +86,7 @@ export const testConfig: GramConfiguration = { systemProvider: new DummySystemProvider(), suggestionSources: [], actionItemExporters: [new DummyActionItemExporter()], + validationProviders: [], }; }, }; diff --git a/core/src/validation/ValidationHandler.ts b/core/src/validation/ValidationHandler.ts index d7708454..e03b5964 100644 --- a/core/src/validation/ValidationHandler.ts +++ b/core/src/validation/ValidationHandler.ts @@ -9,6 +9,7 @@ export interface ValidationResult { } export interface ValidationProvider { + name: string; validate(model: Model): Promise; } @@ -49,9 +50,9 @@ export class ValidationHandler { return [ ...(await Promise.all( this.validationProviders.map( - async (provider): Promise => ({ + async (provider): Promise => [ ...(await provider.validate(model)), - }) + ] ) )), ].flat(); From fed2afeaff5da9becc6a22993b37ecd92366e31e Mon Sep 17 00:00:00 2001 From: Pauline Didier Date: Fri, 6 Sep 2024 17:15:04 +0200 Subject: [PATCH 3/4] style: fix linting --- config/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/package.json b/config/package.json index cd4b9c69..932b4352 100644 --- a/config/package.json +++ b/config/package.json @@ -44,4 +44,4 @@ "ts-node": "^10.9.1", "tsconfig-paths": "^4.1.2" } -} \ No newline at end of file +} From 30f9df2df3a35290e85b4dc76e60f40863fd004b Mon Sep 17 00:00:00 2001 From: Pauline Didier Date: Mon, 9 Sep 2024 10:46:52 +0200 Subject: [PATCH 4/4] test: skip login tests --- app/src/components/login/Login.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/components/login/Login.spec.js b/app/src/components/login/Login.spec.js index 178ca22a..0b46a36e 100644 --- a/app/src/components/login/Login.spec.js +++ b/app/src/components/login/Login.spec.js @@ -20,7 +20,7 @@ const wrappedRender = (login) => ); -describe("Login", () => { +describe.skip("Login", () => { it("should render successfully", () => { const login = { signInRequired: true,