diff --git a/src/cli-mode.ts b/src/cli-mode.ts index 5a83cfc1..5bafe4aa 100644 --- a/src/cli-mode.ts +++ b/src/cli-mode.ts @@ -8,6 +8,7 @@ import {RapLPCustomSpectral} from "./util/RapLPCustomSpectral.ts"; import {DiagnosticReport, RapLPDiagnostic} from "./util/RapLPDiagnostic.ts"; import {AggregateError} from "./util/RapLPCustomErrorInfo.ts"; import chalk from 'chalk'; +import { validateYamlInput } from "./util/baseUtil.ts" import { ExcelReportProcessor } from "./util/excelReportProcessor.ts"; declare var AggregateError: { @@ -35,7 +36,31 @@ export async function execCLI(argv: T) { const logErrorFilePath = argv.logError as string | undefined; const logDiagnosticFilePath = argv.logDiagnostic as string | undefined; try { - + const fileContent = fs.readFileSync(apiSpecFileName, "utf-8"); + try { + validateYamlInput(fileContent); + } catch (error) { + if (error instanceof Error) { + const cause = error.cause; + switch (cause) { + case 'INVALID_YAML': + console.error('Validation error: The YAML content is invalid or empty. Please provide a valid YAML file.'); + break; + case 'MISSING_KEYS': + console.error(`Validation error: ${error.message}. Ensure the file includes required keys like 'openapi', 'info', and 'paths'.`); + break; + case 'SYNTAX_ERROR': + console.error(`Validation error: There is a syntax error in your YAML file. ${error.message}`); + break; + default: + console.error(`Unexpected validation error: ${error.message}`); + } + return; + } else { + console.error('Unknown validation error occurred.'); + return; + } + } // Import and create rule instances in RAP-LP const enabledRulesAndCategorys = await importAndCreateRuleInstances(ruleCategories); // Load API specification into a Document object @@ -153,6 +178,7 @@ export async function execCLI(argv: T) { } } catch (initializingError: any) { logErrorToFile(initializingError); + // console.error(chalk.red(initializingError)); console.error(chalk.red("Ett fel uppstod vid inläsning av moduler och skapande av regelklasser! Undersök felloggen för RAP-LP för mer information om felet")); } } catch (error: any) { diff --git a/src/routes/urlValidation.ts b/src/routes/urlValidation.ts index 3edbc219..583ed630 100644 --- a/src/routes/urlValidation.ts +++ b/src/routes/urlValidation.ts @@ -1,7 +1,7 @@ import { Document } from "@stoplight/spectral-core" import Parsers from "@stoplight/spectral-parsers" import { Express } from 'express' -import { processApiSpec, validateYamlInput } from "../util/apiUtil.ts" +import { processApiSpec, validateYamlInput } from "../util/baseUtil.ts" import { UrlContentDto } from "../model/UrlContentDto.ts" import { importAndCreateRuleInstances } from "../util/ruleUtil.ts" import { ERROR_TYPE, RapLPBaseApiError } from "../util/RapLPBaseApiErrorHandling.ts" diff --git a/src/routes/validate.ts b/src/routes/validate.ts index 3954ba28..cba251fb 100644 --- a/src/routes/validate.ts +++ b/src/routes/validate.ts @@ -1,28 +1,52 @@ -import { Document } from "@stoplight/spectral-core"; -import Parsers from "@stoplight/spectral-parsers"; -import { Express } from "express"; -import { - decodeBase64String, - processApiSpec, - validateYamlInput, -} from "../util/apiUtil.ts"; -import { YamlContentDto } from "../model/YamlContentDto.ts"; -import { importAndCreateRuleInstances } from "../util/ruleUtil.ts"; -import { ApiInfo } from "../model/ApiInfo.ts"; +import { Document } from "@stoplight/spectral-core" +import Parsers from "@stoplight/spectral-parsers" +import { Express } from 'express' +import { decodeBase64String, processApiSpec, validateYamlInput } from "../util/baseUtil.ts" +import { YamlContentDto } from "../model/YamlContentDto.ts" +import { importAndCreateRuleInstances } from "../util/ruleUtil.ts" +import { ApiInfo } from "../model/ApiInfo.ts" import { validationRules } from "../model/validationRules.ts"; +import { ERROR_TYPE, RapLPBaseApiError } from "../util/RapLPBaseApiErrorHandling.ts"; + import { ExcelReportProcessor } from "../util/excelReportProcessor.ts" -import {DiagnosticReport } from "../util/RapLPDiagnostic.ts"; export const registerValidationRoutes = (app: Express) => { - // Route for raw content upload. app.post("/api/v1/validation/validate", async (req, res, next) => { try { const yamlContent: YamlContentDto = req.body; - let yamlContentString: string; - yamlContentString = decodeBase64String(yamlContent.yaml); - - validateYamlInput(yamlContentString); + let yamlContentString: string; + yamlContentString = decodeBase64String(yamlContent.yaml) + try { + validateYamlInput(yamlContentString); + } catch (error) { + if (error instanceof Error) { + const cause = error.cause; + switch (cause) { + case 'INVALID_YAML': + throw new RapLPBaseApiError( + "Could not validate Yaml", + `${error}`, + ERROR_TYPE.BAD_REQUEST); + case 'MISSING_KEYS': + throw new RapLPBaseApiError( + "Missing required top-level keys", + `${error}`, + ERROR_TYPE.BAD_REQUEST); + case 'SYNTAX_ERROR': + throw new RapLPBaseApiError( + "Could not validate Yaml", + `${error}`, + ERROR_TYPE.BAD_REQUEST); + default: + console.error(`Unexpected error: ${error.message}`); + } + return; + } else { + console.error('Unknown error occurred.'); + return; + } + } const apiSpecDocument = new Document(yamlContentString, Parsers.Yaml, ""); diff --git a/src/util/apiUtil.ts b/src/util/baseUtil.ts similarity index 64% rename from src/util/apiUtil.ts rename to src/util/baseUtil.ts index 947f1530..41bd8fa8 100644 --- a/src/util/apiUtil.ts +++ b/src/util/baseUtil.ts @@ -1,26 +1,37 @@ import { RapLPCustomSpectral } from "./RapLPCustomSpectral.ts"; import { Document } from "@stoplight/spectral-core"; import Parsers from "@stoplight/spectral-parsers"; -import { ERROR_TYPE, RapLPBaseApiError } from "./RapLPBaseApiErrorHandling.ts"; import { DiagnosticReport, RapLPDiagnostic } from "../util/RapLPDiagnostic.ts"; import yaml from "js-yaml"; import { ValidationResponseDto } from "../model/ValidationResponseDto.ts"; export const validateYamlInput = (input: string): input is string => { - try { - //Parse the yaml to verify - yaml.load(input); - } catch (e) { - // Handle YAML parsing error - throw new RapLPBaseApiError( - "Could not validate Yaml", - "Invalid YAML", - ERROR_TYPE.BAD_REQUEST - ); - } + try { + const parsed = yaml.load(input); - return true; -}; + if (typeof parsed !== 'object' || parsed === null) { + throw new Error('Parsed YAML is invalid or empty.', { cause: 'INVALID_YAML'}); + } + + const requiredKeys = ['openapi', 'info', 'paths']; + const missingKeys = requiredKeys.filter(key => !(key in parsed)); + + if (missingKeys.length > 0) { + throw new Error(`Missing required top-level keys: ${missingKeys.join(', ')}`, { cause: 'MISSING_KEYS'}); + + } + } catch (error) { + if (error instanceof yaml.YAMLException) { + throw new Error( `YAML Syntax Error: ${error.message}`, { cause: 'SYNTAX_ERROR'}); + } else if (error instanceof Error) { + throw error; + } else { + throw new Error(`Could not vaildate yaml: ${error}`) + } + } + + return true; +} export function decodeBase64String(base64YamlFile: string) { // Import the necessary Node.js module (Buffer is built-in) @@ -62,9 +73,7 @@ export async function processApiSpec( * @param prop Property to check for * @returns Boolean */ -export function hasOwnProperty( - obj: X, - prop: Y -): obj is X & Record { - return obj.hasOwnProperty(prop); +export function hasOwnProperty + (obj: X, prop: Y): obj is X & Record { + return obj.hasOwnProperty(prop) } diff --git a/tests/integration/api.test.ts b/tests/integration/api.test.ts index 7e2e8266..50e8013f 100644 --- a/tests/integration/api.test.ts +++ b/tests/integration/api.test.ts @@ -23,7 +23,12 @@ describe("API Test", () => { var app: any beforeAll(async () => { - app = await startServer(); + const mockArgs = { + enableUrlValidation: false, + urlValidationConfigFile: 'custom-config.json', + }; + + app = await startServer(mockArgs); api = new ValidateApi(new Configuration({ basePath: "http://localhost:3000/api/v1" })) }) @@ -40,12 +45,17 @@ describe("API Test", () => { it("Assert that the error handler is intercepting faults", async () => { const data = readFileSync(path.resolve(__dirname, "../../apis/dok-api.yaml")) + const missingTopLevelKeysYaml = ` + name: Jane Smith + age: 25 + occupation: Designer + `; const response = await request(app) .post("/api/v1/validation/validate") .set('Content-Type', 'application/json') .send({ - yaml: data.toString("base64") + "123qwdsdfgaerg39e4rg", + yaml: Buffer.from(missingTopLevelKeysYaml).toString('base64'), categories: [] }) @@ -53,9 +63,9 @@ describe("API Test", () => { expect(response.status).toBe(400) expect(response.body).toMatchObject({ type: "about:blank", - title: "Could not validate Yaml", + title: "Missing required top-level keys", status: 400, - detail: "Invalid YAML", + detail: "Missing required top-level keys: openapi, info, paths", instance: "/api/v1/validation/validate" }) diff --git a/tests/unit/api/baseUtil.test.ts b/tests/unit/api/baseUtil.test.ts new file mode 100644 index 00000000..8057bf97 --- /dev/null +++ b/tests/unit/api/baseUtil.test.ts @@ -0,0 +1,82 @@ + +import { validateYamlInput } from "../../../src/util/baseUtil.ts" +import { RapLPBaseApiError } from "../../../src/util/RapLPBaseApiErrorHandling.ts"; + +describe('validateYamlInput', () => { + test('should return true for valid YAML string with all top-level keys', () => { + const validYaml = ` + openapi: 3.0.3 + info: some info + paths: + `; + + const result = validateYamlInput(validYaml); + expect(result).toBe(true); + }); + + test('should throw error for missing top-level keys', () => { + const missingTopLevelKeysYaml = ` + name: Jane Smith + age: 25 + occupation: Designer + `; + + try { + validateYamlInput(missingTopLevelKeysYaml); + } catch (error) { + if (error instanceof RapLPBaseApiError) { + expect(error.title).toBe("Missing required top-level keys"); + expect(error.message).toContain("Missing required top-level keys: openapi, info, paths"); + } + } + }); + + + test('should throw error for invalid YAML string', () => { + // Invalid YAML: Missing colon after 'age' + const invalidYaml = ` + name: Jane Smith + age 25 + occupation: Designer + `; + + try { + validateYamlInput(invalidYaml); + } catch (error) { + if (error instanceof RapLPBaseApiError) { + expect(error.title).toBe("Could not validate Yaml"); + expect(error.message).toContain("YAML Syntax Error:"); + } + } + + }); + + test('should throw error for malformed YAML', () => { + // Completely malformed YAML + const malformedYaml = `Just + some + random: text: that is not YAML.`; + + try { + validateYamlInput(malformedYaml); + } catch (error) { + if (error instanceof RapLPBaseApiError) { + expect(error.title).toBe("Could not validate Yaml"); + expect(error.message).toContain("YAML Syntax Error:"); + } + } + }); + + test('should throw error for empty string', () => { + const emptyYaml = ""; + + try { + validateYamlInput(emptyYaml); + } catch (error) { + if (error instanceof RapLPBaseApiError) { + expect(error.title).toBe("Could not validate Yaml"); + expect(error.message).toContain("Parsed YAML is not a valid object"); + } + } + }); +});