Skip to content

Commit

Permalink
Merge pull request #117 from klarna-incubator/validation-endpoint
Browse files Browse the repository at this point in the history
feature: add validation endpoint
  • Loading branch information
Tyouxik authored Sep 9, 2024
2 parents b5d73a8 + 30f9df2 commit 855ea53
Show file tree
Hide file tree
Showing 19 changed files with 673 additions and 7 deletions.
4 changes: 2 additions & 2 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -68,4 +68,4 @@
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.1.2"
}
}
}
4 changes: 4 additions & 0 deletions api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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",
Expand Down
1 change: 0 additions & 1 deletion api/src/resources/gram/v1/token/delete.spec.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
11 changes: 11 additions & 0 deletions api/src/resources/gram/v1/validation/router.ts
Original file line number Diff line number Diff line change
@@ -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;
}
193 changes: 193 additions & 0 deletions api/src/resources/gram/v1/validation/validateModel.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
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 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 {
ValidationProvider,
ValidationResult,
} from "@gram/core/dist/validation/ValidationHandler.js";
import { log } from "console";

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 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);
});
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);
console.log("body", res.body.results);

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.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();
});
});
40 changes: 40 additions & 0 deletions api/src/resources/gram/v1/validation/validateModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* 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

const validationResults = await dal.validationHandler.validate(model);
console.log("Results from static validation", validationResults);

return res.json({
id: modelId,
total: validationResults.length,
results: validationResults,
});
} catch (error) {
return res.sendStatus(400);
}
};
}
7 changes: 4 additions & 3 deletions api/src/test-util/model.ts
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 2 additions & 0 deletions api/src/test-util/testConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -101,6 +102,7 @@ export const testConfig: GramConfiguration = {
systemProvider: testSystemProvider,
suggestionSources: [],
teamProvider: new TestTeamProvider(),
validationProviders: [new StaticValidationProvider()],
};
},
};
3 changes: 3 additions & 0 deletions config/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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: [
Expand Down Expand Up @@ -220,6 +222,7 @@ export const defaultConfig: GramConfiguration = {
teamProvider, // completely optional
dal.modelService,
],
validationProviders: [staticValidationProvider],
};
},
};
23 changes: 23 additions & 0 deletions config/jest.config.ts
Original file line number Diff line number Diff line change
@@ -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: ["<rootDir>/node_modules/"],
};
export default config;
3 changes: 2 additions & 1 deletion config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading

0 comments on commit 855ea53

Please sign in to comment.