From 6d882cff3af7e7b6aa9ed3bfa14b8dc62d165fea Mon Sep 17 00:00:00 2001 From: "Gregor \"hrax\" Magdolen" Date: Sun, 25 Aug 2024 21:11:51 +0200 Subject: [PATCH] More tests --- spec/linter/Linter.spec.ts | 148 ++++++++++++++++++++---------- spec/linter/UpdateXMLScan.spec.ts | 68 ++++++++------ src/core/ProfileManager.ts | 6 +- src/linter/Linter.ts | 51 ++++++---- src/linter/UpdateXMLScan.ts | 43 +++++++-- 5 files changed, 211 insertions(+), 105 deletions(-) diff --git a/spec/linter/Linter.spec.ts b/spec/linter/Linter.spec.ts index 1a921ff..01f8f94 100644 --- a/spec/linter/Linter.spec.ts +++ b/spec/linter/Linter.spec.ts @@ -1,95 +1,145 @@ +/* eslint-disable camelcase */ import fs from "fs"; import { UpdateXMLScan } from "../../src/linter/UpdateXMLScan.js"; -import { Linter } from "../../src/linter/Linter.js"; -import { Profile } from "../../src/core/ProfileManager.js"; +import { Linter, LinterOptions } from "../../src/linter/Linter.js"; +import profileManager, { Profile } from "../../src/core/ProfileManager.js"; +import { SNUpdateXMLData } from "../../src/core/sn.js"; +import { RESTClient } from "../../src/core/RESTClient.js"; +import oauthClient from "../../src/core/OAuthClient.js"; +import { resetAllWhenMocks, when } from "jest-when"; +import { defaultWhenImplementationThrow } from "../helpers.js"; +import { ESLint } from "eslint"; describe("LinterSpec", () => { - /* beforeEach(() => { - payload = fs.readFileSync("./spec/payloads/sys_script_include_50ba882f07d610108110f2ae7c1ed00d.xml", {encoding: "utf8"}); - data = { - "name": "sys_script_include_50ba882f07d610108110f2ae7c1ed00d", - "sys_id": "50ba882f07d610108110f2ae7c1ed00d", - "action": "INSERT_OR_UPDATE", - "sys_created_by": "admin", - "sys_created_on": "1970-01-01 00:00:01", - "sys_updated_by": "admin", - "sys_updated_on": "1970-01-01 00:00:01", - "type": "Script Include", - "target_name": "sys_script_include_50ba882f07d610108110f2ae7c1ed00d", - "update_set": "1234", - "payload": payload - }; - }); */ + const payload = fs.readFileSync("./spec/payloads/sys_script_include_50ba882f07d610108110f2ae7c1ed00d.xml", {encoding: "utf8"}); + const data: SNUpdateXMLData = { + "name": "sys_script_include_50ba882f07d610108110f2ae7c1ed00d", + "sys_id": "50ba882f07d610108110f2ae7c1ed00d", + "action": "INSERT_OR_UPDATE", + "sys_created_by": "admin", + "sys_created_on": "1970-01-01 00:00:01", + "sys_updated_by": "admin", + "sys_updated_on": "1970-01-01 00:00:01", + "type": "Script Include", + "target_name": "sys_script_include_50ba882f07d610108110f2ae7c1ed00d", + "update_set": "1234", + "payload": payload, + "application": "global", + "payloadHash": -1234, + "sys_mod_count": 1 + }; + + let profile: Profile, client: RESTClient; + + beforeEach(() => { + resetAllWhenMocks(); + profile = profileManager.fromData({ + name: "dev1", + baseUrl: "https://example.com", + auth: { + clientID: "123", + clientSecret: "123", + lastRetrieved: 0, + type: "oauth-token", + token: { + access_token: "123", + refresh_token: "123", + expires_in: 60, + scope: "", + token_type: "Bearer" + } + } + }); + client = new RESTClient(oauthClient); + }); - it.skip("Fetch and prepare NowUpdateXML records", async() => { - const options = { + it("should fetch and prepare UpdateXML records", async() => { + const options: LinterOptions = { "title": "Test Report", "query": "name=Default" }; - const linter = {}; - // spyOn(linter.instance, "requestUpdateXMLByUpdateSetQuery").and.returnValue({result: [data]}); + const linter = new Linter(profile, client, options); + const clientUpdateXMLSpy = jest.spyOn(client, "loadUpdateXMLByUpdateSetQuery"); + when(clientUpdateXMLSpy) + .defaultImplementation(defaultWhenImplementationThrow) + .calledWith(profile, options.query).mockResolvedValue([data]); await linter.fetch(); const changes = linter.changes; - // expect(linter.instance.requestUpdateXMLByUpdateSetQuery).toHaveBeenCalledWith(options.query); - // expect(linter.instance.requestUpdateXMLByUpdateSetQuery).toHaveBeenCalledTimes(1); - // expect(changes.size).toBe(1); - // expect(changes.values().next().value.id).toBe(data.sys_id); + expect(clientUpdateXMLSpy).toHaveBeenCalledTimes(1); + expect(changes.size).toBe(1); + expect((changes.values().next().value as UpdateXMLScan).ID).toBe(data.sys_id); }); - it.skip("Lint NowUpdateXML records", async () => { + it("Lint NowUpdateXML records", async() => { + profile.setTableConfiguration({ + tables: { + "sys_script_include": { + name: "sys_script_include", + fields: { + "script": { + name: "script" + } + } + } + } + }); const options = { "title": "Test Report", "query": "name=Default" }; - const linter = {}; + const linter = new Linter(profile, client, options); + const clientUpdateXMLSpy = jest.spyOn(client, "loadUpdateXMLByUpdateSetQuery"); + when(clientUpdateXMLSpy) + .defaultImplementation(defaultWhenImplementationThrow) + .calledWith(profile, options.query).mockResolvedValue([data]); - // Not testing NowLoader - // spyOn(linter.instance, "requestUpdateXMLByUpdateSetQuery").and.returnValue({result: [data]}); - // Not testing ESLint - // spyOn(linter.eslint, "lintText").and.returnValue([{ - // warningCount: 0, - // errorCount: 0 - // }]); + const lintTextSpy = jest.spyOn(linter.getESLint(), "lintText").mockResolvedValue([ + { + warningCount: 0, + errorCount: 0 + } as ESLint.LintResult + ]); await linter.process(); const changes = linter.changes; - // expect(linter.instance.requestUpdateXMLByUpdateSetQuery).toHaveBeenCalledTimes(1); - // expect(linter.eslint.lintText).toHaveBeenCalledTimes(1); + expect(clientUpdateXMLSpy).toHaveBeenCalledTimes(1); + expect(lintTextSpy).toHaveBeenCalledTimes(1); expect(changes.size).toBe(1); - expect(changes.values().next().value.status).toBe("OK"); - console.log("SIZE " + changes.values().next().value.reports.size); - expect(changes.values().next().value.reports.keys().next().value).toBe("script"); + expect(changes.values().next().value.getStatus()).toBe("OK"); + expect(changes.values().next().value.getReports().keys().next().value).toBe("script"); }); - it.skip("Ignore not configured tables", async () => { + it("Ignore not configured tables", async() => { const options = { "title": "Test Report", "query": "name=Default" }; - const linter = {}; + const linter = new Linter(profile, client, options); // Not testing NowLoader - // spyOn(linter.instance, "requestUpdateXMLByUpdateSetQuery").and.returnValue({result: [data]}); + const clientUpdateXMLSpy = jest.spyOn(client, "loadUpdateXMLByUpdateSetQuery"); + when(clientUpdateXMLSpy) + .defaultImplementation(defaultWhenImplementationThrow) + .calledWith(profile, options.query).mockResolvedValue([data]); - // Not testing ESLint - // spyOn(linter.eslint, "lintText").and.callThrough(); + const lintTextSpy = jest.spyOn(linter.getESLint(), "lintText"); await linter.process(); const changes = linter.changes; - // expect(linter.instance.requestUpdateXMLByUpdateSetQuery).toHaveBeenCalledTimes(1); - // expect(linter.eslint.lintText).not.toHaveBeenCalled(); + expect(clientUpdateXMLSpy).toHaveBeenCalledTimes(1); + expect(lintTextSpy).not.toHaveBeenCalled(); expect(changes.size).toBe(1); - // expect(changes.values().next().value.status).toBe(UpdateXMLScan.STATUS.IGNORED); - expect(changes.values().next().value.hasReports).toBe(false); + expect(changes.values().next().value.getStatus()).toBe("IGNORED"); + expect(changes.values().next().value.hasReports()).toBe(false); }); it.skip("Skip on empty field", async () => { diff --git a/spec/linter/UpdateXMLScan.spec.ts b/spec/linter/UpdateXMLScan.spec.ts index d2d4df8..2107b2b 100644 --- a/spec/linter/UpdateXMLScan.spec.ts +++ b/spec/linter/UpdateXMLScan.spec.ts @@ -1,41 +1,55 @@ -/* eslint-disable camelcase */ +/* eslint-disable camelcase, no-magic-numbers */ +import { ESLint } from "eslint"; import { SNUpdateXMLData } from "../../src/core/sn.js"; import { UpdateXMLScan } from "../../src/linter/UpdateXMLScan.js"; -describe("NowUpdateXMLScan", () => { - it("should return correct error/warning count for all reports", () => { - const data: SNUpdateXMLData = { - name: "name", - sys_id: "sysId", - action: "INSERT_OR_UPDATE", - type: "sysId", - target_name: "sysId", - update_set: "sysId", - payload: "sysId", - sys_created_by: "sysId", - sys_created_on: "sysId", - sys_updated_by: "sysId", - sys_updated_on: "sysId", - application: "global", - payloadHash: -1234, - sys_mod_count: 0 - }; - +describe("UpdateXMLScan", () => { + const data: SNUpdateXMLData = { + name: "name", + sys_id: "sysId", + action: "INSERT_OR_UPDATE", + type: "sysId", + target_name: "sysId", + update_set: "sysId", + payload: "sysId", + sys_created_by: "sysId", + sys_created_on: "sysId", + sys_updated_by: "sysId", + sys_updated_on: "sysId", + application: "global", + payloadHash: -1234, + sys_mod_count: 0 + }; + + it("should return correct error count and status for all reports", () => { const scan = new UpdateXMLScan(data); - scan.reports.set("script", { + scan.setReport("script", { warningCount: 1, errorCount: 2 - }); - scan.reports.set("condition", { + } as ESLint.LintResult); + scan.setReport("condition", { warningCount: 1, errorCount: 0 - }); - + } as ESLint.LintResult); - expect(scan.getReportsWarningCount()).toBe(2); expect(scan.getReportsErrorCount()).toBe(2); - expect(scan.hasReportsWarnings()).toBe(true); expect(scan.hasReportsErrors()).toBe(true); expect(scan.getStatus()).toBe("ERROR"); }); + + it("should return correct warning count and status for all reports", () => { + const scan = new UpdateXMLScan(data); + scan.setReport("script", { + warningCount: 1, + errorCount: 0 + } as ESLint.LintResult); + scan.setReport("condition", { + warningCount: 3, + errorCount: 0 + } as ESLint.LintResult); + + expect(scan.getReportsWarningCount()).toBe(4); + expect(scan.hasReportsWarnings()).toBe(true); + expect(scan.getStatus()).toBe("WARNING"); + }); }); \ No newline at end of file diff --git a/src/core/ProfileManager.ts b/src/core/ProfileManager.ts index dc8ae58..4a7ff51 100644 --- a/src/core/ProfileManager.ts +++ b/src/core/ProfileManager.ts @@ -183,10 +183,14 @@ export class Profile { return this.tables; } + setTableConfiguration(config: TableConfig) { + this.tables = config; + } + // TODO: better name, load all other profile files/table setup async fetchTableConfiguration(): Promise { if (this.client != null) { - this.tables = await this.client.getTableConfiguration(this); + this.setTableConfiguration(await this.client.getTableConfiguration(this)); } } } diff --git a/src/linter/Linter.ts b/src/linter/Linter.ts index dba9d07..2c3eb81 100644 --- a/src/linter/Linter.ts +++ b/src/linter/Linter.ts @@ -4,7 +4,7 @@ import { DOMParser } from "@xmldom/xmldom"; import { UpdateXMLScan } from "./UpdateXMLScan.js"; import AbstractReportGenerator from "../generator/AbstractReportGenerator.js"; -import { Profile } from "../core/ProfileManager.js"; +import profileManager, { Profile } from "../core/ProfileManager.js"; import { RESTClient } from "../core/RESTClient.js"; import * as xmlhelpers from "../util/xmlhelpers.js"; import { SNField } from "../core/sn.js"; @@ -14,14 +14,21 @@ export interface LinterOptions { query: string; }; +export type LinterMetricType = "byStatus" | "totalChanges" | "uniqueChanges" | "totalUpdateSets"; + export class Linter { + static readonly METRIC_BY_STATUS: LinterMetricType = "byStatus"; + static readonly METRIC_TOTAL_CHANGES: LinterMetricType = "totalChanges"; + static readonly METRIC_UNIQUE_CHANGES: LinterMetricType = "uniqueChanges"; + static readonly METRIC_TOTAL_UPDATE_SETS: LinterMetricType = "totalUpdateSets"; + private profile: Profile; private options: LinterOptions; private client: RESTClient; private eslint: ESLint = new ESLint(); - changes: Map = new Map(); - metrics: Map = new Map(); + changes: Map = new Map(); + metrics: Map = new Map(); /** * @@ -34,6 +41,10 @@ export class Linter { this.options = options; } + getESLint(): ESLint { + return this.eslint; + } + /** * Fetch update set changes from the instance. Resets loaded changes & metrics on each call! * @returns {void} @@ -68,19 +79,19 @@ export class Linter { // Check the changes against configured lint tables this.changes.forEach((scan, name) => { - if (scan.status() !== "SCAN") { + if (scan.getStatus() !== "SCAN") { return; } const table = scan.targetTable; - if (config!.tables[table] == null) { - scan.ignore(); + if (config?.tables[table] == null) { + scan.setIgnore(); return; } - const fields = config!.tables[table].fields || null; + const fields = config?.tables[table].fields ?? null; if (fields == null || Object.keys(fields).length === 0) { - scan.manual(); + scan.setManual(); return; } @@ -101,7 +112,7 @@ export class Linter { Object.values(fields).forEach(async (field: SNField) => { const data = xmlhelpers.parsePayloadTableFieldValue(table, field.name, document); if (data == null || data === "") { - scan.skip(); + scan.setSkip(); return; } @@ -114,33 +125,33 @@ export class Linter { const report = await this.eslint.lintText(data); if (report.length) { report[0].filePath = scan.reportPathForField(field.name); - scan.reports.set(field.name, report[0]); + scan.setReport(field.name, report[0]); } }); }); // Metrics - this.metrics.set("byStatus", {}); - this.metrics.set("totalChanges", 0); - this.metrics.set("uniqueChanges", 0); - this.metrics.set("totalUpdateSets", 0); + this.metrics.set(Linter.METRIC_BY_STATUS, {}); + this.metrics.set(Linter.METRIC_TOTAL_CHANGES, 0); + this.metrics.set(Linter.METRIC_UNIQUE_CHANGES, 0); + this.metrics.set(Linter.METRIC_TOTAL_UPDATE_SETS, 0); // Calculate metrics this.changes.forEach((scan, name) => { // Status metrics - if (this.metrics.get("byStatus")[scan.status()] == null) { - this.metrics.get("byStatus")[scan.status()] = 0; + if (this.metrics.get(Linter.METRIC_BY_STATUS)[scan.getStatus()] == null) { + this.metrics.get(Linter.METRIC_BY_STATUS)[scan.getStatus()] = 0; } - this.metrics.get("byStatus")[scan.status()]++; + this.metrics.get(Linter.METRIC_BY_STATUS)[scan.getStatus()]++; // Changes metrics - this.metrics.set("totalChanges", this.metrics.get("totalChanges") + scan.updates); + this.metrics.set(Linter.METRIC_TOTAL_CHANGES, this.metrics.get(Linter.METRIC_TOTAL_CHANGES) + scan.updates); // Update Sets updateSetIDs.add(scan.updateSetID); }); - this.metrics.set("uniqueChanges", this.changes.size); - this.metrics.set("totalUpdateSets", updateSetIDs.size); + this.metrics.set(Linter.METRIC_UNIQUE_CHANGES, this.changes.size); + this.metrics.set(Linter.METRIC_TOTAL_UPDATE_SETS, updateSetIDs.size); } /** diff --git a/src/linter/UpdateXMLScan.ts b/src/linter/UpdateXMLScan.ts index 469a72a..5c4cb17 100644 --- a/src/linter/UpdateXMLScan.ts +++ b/src/linter/UpdateXMLScan.ts @@ -1,3 +1,4 @@ +import { ESLint } from "eslint"; import { SNUpdateXML } from "../core/sn.js"; export type UpdateXMLScanStatus = @@ -29,25 +30,39 @@ export type UpdateXMLScanStatus = "OK"; export class UpdateXMLScan extends SNUpdateXML { - private _status: UpdateXMLScanStatus = "SCAN"; - - // eslint-disable-next-line @typescript-eslint/consistent-generic-constructors, @typescript-eslint/no-explicit-any - reports: Map = new Map(); + private status: UpdateXMLScanStatus = "SCAN"; + private reports: Map = new Map(); getStatus(): UpdateXMLScanStatus { - return this._status; + if (this.action === "DELETE") { + return "DELETED"; + } + + if (this.hasReportsErrors()) { + return "ERROR"; + } + + if (this.hasReportsWarnings()) { + return "WARNING"; + } + + if (this.hasReports()) { + return "OK"; + } + + return this.status; } setIgnore(): void { - this._status = "IGNORED"; + this.status = "IGNORED"; } setManual(): void { - this._status = "MANUAL"; + this.status = "MANUAL"; } setSkip(): void { - this._status = "SKIPPED"; + this.status = "SKIPPED"; } getReportsWarningCount(): number { @@ -59,6 +74,18 @@ export class UpdateXMLScan extends SNUpdateXML { return count; } + setReport(field: string, report: ESLint.LintResult): void { + this.reports.set(field, report); + } + + getReports() { + return this.reports; + } + + hasReports(): boolean { + return this.reports.size > 0; + } + hasReportsWarnings(): boolean { return this.getReportsWarningCount() !== 0; }