From efb184e072dfd5dc8a9ca4b80665b53279652480 Mon Sep 17 00:00:00 2001 From: Sebastian Vollbrecht Date: Mon, 24 Jul 2023 00:21:16 +0200 Subject: [PATCH 01/19] Remove `plugin.overwriteIssueSummary` option --- src/constants.ts | 1 - src/context.spec.ts | 34 ---------------- src/context.ts | 5 --- .../importExecutionConverter.spec.ts | 39 +------------------ .../importExecutionConverter.ts | 4 +- src/types/plugin.ts | 9 ----- 6 files changed, 2 insertions(+), 90 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index d2a3bf45..4fc3d888 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -44,7 +44,6 @@ export const ENV_PLUGIN_DEBUG = "PLUGIN_DEBUG"; export const ENV_PLUGIN_ENABLED = "PLUGIN_ENABLED"; export const ENV_PLUGIN_LOG_DIRECTORY = "PLUGIN_LOG_DIRECTORY"; export const ENV_PLUGIN_NORMALIZE_SCREENSHOT_NAMES = "PLUGIN_NORMALIZE_SCREENSHOT_NAMES"; -export const ENV_PLUGIN_OVERWRITE_ISSUE_SUMMARY = "PLUGIN_OVERWRITE_ISSUE_SUMMARY"; // ============================================================================================== // // | OpenSSL Configuration | // // ============================================================================================== // diff --git a/src/context.spec.ts b/src/context.spec.ts index 640b685d..fb25f0ac 100644 --- a/src/context.spec.ts +++ b/src/context.spec.ts @@ -58,9 +58,6 @@ describe("the plugin context configuration", () => { it("normalizeScreenshotNames", () => { expect(options.plugin.normalizeScreenshotNames).to.eq(false); }); - it("overwriteIssueSummary", () => { - expect(options.plugin.overwriteIssueSummary).to.eq(false); - }); }); describe("xray", () => { @@ -278,21 +275,6 @@ describe("the plugin context configuration", () => { ); expect(options.plugin.normalizeScreenshotNames).to.eq(true); }); - it("overwriteIssueSummary", () => { - const options = initOptions( - {}, - { - jira: { - projectKey: "PRJ", - url: "https://example.org", - }, - plugin: { - overwriteIssueSummary: true, - }, - } - ); - expect(options.plugin.overwriteIssueSummary).to.eq(true); - }); }); describe("xray", () => { @@ -867,22 +849,6 @@ describe("the plugin context configuration", () => { }); expect(options.plugin?.normalizeScreenshotNames).to.be.true; }); - - it("PLUGIN_OVERWRITE_ISSUE_SUMMARY", () => { - const env = { - PLUGIN_OVERWRITE_ISSUE_SUMMARY: "true", - }; - const options = initOptions(env, { - jira: { - projectKey: "CYP", - url: "https://example.org", - }, - plugin: { - overwriteIssueSummary: false, - }, - }); - expect(options.plugin?.overwriteIssueSummary).to.be.true; - }); }); describe("openSSL", () => { it("OPENSSL_ROOT_CA_PATH ", () => { diff --git a/src/context.ts b/src/context.ts index cb6acb95..11472ca5 100644 --- a/src/context.ts +++ b/src/context.ts @@ -26,7 +26,6 @@ import { ENV_PLUGIN_ENABLED, ENV_PLUGIN_LOG_DIRECTORY, ENV_PLUGIN_NORMALIZE_SCREENSHOT_NAMES, - ENV_PLUGIN_OVERWRITE_ISSUE_SUMMARY, ENV_XRAY_CLIENT_ID, ENV_XRAY_CLIENT_SECRET, ENV_XRAY_STATUS_FAILED, @@ -80,10 +79,6 @@ export function initOptions(env: Cypress.ObjectLike, options: Options): Internal parse(env, ENV_PLUGIN_NORMALIZE_SCREENSHOT_NAMES, asBoolean) ?? options.plugin?.normalizeScreenshotNames ?? false, - overwriteIssueSummary: - parse(env, ENV_PLUGIN_OVERWRITE_ISSUE_SUMMARY, asBoolean) ?? - options.plugin?.overwriteIssueSummary ?? - false, }, xray: { statusFailed: diff --git a/src/conversion/importExecution/importExecutionConverter.spec.ts b/src/conversion/importExecution/importExecutionConverter.spec.ts index 65d9eefe..7d795c55 100644 --- a/src/conversion/importExecution/importExecutionConverter.spec.ts +++ b/src/conversion/importExecution/importExecutionConverter.spec.ts @@ -158,11 +158,10 @@ describe("the import execution converters", () => { expect(json.tests[1].status).to.eq("omit"); }); - it("should include step updates if overwrite issue summaries is enabled", () => { + it("includes step updates", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); - options.plugin.overwriteIssueSummary = true; options.xray.testTypes = { "CYP-40": "Manual", "CYP-41": "Manual", @@ -182,7 +181,6 @@ describe("the import execution converters", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); - options.plugin.overwriteIssueSummary = true; options.xray.steps.update = false; options.xray.testTypes = { "CYP-40": "Manual", @@ -200,7 +198,6 @@ describe("the import execution converters", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultLongBodies.json", "utf-8") ); - options.plugin.overwriteIssueSummary = true; options.xray.testTypes = { "CYP-123": "Manual", "CYP-456": "Manual", @@ -216,7 +213,6 @@ describe("the import execution converters", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultLongBodies.json", "utf-8") ); - options.plugin.overwriteIssueSummary = true; options.xray.steps.maxLengthAction = 5; options.xray.testTypes = { "CYP-123": "Manual", @@ -247,7 +243,6 @@ describe("the import execution converters", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); - options.plugin.overwriteIssueSummary = true; options.xray.steps.update = false; options.xray.testTypes = { "CYP-40": "Manual", @@ -299,38 +294,6 @@ describe("the import execution converters", () => { expect(json.info.testPlanKey).to.be.undefined; }); - it("should overwrite existing test issue information if specified", () => { - const result: CypressCommandLine.CypressRunResult = JSON.parse( - readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") - ); - options.plugin.overwriteIssueSummary = true; - options.xray.testTypes = { - "CYP-40": "Manual", - "CYP-41": "Manual", - "CYP-49": "Manual", - }; - const json = converter.convert(result); - expect(json.tests).to.have.length(3); - expect(json.tests[0].testKey).to.eq("CYP-40"); - expect(json.tests[1].testKey).to.eq("CYP-41"); - expect(json.tests[2].testKey).to.eq("CYP-49"); - expect(json.tests[0].testInfo).to.not.be.undefined; - expect(json.tests[1].testInfo).to.not.be.undefined; - expect(json.tests[2].testInfo).to.not.be.undefined; - }); - - it("should not update test information with summary overwriting disabled", () => { - const result: CypressCommandLine.CypressRunResult = JSON.parse( - readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") - ); - options.plugin.overwriteIssueSummary = false; - const json = converter.convert(result); - expect(json.tests).to.have.length(3); - expect(json.tests[0].testInfo).to.not.exist; - expect(json.tests[1].testInfo).to.not.exist; - expect(json.tests[2].testInfo).to.not.exist; - }); - it("should include a custom test execution summary if provided", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") diff --git a/src/conversion/importExecution/importExecutionConverter.ts b/src/conversion/importExecution/importExecutionConverter.ts index c48bf88e..ac559a87 100644 --- a/src/conversion/importExecution/importExecutionConverter.ts +++ b/src/conversion/importExecution/importExecutionConverter.ts @@ -45,9 +45,7 @@ export abstract class ImportExecutionConverter< test = this.getTest(attempts[attempts.length - 1]); const issueKey = getTestIssueKey(title, this.options.jira.projectKey); test.testKey = issueKey; - if (this.options.plugin.overwriteIssueSummary) { - test.testInfo = this.getTestInfo(issueKey, testResult); - } + test.testInfo = this.getTestInfo(issueKey, testResult); this.addTest(json, test); } catch (error: unknown) { let reason = error; diff --git a/src/types/plugin.ts b/src/types/plugin.ts index b20b1d34..42f7c61c 100644 --- a/src/types/plugin.ts +++ b/src/types/plugin.ts @@ -98,10 +98,6 @@ export interface XrayStepOptions { * * Note: the plugin currently creates only one step containing the code of the corresponding * Cypress test function. - * - * Note: steps of existing issues can only be updated if - * {@link PluginOptions.overwriteIssueSummary} is enabled as well, since Xray requires an issue - * summary whenever test details are updated. */ update?: boolean; } @@ -196,11 +192,6 @@ export interface PluginOptions { * screenshot names and replaces all other sequences with `_`. */ normalizeScreenshotNames?: boolean; - /** - * Decide whether to keep the issues' existing summaries or whether to overwrite them with - * each upload. - */ - overwriteIssueSummary?: boolean; } export interface OpenSSLOptions { From 48de5dd084285f35ce5e5c1d75d10ec9dfe530e8 Mon Sep 17 00:00:00 2001 From: Sebastian Vollbrecht Date: Tue, 25 Jul 2023 21:33:16 +0200 Subject: [PATCH 02/19] Add issue repository classes --- src/repository/jira/issueRepository.ts | 153 +++++++++++++++++++ src/repository/jira/issueRepositoryCloud.ts | 9 ++ src/repository/jira/issueRepositoryServer.ts | 5 + 3 files changed, 167 insertions(+) create mode 100644 src/repository/jira/issueRepository.ts create mode 100644 src/repository/jira/issueRepositoryCloud.ts create mode 100644 src/repository/jira/issueRepositoryServer.ts diff --git a/src/repository/jira/issueRepository.ts b/src/repository/jira/issueRepository.ts new file mode 100644 index 00000000..ddd5e8f6 --- /dev/null +++ b/src/repository/jira/issueRepository.ts @@ -0,0 +1,153 @@ +import { JiraClientCloud } from "../../client/jira/jiraClientCloud"; +import { JiraClientServer } from "../../client/jira/jiraClientServer"; +import { XrayClientCloud } from "../../client/xray/xrayClientCloud"; +import { XrayClientServer } from "../../client/xray/xrayClientServer"; +import { logWarning } from "../../logging/logging"; +import { IssueCloud, IssueServer } from "../../types/jira/responses/issue"; +import { Options } from "../../types/plugin"; + +export interface FieldResponse { + [key: string]: T; +} + +export abstract class IssueRepository< + XrayClientType extends XrayClientServer | XrayClientCloud, + JiraClientType extends JiraClientServer | JiraClientCloud +> { + protected readonly xrayClient: XrayClientType; + protected readonly jiraClient: JiraClientType; + protected readonly options: Options; + + private readonly fieldIds: { [key: string]: string } = {}; + private readonly summaries: FieldResponse = {}; + private readonly descriptions: FieldResponse = {}; + private readonly testTypes: FieldResponse = {}; + + constructor(jiraClient: JiraClientType, options: Options) { + this.jiraClient = jiraClient; + this.options = options; + } + + public async getSummaries(...issueKeys: string[]): Promise { + const missingSummaries: string[] = await this.fetchFields( + this.summaries, + this.fetchSummaries, + ...issueKeys + ); + if (missingSummaries.length > 0) { + throw new Error(`Failed to fetch summaries of issues: ${missingSummaries.join(",")}`); + } + return issueKeys.map((key) => this.summaries[key]); + } + + public async getDescriptions(...issueKeys: string[]): Promise { + const missingDescriptions: string[] = await this.fetchFields( + this.descriptions, + this.fetchDescriptions, + ...issueKeys + ); + if (missingDescriptions.length > 0) { + throw new Error( + `Failed to fetch descriptions of issues: ${missingDescriptions.join(",")}` + ); + } + return issueKeys.map((key) => this.descriptions[key]); + } + + public async getTestTypes(...issueKeys: string[]): Promise { + const missingTestTypes: string[] = await this.fetchFields( + this.descriptions, + this.fetchTestTypes, + ...issueKeys + ); + if (missingTestTypes.length > 0) { + throw new Error(`Failed to fetch test types of issues: ${missingTestTypes.join(",")}`); + } + return issueKeys.map((key) => this.testTypes[key]); + } + + protected async fetchSummaries(...issueKeys: string[]): Promise> { + // Field property example: + // summary: "Bug 12345" + return await this.getJiraField("summary", this.stringExtractor, ...issueKeys); + } + + protected async fetchDescriptions(...issueKeys: string[]): Promise> { + // Field property example: + // description: "This is a description" + return await this.getJiraField("description", this.stringExtractor, ...issueKeys); + } + + protected async fetchTestTypes(...issueKeys: string[]): Promise> { + // Field property example: + // customfield_12100: { + // value: "Cucumber", + // id: "12702", + // disabled: false + // } + return await this.getJiraField("Test Type", this.valueExtractor, ...issueKeys); + } + + private async fetchFields( + existingFields: FieldResponse, + fetcher: (...issueKeys: string[]) => Promise>, + ...issueKeys: string[] + ): Promise { + const missingFields: string[] = issueKeys.filter((key: string) => !(key in existingFields)); + if (missingFields.length > 0) { + const fetchedFields = await fetcher(...missingFields); + for (let i = missingFields.length - 1; i >= 0; i--) { + const key = missingFields[i]; + if (key in fetchedFields) { + existingFields[key] = fetchedFields[key]; + missingFields.pop(); + } + } + } + return missingFields; + } + + protected async getJiraField( + field: string, + extractor: (value: unknown) => T, + ...issueKeys: string[] + ): Promise> { + const results: FieldResponse = {}; + if (!(field in this.fieldIds)) { + const jiraFields = await this.jiraClient.getFields(); + jiraFields.forEach((jiraField) => { + this.fieldIds[jiraField.name] = jiraField.id; + }); + } + const fieldId = this.fieldIds[field]; + if (fieldId !== undefined) { + const issues: IssueServer[] | IssueCloud[] = await this.jiraClient.search({ + jql: `project = ${this.options.jira.projectKey} AND issue in (${issueKeys.join( + "," + )})`, + fields: [fieldId], + }); + issues.forEach((issue: IssueServer | IssueCloud) => { + const value = extractor(issue.fields[field]); + if (value !== undefined) { + results[issue.key] = value; + } + }); + } else { + logWarning(`Failed to fetch Jira field ID for field: ${field}`); + } + return results; + } + + private stringExtractor(value: unknown): string | undefined { + if (typeof value === "string") { + return value; + } + } + + private valueExtractor(data: unknown): string | undefined { + if (typeof data === "object" && data !== null) { + return data["value"]; + } + } +} diff --git a/src/repository/jira/issueRepositoryCloud.ts b/src/repository/jira/issueRepositoryCloud.ts new file mode 100644 index 00000000..9fbab9a4 --- /dev/null +++ b/src/repository/jira/issueRepositoryCloud.ts @@ -0,0 +1,9 @@ +import { JiraClientCloud } from "../../client/jira/jiraClientCloud"; +import { XrayClientCloud } from "../../client/xray/xrayClientCloud"; +import { FieldResponse, IssueRepository } from "./issueRepository"; + +export class IssueRepositoryCloud extends IssueRepository { + protected async fetchTestTypes(...issueKeys: string[]): Promise> { + return this.xrayClient.getTestTypes(this.options.jira.projectKey, ...issueKeys); + } +} diff --git a/src/repository/jira/issueRepositoryServer.ts b/src/repository/jira/issueRepositoryServer.ts new file mode 100644 index 00000000..ee9654ac --- /dev/null +++ b/src/repository/jira/issueRepositoryServer.ts @@ -0,0 +1,5 @@ +import { JiraClientServer } from "../../client/jira/jiraClientServer"; +import { XrayClientServer } from "../../client/xray/xrayClientServer"; +import { IssueRepository } from "./issueRepository"; + +export class IssueRepositoryServer extends IssueRepository {} From d13bf1c5103288215d3ffe880ececd857f8256c5 Mon Sep 17 00:00:00 2001 From: Sebastian Vollbrecht Date: Wed, 26 Jul 2023 00:10:56 +0200 Subject: [PATCH 03/19] Update issue repositories --- src/https/requests.ts | 3 + src/repository/jira/issueRepository.ts | 120 +++---- src/repository/jira/issueRepositoryCloud.ts | 18 +- .../jira/issueRepositoryServer.spec.ts | 311 ++++++++++++++++++ src/repository/jira/issueRepositoryServer.ts | 25 +- src/types/util.ts | 7 + 6 files changed, 423 insertions(+), 61 deletions(-) create mode 100644 src/repository/jira/issueRepositoryServer.spec.ts diff --git a/src/https/requests.ts b/src/https/requests.ts index ba24e44b..5592099d 100644 --- a/src/https/requests.ts +++ b/src/https/requests.ts @@ -26,6 +26,9 @@ export class Requests { } private static axios(): Axios { + if (!Requests.options) { + throw new Error("Requests module has not been initialized"); + } if (!Requests.AXIOS) { Requests.AXIOS = axios; if (Requests.options.plugin.debug) { diff --git a/src/repository/jira/issueRepository.ts b/src/repository/jira/issueRepository.ts index ddd5e8f6..52adbd00 100644 --- a/src/repository/jira/issueRepository.ts +++ b/src/repository/jira/issueRepository.ts @@ -2,13 +2,10 @@ import { JiraClientCloud } from "../../client/jira/jiraClientCloud"; import { JiraClientServer } from "../../client/jira/jiraClientServer"; import { XrayClientCloud } from "../../client/xray/xrayClientCloud"; import { XrayClientServer } from "../../client/xray/xrayClientServer"; -import { logWarning } from "../../logging/logging"; +import { logError, logWarning } from "../../logging/logging"; import { IssueCloud, IssueServer } from "../../types/jira/responses/issue"; import { Options } from "../../types/plugin"; - -export interface FieldResponse { - [key: string]: T; -} +import { StringMap } from "../../types/util"; export abstract class IssueRepository< XrayClientType extends XrayClientServer | XrayClientCloud, @@ -19,25 +16,31 @@ export abstract class IssueRepository< protected readonly options: Options; private readonly fieldIds: { [key: string]: string } = {}; - private readonly summaries: FieldResponse = {}; - private readonly descriptions: FieldResponse = {}; - private readonly testTypes: FieldResponse = {}; + private readonly summaries: StringMap = {}; + private readonly descriptions: StringMap = {}; + private readonly testTypes: StringMap = {}; constructor(jiraClient: JiraClientType, options: Options) { this.jiraClient = jiraClient; this.options = options; } - public async getSummaries(...issueKeys: string[]): Promise { + public async getSummaries(...issueKeys: string[]): Promise> { const missingSummaries: string[] = await this.fetchFields( this.summaries, - this.fetchSummaries, + this.fetchSummaries.bind(this), ...issueKeys ); if (missingSummaries.length > 0) { - throw new Error(`Failed to fetch summaries of issues: ${missingSummaries.join(",")}`); + logError(`Failed to fetch summaries of issues:\n${missingSummaries.join("\n")}`); } - return issueKeys.map((key) => this.summaries[key]); + const result: StringMap = {}; + issueKeys.forEach((key: string) => { + if (key in this.summaries) { + result[key] = this.summaries[key]; + } + }); + return result; } public async getDescriptions(...issueKeys: string[]): Promise { @@ -66,60 +69,28 @@ export abstract class IssueRepository< return issueKeys.map((key) => this.testTypes[key]); } - protected async fetchSummaries(...issueKeys: string[]): Promise> { - // Field property example: - // summary: "Bug 12345" - return await this.getJiraField("summary", this.stringExtractor, ...issueKeys); - } + protected abstract fetchSummaries(...issueKeys: string[]): Promise>; - protected async fetchDescriptions(...issueKeys: string[]): Promise> { - // Field property example: - // description: "This is a description" - return await this.getJiraField("description", this.stringExtractor, ...issueKeys); - } + protected abstract fetchDescriptions(...issueKeys: string[]): Promise>; - protected async fetchTestTypes(...issueKeys: string[]): Promise> { - // Field property example: - // customfield_12100: { - // value: "Cucumber", - // id: "12702", - // disabled: false - // } - return await this.getJiraField("Test Type", this.valueExtractor, ...issueKeys); - } - - private async fetchFields( - existingFields: FieldResponse, - fetcher: (...issueKeys: string[]) => Promise>, - ...issueKeys: string[] - ): Promise { - const missingFields: string[] = issueKeys.filter((key: string) => !(key in existingFields)); - if (missingFields.length > 0) { - const fetchedFields = await fetcher(...missingFields); - for (let i = missingFields.length - 1; i >= 0; i--) { - const key = missingFields[i]; - if (key in fetchedFields) { - existingFields[key] = fetchedFields[key]; - missingFields.pop(); - } - } - } - return missingFields; - } + protected abstract fetchTestTypes(...issueKeys: string[]): Promise>; protected async getJiraField( - field: string, + fieldName: string, extractor: (value: unknown) => T, ...issueKeys: string[] - ): Promise> { - const results: FieldResponse = {}; - if (!(field in this.fieldIds)) { + ): Promise> { + const results: StringMap = {}; + if (!(fieldName in this.fieldIds)) { const jiraFields = await this.jiraClient.getFields(); + if (!jiraFields) { + return results; + } jiraFields.forEach((jiraField) => { this.fieldIds[jiraField.name] = jiraField.id; }); } - const fieldId = this.fieldIds[field]; + const fieldId = this.fieldIds[fieldName]; if (fieldId !== undefined) { const issues: IssueServer[] | IssueCloud[] = await this.jiraClient.search({ jql: `project = ${this.options.jira.projectKey} AND issue in (${issueKeys.join( @@ -127,27 +98,58 @@ export abstract class IssueRepository< )})`, fields: [fieldId], }); + const issuesWithUnparseableField: string[] = []; issues.forEach((issue: IssueServer | IssueCloud) => { - const value = extractor(issue.fields[field]); + const value = extractor(issue.fields[fieldId]); if (value !== undefined) { results[issue.key] = value; + } else { + issuesWithUnparseableField.push(issue.key); } }); + if (issuesWithUnparseableField.length > 0) { + logWarning( + `Failed to parse the following field of the following issues: ${fieldName}\n${issuesWithUnparseableField.join( + "\n" + )}` + ); + } } else { - logWarning(`Failed to fetch Jira field ID for field: ${field}`); + logWarning(`Failed to fetch Jira field ID for field: ${fieldName}`); } return results; } - private stringExtractor(value: unknown): string | undefined { + protected stringExtractor(value: unknown): string | undefined { if (typeof value === "string") { return value; } } - private valueExtractor(data: unknown): string | undefined { + protected valueExtractor(data: unknown): string | undefined { if (typeof data === "object" && data !== null) { return data["value"]; } } + + private async fetchFields( + existingFields: StringMap, + fetcher: (...issueKeys: string[]) => Promise>, + ...issueKeys: string[] + ): Promise { + const issuesWithMissingField: string[] = issueKeys.filter( + (key: string) => !(key in existingFields) + ); + if (issuesWithMissingField.length > 0) { + const fetchedFields = await fetcher(...issuesWithMissingField); + for (let i = issuesWithMissingField.length - 1; i >= 0; i--) { + const key = issuesWithMissingField[i]; + if (key in fetchedFields) { + existingFields[key] = fetchedFields[key]; + issuesWithMissingField.splice(i, 1); + } + } + } + return issuesWithMissingField; + } } diff --git a/src/repository/jira/issueRepositoryCloud.ts b/src/repository/jira/issueRepositoryCloud.ts index 9fbab9a4..9b72770b 100644 --- a/src/repository/jira/issueRepositoryCloud.ts +++ b/src/repository/jira/issueRepositoryCloud.ts @@ -3,7 +3,23 @@ import { XrayClientCloud } from "../../client/xray/xrayClientCloud"; import { FieldResponse, IssueRepository } from "./issueRepository"; export class IssueRepositoryCloud extends IssueRepository { + protected async fetchSummaries(...issueKeys: string[]): Promise> { + // Field property example: + // summary: "Bug 12345" + return await this.getJiraField("summary", this.stringExtractor.bind(this), ...issueKeys); + } + + protected async fetchDescriptions(...issueKeys: string[]): Promise> { + // Field property example: + // description: "This is a description" + return await this.getJiraField("description", this.stringExtractor, ...issueKeys); + } + protected async fetchTestTypes(...issueKeys: string[]): Promise> { - return this.xrayClient.getTestTypes(this.options.jira.projectKey, ...issueKeys); + const testTypes = await this.xrayClient.getTestTypes( + this.options.jira.projectKey, + ...issueKeys + ); + return testTypes ? testTypes : {}; } } diff --git a/src/repository/jira/issueRepositoryServer.spec.ts b/src/repository/jira/issueRepositoryServer.spec.ts new file mode 100644 index 00000000..94212d94 --- /dev/null +++ b/src/repository/jira/issueRepositoryServer.spec.ts @@ -0,0 +1,311 @@ +import { expect } from "chai"; +import dedent from "dedent"; +import { stub } from "sinon"; +import { stubLogging } from "../../../test/util"; +import { PATCredentials } from "../../authentication/credentials"; +import { JiraClientServer } from "../../client/jira/jiraClientServer"; +import { initOptions } from "../../context"; +import { InternalOptions } from "../../types/plugin"; +import { IssueRepositoryServer } from "./issueRepositoryServer"; + +describe.only("the server issue repository", () => { + let options: InternalOptions; + let client: JiraClientServer; + let repository: IssueRepositoryServer; + + beforeEach(() => { + options = initOptions( + {}, + { + jira: { + projectKey: "CYP", + url: "https://example.org", + }, + } + ); + client = new JiraClientServer("https://example.org", new PATCredentials("token")); + repository = new IssueRepositoryServer(client, options); + }); + + describe("getSummaries", () => { + it("fetches summaries", async () => { + stub(client, "getFields").resolves([ + { + id: "summary", + name: "Summary", + custom: false, + orderable: true, + navigable: true, + searchable: true, + clauseNames: ["summary"], + schema: { + type: "string", + system: "summary", + }, + }, + ]); + const searchStub = stub(client, "search").resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1000", + self: "https://example.org/rest/api/2/issue/1000", + key: "CYP-123", + fields: { + summary: "Hello", + }, + }, + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1001", + self: "https://example.org/rest/api/2/issue/1001", + key: "CYP-456", + fields: { + summary: "Good Morning", + }, + }, + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1002", + self: "https://example.org/rest/api/2/issue/1002", + key: "CYP-789", + fields: { + summary: "Goodbye", + }, + }, + ]); + const summaries = await repository.getSummaries("CYP-123", "CYP-456", "CYP-789"); + expect(summaries).to.deep.eq({ + "CYP-123": "Hello", + "CYP-456": "Good Morning", + "CYP-789": "Goodbye", + }); + expect(searchStub).to.have.been.calledOnceWithExactly({ + jql: "project = CYP AND issue in (CYP-123,CYP-456,CYP-789)", + fields: ["summary"], + }); + }); + + it("fetches summaries only for unknown issues", async () => { + const stubbedGetFields = stub(client, "getFields").resolves([ + { + id: "summary", + name: "Summary", + custom: false, + orderable: true, + navigable: true, + searchable: true, + clauseNames: ["summary"], + schema: { + type: "string", + system: "summary", + }, + }, + ]); + const stubbedSearch = stub(client, "search"); + stubbedSearch.onFirstCall().resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1000", + self: "https://example.org/rest/api/2/issue/1000", + key: "CYP-123", + fields: { + summary: "Hello", + }, + }, + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1002", + self: "https://example.org/rest/api/2/issue/1002", + key: "CYP-789", + fields: { + summary: "Goodbye", + }, + }, + ]); + stubbedSearch.onSecondCall().resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1001", + self: "https://example.org/rest/api/2/issue/1001", + key: "CYP-456", + fields: { + summary: "Good Morning", + }, + }, + ]); + await repository.getSummaries("CYP-123", "CYP-789"); + const summaries = await repository.getSummaries("CYP-123", "CYP-456", "CYP-789"); + expect(summaries).to.deep.eq({ + "CYP-123": "Hello", + "CYP-456": "Good Morning", + "CYP-789": "Goodbye", + }); + // Everything's fetched already, should not fetch anything again. + await repository.getSummaries("CYP-123", "CYP-456", "CYP-789"); + expect(stubbedGetFields).to.have.been.calledOnce; + expect(stubbedSearch).to.have.been.calledTwice; + expect(stubbedSearch.secondCall).to.have.been.calledWithExactly({ + jql: "project = CYP AND issue in (CYP-456)", + fields: ["summary"], + }); + }); + + it("displays an error for issues which do not exist", async () => { + stub(client, "getFields").resolves([ + { + id: "summary", + name: "Summary", + custom: false, + orderable: true, + navigable: true, + searchable: true, + clauseNames: ["summary"], + schema: { + type: "string", + system: "summary", + }, + }, + ]); + const stubbedSearch = stub(client, "search"); + stubbedSearch.resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1000", + self: "https://example.org/rest/api/2/issue/1000", + key: "CYP-123", + fields: { + summary: "Hello", + }, + }, + ]); + const { stubbedError } = stubLogging(); + const summaries = await repository.getSummaries("CYP-123", "CYP-456", "CYP-789"); + expect(stubbedError).to.have.been.calledOnceWithExactly( + dedent(` + Failed to fetch summaries of issues: + CYP-456 + CYP-789 + `) + ); + expect(summaries).to.deep.eq({ + "CYP-123": "Hello", + }); + }); + + it("displays a warning when the summary field does not exist", async () => { + stub(client, "getFields").resolves([]); + const stubbedSearch = stub(client, "search"); + stubbedSearch.resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1000", + self: "https://example.org/rest/api/2/issue/1000", + key: "CYP-123", + }, + ]); + const { stubbedError, stubbedWarning } = stubLogging(); + const summaries = await repository.getSummaries("CYP-123"); + expect(stubbedSearch).to.not.have.been.called; + expect(stubbedWarning).to.have.been.calledOnceWithExactly( + "Failed to fetch Jira field ID for field: Summary" + ); + expect(stubbedError).to.have.been.calledOnceWithExactly( + dedent(` + Failed to fetch summaries of issues: + CYP-123 + `) + ); + expect(summaries).to.deep.eq({}); + }); + + it("handles get field failures gracefully", async () => { + stub(client, "getFields").resolves(undefined); + const stubbedSearch = stub(client, "search"); + stubbedSearch.resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1000", + self: "https://example.org/rest/api/2/issue/1000", + key: "CYP-123", + }, + ]); + const { stubbedError } = stubLogging(); + const summaries = await repository.getSummaries("CYP-123"); + expect(stubbedSearch).to.not.have.been.called; + expect(stubbedError).to.have.been.calledOnceWithExactly( + dedent(` + Failed to fetch summaries of issues: + CYP-123 + `) + ); + expect(summaries).to.deep.eq({}); + }); + + it("handles unparseable field failures gracefully", async () => { + stub(client, "getFields").resolves([ + { + id: "summary", + name: "Summary", + custom: false, + orderable: true, + navigable: true, + searchable: true, + clauseNames: ["summary"], + schema: { + type: "string", + system: "summary", + }, + }, + ]); + const stubbedSearch = stub(client, "search"); + stubbedSearch.resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1000", + self: "https://example.org/rest/api/2/issue/1000", + key: "CYP-123", + fields: { + summary: ["Good Morning", "Summary 2"], + }, + }, + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1001", + self: "https://example.org/rest/api/2/issue/1001", + key: "CYP-456", + fields: { + summary: { + Something: 5, + }, + }, + }, + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1002", + self: "https://example.org/rest/api/2/issue/1002", + key: "CYP-789", + fields: { + summary: "Bonjour", + }, + }, + ]); + const { stubbedError, stubbedWarning } = stubLogging(); + const summaries = await repository.getSummaries("CYP-123", "CYP-456", "CYP-789"); + expect(stubbedWarning).to.have.been.calledOnceWithExactly( + dedent(` + Failed to parse the following field of the following issues: Summary + CYP-123 + CYP-456 + `) + ); + expect(stubbedError).to.have.been.calledOnceWithExactly( + dedent(` + Failed to fetch summaries of issues: + CYP-123 + CYP-456 + `) + ); + expect(summaries).to.deep.eq({ "CYP-789": "Bonjour" }); + }); + }); +}); diff --git a/src/repository/jira/issueRepositoryServer.ts b/src/repository/jira/issueRepositoryServer.ts index ee9654ac..2ae5809c 100644 --- a/src/repository/jira/issueRepositoryServer.ts +++ b/src/repository/jira/issueRepositoryServer.ts @@ -1,5 +1,28 @@ import { JiraClientServer } from "../../client/jira/jiraClientServer"; import { XrayClientServer } from "../../client/xray/xrayClientServer"; +import { StringMap } from "../../types/util"; import { IssueRepository } from "./issueRepository"; -export class IssueRepositoryServer extends IssueRepository {} +export class IssueRepositoryServer extends IssueRepository { + protected async fetchSummaries(...issueKeys: string[]): Promise> { + // Field property example: + // summary: "Bug 12345" + return await this.getJiraField("Summary", this.stringExtractor, ...issueKeys); + } + + protected async fetchDescriptions(...issueKeys: string[]): Promise> { + // Field property example: + // description: "This is a description" + return await this.getJiraField("Description", this.stringExtractor, ...issueKeys); + } + + protected async fetchTestTypes(...issueKeys: string[]): Promise> { + // Field property example: + // customfield_12100: { + // value: "Cucumber", + // id: "12702", + // disabled: false + // } + return await this.getJiraField("Test Type", this.valueExtractor, ...issueKeys); + } +} diff --git a/src/types/util.ts b/src/types/util.ts index 620f54a8..d59af8e4 100644 --- a/src/types/util.ts +++ b/src/types/util.ts @@ -26,3 +26,10 @@ export function getEnumKeyByEnumValue enumType[x] === enumValue); return keys.length === 1 ? keys[0] : null; } + +/** + * Type describing mappings of string keys to arbitrary values. + */ +export type StringMap = { + [key: string]: T; +}; From 1a6023b852057a0280358e1f918cdcbc8f9a9123 Mon Sep 17 00:00:00 2001 From: Sebastian Vollbrecht Date: Wed, 26 Jul 2023 00:28:10 +0200 Subject: [PATCH 04/19] Improve typing --- src/authentication/credentials.ts | 5 ++--- src/client/xray/xrayClient.ts | 4 ++-- src/client/xray/xrayClientCloud.ts | 3 ++- src/client/xray/xrayClientServer.ts | 3 ++- src/repository/jira/issueRepository.ts | 2 +- src/repository/jira/issueRepositoryServer.spec.ts | 2 +- src/types/plugin.ts | 6 ++---- src/types/xray/responses/graphql/xray.ts | 8 +++++--- 8 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/authentication/credentials.ts b/src/authentication/credentials.ts index a14d9582..ebddd0cf 100644 --- a/src/authentication/credentials.ts +++ b/src/authentication/credentials.ts @@ -1,6 +1,7 @@ import { AxiosResponse } from "axios"; import { Requests } from "../https/requests"; import { logInfo, logSuccess } from "../logging/logging"; +import { StringMap } from "../types/util"; import { encode } from "../util/base64"; /** @@ -9,9 +10,7 @@ import { encode } from "../util/base64"; * { "Authorization": "Bearer xyz" } * { "Content-Type": "application/json" } */ -export interface HTTPHeader { - [key: string]: string; -} +export type HTTPHeader = StringMap; export abstract class APICredentials { public abstract getAuthenticationHeader(options?: O): Promise; diff --git a/src/client/xray/xrayClient.ts b/src/client/xray/xrayClient.ts index 7b10b6ce..d8f3af6b 100644 --- a/src/client/xray/xrayClient.ts +++ b/src/client/xray/xrayClient.ts @@ -8,7 +8,7 @@ import { } from "../../authentication/credentials"; import { Requests } from "../../https/requests"; import { logError, logInfo, logSuccess, logWarning, writeErrorFile } from "../../logging/logging"; -import { OneOf } from "../../types/util"; +import { OneOf, StringMap } from "../../types/util"; import { XrayTestExecutionResultsCloud, XrayTestExecutionResultsServer, @@ -305,5 +305,5 @@ export abstract class XrayClient< public abstract getTestTypes( projectKey: string, ...issueKeys: string[] - ): Promise<{ [key: string]: string }>; + ): Promise>; } diff --git a/src/client/xray/xrayClientCloud.ts b/src/client/xray/xrayClientCloud.ts index 17e577b2..8267f235 100644 --- a/src/client/xray/xrayClientCloud.ts +++ b/src/client/xray/xrayClientCloud.ts @@ -2,6 +2,7 @@ import dedent from "dedent"; import { JWTCredentials } from "../../authentication/credentials"; import { Requests } from "../../https/requests"; import { logError, logInfo, logSuccess, logWarning, writeErrorFile } from "../../logging/logging"; +import { StringMap } from "../../types/util"; import { CucumberMultipartInfoCloud } from "../../types/xray/requests/importExecutionCucumberMultipartInfo"; import { GetTestsResponse } from "../../types/xray/responses/graphql/getTests"; import { ImportExecutionResponseCloud } from "../../types/xray/responses/importExecution"; @@ -99,7 +100,7 @@ export class XrayClientCloud extends XrayClient< public async getTestTypes( projectKey: string, ...issueKeys: string[] - ): Promise<{ [key: string]: string }> { + ): Promise> { try { if (!issueKeys || issueKeys.length === 0) { logWarning("No issue keys provided. Skipping test type retrieval"); diff --git a/src/client/xray/xrayClientServer.ts b/src/client/xray/xrayClientServer.ts index 7cf65f9c..904f954a 100644 --- a/src/client/xray/xrayClientServer.ts +++ b/src/client/xray/xrayClientServer.ts @@ -2,6 +2,7 @@ import dedent from "dedent"; import { BasicAuthCredentials, PATCredentials } from "../../authentication/credentials"; import { logError, logInfo, logSuccess, logWarning, writeErrorFile } from "../../logging/logging"; import { FieldDetailServer } from "../../types/jira/responses/fieldDetail"; +import { StringMap } from "../../types/util"; import { CucumberMultipartInfoServer } from "../../types/xray/requests/importExecutionCucumberMultipartInfo"; import { ImportExecutionResponseServer } from "../../types/xray/responses/importExecution"; import { @@ -90,7 +91,7 @@ export class XrayClientServer extends XrayClient< public async getTestTypes( projectKey: string, ...issueKeys: string[] - ): Promise<{ [key: string]: string }> { + ): Promise> { try { if (!issueKeys || issueKeys.length === 0) { logWarning("No issue keys provided. Skipping test type retrieval"); diff --git a/src/repository/jira/issueRepository.ts b/src/repository/jira/issueRepository.ts index 52adbd00..8ace50dc 100644 --- a/src/repository/jira/issueRepository.ts +++ b/src/repository/jira/issueRepository.ts @@ -15,7 +15,7 @@ export abstract class IssueRepository< protected readonly jiraClient: JiraClientType; protected readonly options: Options; - private readonly fieldIds: { [key: string]: string } = {}; + private readonly fieldIds: StringMap = {}; private readonly summaries: StringMap = {}; private readonly descriptions: StringMap = {}; private readonly testTypes: StringMap = {}; diff --git a/src/repository/jira/issueRepositoryServer.spec.ts b/src/repository/jira/issueRepositoryServer.spec.ts index 94212d94..63553f89 100644 --- a/src/repository/jira/issueRepositoryServer.spec.ts +++ b/src/repository/jira/issueRepositoryServer.spec.ts @@ -8,7 +8,7 @@ import { initOptions } from "../../context"; import { InternalOptions } from "../../types/plugin"; import { IssueRepositoryServer } from "./issueRepositoryServer"; -describe.only("the server issue repository", () => { +describe("the server issue repository", () => { let options: InternalOptions; let client: JiraClientServer; let repository: IssueRepositoryServer; diff --git a/src/types/plugin.ts b/src/types/plugin.ts index 42f7c61c..173708a6 100644 --- a/src/types/plugin.ts +++ b/src/types/plugin.ts @@ -4,7 +4,7 @@ import { JiraClientServer } from "../client/jira/jiraClientServer"; import { XrayClientCloud } from "../client/xray/xrayClientCloud"; import { XrayClientServer } from "../client/xray/xrayClientServer"; import { IssueTypeDetailsCloud, IssueTypeDetailsServer } from "./jira/responses/issueTypeDetails"; -import { OneOf } from "./util"; +import { OneOf, StringMap } from "./util"; export interface Options { jira: JiraOptions; @@ -240,9 +240,7 @@ export type InternalOptions = Options & { * `testType` (Xray Server) or `type` (Xray Cloud) properties are required by Xray's JSON * scheme for uploading results. */ - testTypes?: { - [key: string]: string; - }; + testTypes?: StringMap; }; cucumber?: { preprocessor?: Awaited>; diff --git a/src/types/xray/responses/graphql/xray.ts b/src/types/xray/responses/graphql/xray.ts index 8d63ed16..bfc17007 100644 --- a/src/types/xray/responses/graphql/xray.ts +++ b/src/types/xray/responses/graphql/xray.ts @@ -1,13 +1,15 @@ +import { StringMap } from "../../../util"; + /* * Generated using: https://transform.tools/graphql-to-typescript - * Generic types added manually. + * Some type parameters have been added manually. */ export type Maybe = T | null; export type InputMaybe = Maybe; -export type Exact = { [K in keyof T]: T[K] }; +export type Exact> = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; -export type MakeEmpty = { +export type MakeEmpty, K extends keyof T> = { [_ in K]?: never; }; export type Incremental = From 2a8d12748e6bdb3150b8029e1ff2d68bf1f7253e Mon Sep 17 00:00:00 2001 From: Sebastian Vollbrecht Date: Wed, 26 Jul 2023 20:08:57 +0200 Subject: [PATCH 05/19] Add `getDescriptions` and `getTestTypes` tests --- src/repository/jira/issueRepository.ts | 34 +- src/repository/jira/issueRepositoryCloud.ts | 11 +- .../jira/issueRepositoryServer.spec.ts | 610 +++++++++++++++++- 3 files changed, 637 insertions(+), 18 deletions(-) diff --git a/src/repository/jira/issueRepository.ts b/src/repository/jira/issueRepository.ts index 8ace50dc..6d541d6e 100644 --- a/src/repository/jira/issueRepository.ts +++ b/src/repository/jira/issueRepository.ts @@ -43,30 +43,40 @@ export abstract class IssueRepository< return result; } - public async getDescriptions(...issueKeys: string[]): Promise { + public async getDescriptions(...issueKeys: string[]): Promise> { const missingDescriptions: string[] = await this.fetchFields( this.descriptions, - this.fetchDescriptions, + this.fetchDescriptions.bind(this), ...issueKeys ); if (missingDescriptions.length > 0) { - throw new Error( - `Failed to fetch descriptions of issues: ${missingDescriptions.join(",")}` - ); + logError(`Failed to fetch descriptions of issues:\n${missingDescriptions.join("\n")}`); } - return issueKeys.map((key) => this.descriptions[key]); + const result: StringMap = {}; + issueKeys.forEach((key: string) => { + if (key in this.descriptions) { + result[key] = this.descriptions[key]; + } + }); + return result; } - public async getTestTypes(...issueKeys: string[]): Promise { + public async getTestTypes(...issueKeys: string[]): Promise> { const missingTestTypes: string[] = await this.fetchFields( - this.descriptions, - this.fetchTestTypes, + this.testTypes, + this.fetchTestTypes.bind(this), ...issueKeys ); if (missingTestTypes.length > 0) { - throw new Error(`Failed to fetch test types of issues: ${missingTestTypes.join(",")}`); + logError(`Failed to fetch test types of issues:\n${missingTestTypes.join("\n")}`); } - return issueKeys.map((key) => this.testTypes[key]); + const result: StringMap = {}; + issueKeys.forEach((key: string) => { + if (key in this.testTypes) { + result[key] = this.testTypes[key]; + } + }); + return result; } protected abstract fetchSummaries(...issueKeys: string[]): Promise>; @@ -109,7 +119,7 @@ export abstract class IssueRepository< }); if (issuesWithUnparseableField.length > 0) { logWarning( - `Failed to parse the following field of the following issues: ${fieldName}\n${issuesWithUnparseableField.join( + `Failed to parse the following Jira field of the following issues: ${fieldName}\n${issuesWithUnparseableField.join( "\n" )}` ); diff --git a/src/repository/jira/issueRepositoryCloud.ts b/src/repository/jira/issueRepositoryCloud.ts index 9b72770b..af379f61 100644 --- a/src/repository/jira/issueRepositoryCloud.ts +++ b/src/repository/jira/issueRepositoryCloud.ts @@ -1,21 +1,22 @@ import { JiraClientCloud } from "../../client/jira/jiraClientCloud"; import { XrayClientCloud } from "../../client/xray/xrayClientCloud"; -import { FieldResponse, IssueRepository } from "./issueRepository"; +import { StringMap } from "../../types/util"; +import { IssueRepository } from "./issueRepository"; export class IssueRepositoryCloud extends IssueRepository { - protected async fetchSummaries(...issueKeys: string[]): Promise> { + protected async fetchSummaries(...issueKeys: string[]): Promise> { // Field property example: // summary: "Bug 12345" - return await this.getJiraField("summary", this.stringExtractor.bind(this), ...issueKeys); + return await this.getJiraField("summary", this.stringExtractor, ...issueKeys); } - protected async fetchDescriptions(...issueKeys: string[]): Promise> { + protected async fetchDescriptions(...issueKeys: string[]): Promise> { // Field property example: // description: "This is a description" return await this.getJiraField("description", this.stringExtractor, ...issueKeys); } - protected async fetchTestTypes(...issueKeys: string[]): Promise> { + protected async fetchTestTypes(...issueKeys: string[]): Promise> { const testTypes = await this.xrayClient.getTestTypes( this.options.jira.projectKey, ...issueKeys diff --git a/src/repository/jira/issueRepositoryServer.spec.ts b/src/repository/jira/issueRepositoryServer.spec.ts index 63553f89..f7ba5c14 100644 --- a/src/repository/jira/issueRepositoryServer.spec.ts +++ b/src/repository/jira/issueRepositoryServer.spec.ts @@ -293,7 +293,7 @@ describe("the server issue repository", () => { const summaries = await repository.getSummaries("CYP-123", "CYP-456", "CYP-789"); expect(stubbedWarning).to.have.been.calledOnceWithExactly( dedent(` - Failed to parse the following field of the following issues: Summary + Failed to parse the following Jira field of the following issues: Summary CYP-123 CYP-456 `) @@ -308,4 +308,612 @@ describe("the server issue repository", () => { expect(summaries).to.deep.eq({ "CYP-789": "Bonjour" }); }); }); + + describe("getDescriptions", () => { + it("fetches descriptions", async () => { + stub(client, "getFields").resolves([ + { + id: "description", + name: "Description", + custom: false, + orderable: true, + navigable: true, + searchable: true, + clauseNames: ["description"], + schema: { + type: "string", + system: "description", + }, + }, + ]); + const searchStub = stub(client, "search").resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1000", + self: "https://example.org/rest/api/2/issue/1000", + key: "CYP-123", + fields: { + description: "Very informative", + }, + }, + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1001", + self: "https://example.org/rest/api/2/issue/1001", + key: "CYP-456", + fields: { + description: "Even more informative", + }, + }, + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1002", + self: "https://example.org/rest/api/2/issue/1002", + key: "CYP-789", + fields: { + description: "Not that informative", + }, + }, + ]); + const descriptions = await repository.getDescriptions("CYP-123", "CYP-456", "CYP-789"); + expect(descriptions).to.deep.eq({ + "CYP-123": "Very informative", + "CYP-456": "Even more informative", + "CYP-789": "Not that informative", + }); + expect(searchStub).to.have.been.calledOnceWithExactly({ + jql: "project = CYP AND issue in (CYP-123,CYP-456,CYP-789)", + fields: ["description"], + }); + }); + + it("fetches descriptions only for unknown issues", async () => { + const stubbedGetFields = stub(client, "getFields").resolves([ + { + id: "description", + name: "Description", + custom: false, + orderable: true, + navigable: true, + searchable: true, + clauseNames: ["description"], + schema: { + type: "string", + system: "description", + }, + }, + ]); + const stubbedSearch = stub(client, "search"); + stubbedSearch.onFirstCall().resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1000", + self: "https://example.org/rest/api/2/issue/1000", + key: "CYP-123", + fields: { + description: "Very informative", + }, + }, + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1002", + self: "https://example.org/rest/api/2/issue/1002", + key: "CYP-789", + fields: { + description: "Not that informative", + }, + }, + ]); + stubbedSearch.onSecondCall().resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1001", + self: "https://example.org/rest/api/2/issue/1001", + key: "CYP-456", + fields: { + description: "Even more informative", + }, + }, + ]); + await repository.getDescriptions("CYP-123", "CYP-789"); + const descriptions = await repository.getDescriptions("CYP-123", "CYP-456", "CYP-789"); + expect(descriptions).to.deep.eq({ + "CYP-123": "Very informative", + "CYP-456": "Even more informative", + "CYP-789": "Not that informative", + }); + // Everything's fetched already, should not fetch anything again. + await repository.getDescriptions("CYP-123", "CYP-456", "CYP-789"); + expect(stubbedGetFields).to.have.been.calledOnce; + expect(stubbedSearch).to.have.been.calledTwice; + expect(stubbedSearch.secondCall).to.have.been.calledWithExactly({ + jql: "project = CYP AND issue in (CYP-456)", + fields: ["description"], + }); + }); + + it("displays an error for issues which do not exist", async () => { + stub(client, "getFields").resolves([ + { + id: "description", + name: "Description", + custom: false, + orderable: true, + navigable: true, + searchable: true, + clauseNames: ["description"], + schema: { + type: "string", + system: "description", + }, + }, + ]); + const stubbedSearch = stub(client, "search"); + stubbedSearch.resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1000", + self: "https://example.org/rest/api/2/issue/1000", + key: "CYP-123", + fields: { + description: "I am a description", + }, + }, + ]); + const { stubbedError } = stubLogging(); + const descriptions = await repository.getDescriptions("CYP-123", "CYP-456", "CYP-789"); + expect(stubbedError).to.have.been.calledOnceWithExactly( + dedent(` + Failed to fetch descriptions of issues: + CYP-456 + CYP-789 + `) + ); + expect(descriptions).to.deep.eq({ + "CYP-123": "I am a description", + }); + }); + + it("displays a warning when the description field does not exist", async () => { + stub(client, "getFields").resolves([]); + const stubbedSearch = stub(client, "search"); + stubbedSearch.resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1000", + self: "https://example.org/rest/api/2/issue/1000", + key: "CYP-123", + }, + ]); + const { stubbedError, stubbedWarning } = stubLogging(); + const descriptions = await repository.getDescriptions("CYP-123"); + expect(stubbedSearch).to.not.have.been.called; + expect(stubbedWarning).to.have.been.calledOnceWithExactly( + "Failed to fetch Jira field ID for field: Description" + ); + expect(stubbedError).to.have.been.calledOnceWithExactly( + dedent(` + Failed to fetch descriptions of issues: + CYP-123 + `) + ); + expect(descriptions).to.deep.eq({}); + }); + + it("handles get field failures gracefully", async () => { + stub(client, "getFields").resolves(undefined); + const stubbedSearch = stub(client, "search"); + stubbedSearch.resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1000", + self: "https://example.org/rest/api/2/issue/1000", + key: "CYP-123", + }, + ]); + const { stubbedError } = stubLogging(); + const descriptions = await repository.getDescriptions("CYP-123"); + expect(stubbedSearch).to.not.have.been.called; + expect(stubbedError).to.have.been.calledOnceWithExactly( + dedent(` + Failed to fetch descriptions of issues: + CYP-123 + `) + ); + expect(descriptions).to.deep.eq({}); + }); + + it("handles unparseable field failures gracefully", async () => { + stub(client, "getFields").resolves([ + { + id: "description", + name: "Description", + custom: false, + orderable: true, + navigable: true, + searchable: true, + clauseNames: ["description"], + schema: { + type: "string", + system: "description", + }, + }, + ]); + const stubbedSearch = stub(client, "search"); + stubbedSearch.resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1000", + self: "https://example.org/rest/api/2/issue/1000", + key: "CYP-123", + fields: { + description: ["This is a somewhat unexpected", "description"], + }, + }, + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1001", + self: "https://example.org/rest/api/2/issue/1001", + key: "CYP-456", + fields: { + description: { + Something: 5, + }, + }, + }, + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1002", + self: "https://example.org/rest/api/2/issue/1002", + key: "CYP-789", + fields: { + description: "Bonjour (encore)", + }, + }, + ]); + const { stubbedError, stubbedWarning } = stubLogging(); + const summaries = await repository.getDescriptions("CYP-123", "CYP-456", "CYP-789"); + expect(stubbedWarning).to.have.been.calledOnceWithExactly( + dedent(` + Failed to parse the following Jira field of the following issues: Description + CYP-123 + CYP-456 + `) + ); + expect(stubbedError).to.have.been.calledOnceWithExactly( + dedent(` + Failed to fetch descriptions of issues: + CYP-123 + CYP-456 + `) + ); + expect(summaries).to.deep.eq({ "CYP-789": "Bonjour (encore)" }); + }); + }); + + describe("getTestTypes", () => { + it("fetches test types", async () => { + stub(client, "getFields").resolves([ + { + id: "customfield_12100", + name: "Test Type", + custom: true, + orderable: true, + navigable: true, + searchable: true, + clauseNames: ["cf[12100]", "Test Type"], + schema: { + type: "option", + custom: "com.xpandit.plugins.xray:test-type-custom-field", + customId: 12100, + }, + }, + ]); + const searchStub = stub(client, "search").resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1000", + self: "https://example.org/rest/api/2/issue/1000", + key: "CYP-123", + fields: { + customfield_12100: { + self: "https://example.org/rest/api/2/customFieldOption/12702", + value: "Cucumber", + id: "12702", + disabled: false, + }, + }, + }, + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1001", + self: "https://example.org/rest/api/2/issue/1001", + key: "CYP-456", + fields: { + customfield_12100: { + self: "https://example.org/rest/api/2/customFieldOption/12701", + value: "Generic", + id: "12701", + disabled: false, + }, + }, + }, + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1002", + self: "https://example.org/rest/api/2/issue/1002", + key: "CYP-789", + fields: { + customfield_12100: { + self: "https://example.org/rest/api/2/customFieldOption/12700", + value: "Manual", + id: "12700", + disabled: false, + }, + }, + }, + ]); + const testTypes = await repository.getTestTypes("CYP-123", "CYP-456", "CYP-789"); + expect(testTypes).to.deep.eq({ + "CYP-123": "Cucumber", + "CYP-456": "Generic", + "CYP-789": "Manual", + }); + expect(searchStub).to.have.been.calledOnceWithExactly({ + jql: "project = CYP AND issue in (CYP-123,CYP-456,CYP-789)", + fields: ["customfield_12100"], + }); + }); + + it("fetches test types only for unknown issues", async () => { + const stubbedGetFields = stub(client, "getFields").resolves([ + { + id: "customfield_12100", + name: "Test Type", + custom: true, + orderable: true, + navigable: true, + searchable: true, + clauseNames: ["cf[12100]", "Test Type"], + schema: { + type: "option", + custom: "com.xpandit.plugins.xray:test-type-custom-field", + customId: 12100, + }, + }, + ]); + const stubbedSearch = stub(client, "search"); + stubbedSearch.onFirstCall().resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1000", + self: "https://example.org/rest/api/2/issue/1000", + key: "CYP-123", + fields: { + customfield_12100: { + self: "https://example.org/rest/api/2/customFieldOption/12702", + value: "Cucumber", + id: "12702", + disabled: false, + }, + }, + }, + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1002", + self: "https://example.org/rest/api/2/issue/1002", + key: "CYP-789", + fields: { + customfield_12100: { + self: "https://example.org/rest/api/2/customFieldOption/12700", + value: "Manual", + id: "12700", + disabled: false, + }, + }, + }, + ]); + stubbedSearch.onSecondCall().resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1001", + self: "https://example.org/rest/api/2/issue/1001", + key: "CYP-456", + fields: { + customfield_12100: { + self: "https://example.org/rest/api/2/customFieldOption/12701", + value: "Generic", + id: "12701", + disabled: false, + }, + }, + }, + ]); + await repository.getTestTypes("CYP-123", "CYP-789"); + const testTypes = await repository.getTestTypes("CYP-123", "CYP-456", "CYP-789"); + expect(testTypes).to.deep.eq({ + "CYP-123": "Cucumber", + "CYP-456": "Generic", + "CYP-789": "Manual", + }); + // Everything's fetched already, should not fetch anything again. + await repository.getTestTypes("CYP-123", "CYP-456", "CYP-789"); + expect(stubbedGetFields).to.have.been.calledOnce; + expect(stubbedSearch).to.have.been.calledTwice; + expect(stubbedSearch.secondCall).to.have.been.calledWithExactly({ + jql: "project = CYP AND issue in (CYP-456)", + fields: ["customfield_12100"], + }); + }); + + it("displays an error for issues which do not exist", async () => { + stub(client, "getFields").resolves([ + { + id: "customfield_12100", + name: "Test Type", + custom: true, + orderable: true, + navigable: true, + searchable: true, + clauseNames: ["cf[12100]", "Test Type"], + schema: { + type: "option", + custom: "com.xpandit.plugins.xray:test-type-custom-field", + customId: 12100, + }, + }, + ]); + const stubbedSearch = stub(client, "search"); + stubbedSearch.resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1000", + self: "https://example.org/rest/api/2/issue/1000", + key: "CYP-123", + fields: { + customfield_12100: { + self: "https://example.org/rest/api/2/customFieldOption/12705", + value: "Custom", + id: "12705", + disabled: false, + }, + }, + }, + ]); + const { stubbedError } = stubLogging(); + const testTypes = await repository.getTestTypes("CYP-123", "CYP-456", "CYP-789"); + expect(stubbedError).to.have.been.calledOnceWithExactly( + dedent(` + Failed to fetch test types of issues: + CYP-456 + CYP-789 + `) + ); + expect(testTypes).to.deep.eq({ + "CYP-123": "Custom", + }); + }); + + it("displays a warning when the description field does not exist", async () => { + stub(client, "getFields").resolves([]); + const stubbedSearch = stub(client, "search"); + stubbedSearch.resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1000", + self: "https://example.org/rest/api/2/issue/1000", + key: "CYP-123", + }, + ]); + const { stubbedError, stubbedWarning } = stubLogging(); + const testTypes = await repository.getTestTypes("CYP-123"); + expect(stubbedSearch).to.not.have.been.called; + expect(stubbedWarning).to.have.been.calledOnceWithExactly( + "Failed to fetch Jira field ID for field: Test Type" + ); + expect(stubbedError).to.have.been.calledOnceWithExactly( + dedent(` + Failed to fetch test types of issues: + CYP-123 + `) + ); + expect(testTypes).to.deep.eq({}); + }); + + it("handles get field failures gracefully", async () => { + stub(client, "getFields").resolves(undefined); + const stubbedSearch = stub(client, "search"); + stubbedSearch.resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1000", + self: "https://example.org/rest/api/2/issue/1000", + key: "CYP-123", + }, + ]); + const { stubbedError } = stubLogging(); + const testTypes = await repository.getTestTypes("CYP-123"); + expect(stubbedSearch).to.not.have.been.called; + expect(stubbedError).to.have.been.calledOnceWithExactly( + dedent(` + Failed to fetch test types of issues: + CYP-123 + `) + ); + expect(testTypes).to.deep.eq({}); + }); + + it("handles unparseable field failures gracefully", async () => { + stub(client, "getFields").resolves([ + { + id: "customfield_12100", + name: "Test Type", + custom: true, + orderable: true, + navigable: true, + searchable: true, + clauseNames: ["cf[12100]", "Test Type"], + schema: { + type: "option", + custom: "com.xpandit.plugins.xray:test-type-custom-field", + customId: 12100, + }, + }, + ]); + const stubbedSearch = stub(client, "search"); + stubbedSearch.resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1000", + self: "https://example.org/rest/api/2/issue/1000", + key: "CYP-123", + fields: { + customfield_12100: ["This is a somewhat unexpected", "description"], + }, + }, + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1001", + self: "https://example.org/rest/api/2/issue/1001", + key: "CYP-456", + fields: { + customfield_12100: { + Something: 5, + }, + }, + }, + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1002", + self: "https://example.org/rest/api/2/issue/1002", + key: "CYP-789", + fields: { + customfield_12100: { + self: "https://example.org/rest/api/2/customFieldOption/12701", + value: "Generic", + id: "12701", + disabled: false, + }, + }, + }, + ]); + const { stubbedError, stubbedWarning } = stubLogging(); + const testTypes = await repository.getTestTypes("CYP-123", "CYP-456", "CYP-789"); + expect(stubbedWarning).to.have.been.calledOnceWithExactly( + dedent(` + Failed to parse the following Jira field of the following issues: Test Type + CYP-123 + CYP-456 + `) + ); + expect(stubbedError).to.have.been.calledOnceWithExactly( + dedent(` + Failed to fetch test types of issues: + CYP-123 + CYP-456 + `) + ); + expect(testTypes).to.deep.eq({ "CYP-789": "Generic" }); + }); + }); }); From 6744152bad8221112458c6661d7644953fad9bb1 Mon Sep 17 00:00:00 2001 From: Sebastian Vollbrecht Date: Wed, 26 Jul 2023 20:32:12 +0200 Subject: [PATCH 06/19] Rename repository classes --- .../{issueRepository.ts => jiraRepository.ts} | 11 +-- ...ositoryCloud.ts => jiraRepositoryCloud.ts} | 4 +- ...r.spec.ts => jiraRepositoryServer.spec.ts} | 89 ++++++++++--------- ...itoryServer.ts => jiraRepositoryServer.ts} | 4 +- 4 files changed, 58 insertions(+), 50 deletions(-) rename src/repository/jira/{issueRepository.ts => jiraRepository.ts} (95%) rename src/repository/jira/{issueRepositoryCloud.ts => jiraRepositoryCloud.ts} (87%) rename src/repository/jira/{issueRepositoryServer.spec.ts => jiraRepositoryServer.spec.ts} (92%) rename src/repository/jira/{issueRepositoryServer.ts => jiraRepositoryServer.ts} (87%) diff --git a/src/repository/jira/issueRepository.ts b/src/repository/jira/jiraRepository.ts similarity index 95% rename from src/repository/jira/issueRepository.ts rename to src/repository/jira/jiraRepository.ts index 6d541d6e..8a792233 100644 --- a/src/repository/jira/issueRepository.ts +++ b/src/repository/jira/jiraRepository.ts @@ -7,12 +7,12 @@ import { IssueCloud, IssueServer } from "../../types/jira/responses/issue"; import { Options } from "../../types/plugin"; import { StringMap } from "../../types/util"; -export abstract class IssueRepository< - XrayClientType extends XrayClientServer | XrayClientCloud, - JiraClientType extends JiraClientServer | JiraClientCloud +export abstract class JiraRepository< + JiraClientType extends JiraClientServer | JiraClientCloud, + XrayClientType extends XrayClientServer | XrayClientCloud > { - protected readonly xrayClient: XrayClientType; protected readonly jiraClient: JiraClientType; + protected readonly xrayClient: XrayClientType; protected readonly options: Options; private readonly fieldIds: StringMap = {}; @@ -20,8 +20,9 @@ export abstract class IssueRepository< private readonly descriptions: StringMap = {}; private readonly testTypes: StringMap = {}; - constructor(jiraClient: JiraClientType, options: Options) { + constructor(jiraClient: JiraClientType, xrayClient: XrayClientType, options: Options) { this.jiraClient = jiraClient; + this.xrayClient = xrayClient; this.options = options; } diff --git a/src/repository/jira/issueRepositoryCloud.ts b/src/repository/jira/jiraRepositoryCloud.ts similarity index 87% rename from src/repository/jira/issueRepositoryCloud.ts rename to src/repository/jira/jiraRepositoryCloud.ts index af379f61..e9c63a6e 100644 --- a/src/repository/jira/issueRepositoryCloud.ts +++ b/src/repository/jira/jiraRepositoryCloud.ts @@ -1,9 +1,9 @@ import { JiraClientCloud } from "../../client/jira/jiraClientCloud"; import { XrayClientCloud } from "../../client/xray/xrayClientCloud"; import { StringMap } from "../../types/util"; -import { IssueRepository } from "./issueRepository"; +import { JiraRepository } from "./jiraRepository"; -export class IssueRepositoryCloud extends IssueRepository { +export class JiraRepositoryCloud extends JiraRepository { protected async fetchSummaries(...issueKeys: string[]): Promise> { // Field property example: // summary: "Bug 12345" diff --git a/src/repository/jira/issueRepositoryServer.spec.ts b/src/repository/jira/jiraRepositoryServer.spec.ts similarity index 92% rename from src/repository/jira/issueRepositoryServer.spec.ts rename to src/repository/jira/jiraRepositoryServer.spec.ts index f7ba5c14..621394e9 100644 --- a/src/repository/jira/issueRepositoryServer.spec.ts +++ b/src/repository/jira/jiraRepositoryServer.spec.ts @@ -4,14 +4,16 @@ import { stub } from "sinon"; import { stubLogging } from "../../../test/util"; import { PATCredentials } from "../../authentication/credentials"; import { JiraClientServer } from "../../client/jira/jiraClientServer"; +import { XrayClientServer } from "../../client/xray/xrayClientServer"; import { initOptions } from "../../context"; import { InternalOptions } from "../../types/plugin"; -import { IssueRepositoryServer } from "./issueRepositoryServer"; +import { JiraRepositoryServer } from "./jiraRepositoryServer"; describe("the server issue repository", () => { let options: InternalOptions; - let client: JiraClientServer; - let repository: IssueRepositoryServer; + let xrayClient: XrayClientServer; + let jiraClient: JiraClientServer; + let repository: JiraRepositoryServer; beforeEach(() => { options = initOptions( @@ -23,13 +25,18 @@ describe("the server issue repository", () => { }, } ); - client = new JiraClientServer("https://example.org", new PATCredentials("token")); - repository = new IssueRepositoryServer(client, options); + jiraClient = new JiraClientServer("https://example.org", new PATCredentials("token")); + xrayClient = new XrayClientServer( + "https://example.org", + new PATCredentials("token"), + jiraClient + ); + repository = new JiraRepositoryServer(jiraClient, xrayClient, options); }); describe("getSummaries", () => { it("fetches summaries", async () => { - stub(client, "getFields").resolves([ + stub(jiraClient, "getFields").resolves([ { id: "summary", name: "Summary", @@ -44,7 +51,7 @@ describe("the server issue repository", () => { }, }, ]); - const searchStub = stub(client, "search").resolves([ + const searchStub = stub(jiraClient, "search").resolves([ { expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", id: "1000", @@ -86,7 +93,7 @@ describe("the server issue repository", () => { }); it("fetches summaries only for unknown issues", async () => { - const stubbedGetFields = stub(client, "getFields").resolves([ + const stubbedGetFields = stub(jiraClient, "getFields").resolves([ { id: "summary", name: "Summary", @@ -101,7 +108,7 @@ describe("the server issue repository", () => { }, }, ]); - const stubbedSearch = stub(client, "search"); + const stubbedSearch = stub(jiraClient, "search"); stubbedSearch.onFirstCall().resolves([ { expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", @@ -151,7 +158,7 @@ describe("the server issue repository", () => { }); it("displays an error for issues which do not exist", async () => { - stub(client, "getFields").resolves([ + stub(jiraClient, "getFields").resolves([ { id: "summary", name: "Summary", @@ -166,7 +173,7 @@ describe("the server issue repository", () => { }, }, ]); - const stubbedSearch = stub(client, "search"); + const stubbedSearch = stub(jiraClient, "search"); stubbedSearch.resolves([ { expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", @@ -193,8 +200,8 @@ describe("the server issue repository", () => { }); it("displays a warning when the summary field does not exist", async () => { - stub(client, "getFields").resolves([]); - const stubbedSearch = stub(client, "search"); + stub(jiraClient, "getFields").resolves([]); + const stubbedSearch = stub(jiraClient, "search"); stubbedSearch.resolves([ { expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", @@ -219,8 +226,8 @@ describe("the server issue repository", () => { }); it("handles get field failures gracefully", async () => { - stub(client, "getFields").resolves(undefined); - const stubbedSearch = stub(client, "search"); + stub(jiraClient, "getFields").resolves(undefined); + const stubbedSearch = stub(jiraClient, "search"); stubbedSearch.resolves([ { expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", @@ -242,7 +249,7 @@ describe("the server issue repository", () => { }); it("handles unparseable field failures gracefully", async () => { - stub(client, "getFields").resolves([ + stub(jiraClient, "getFields").resolves([ { id: "summary", name: "Summary", @@ -257,7 +264,7 @@ describe("the server issue repository", () => { }, }, ]); - const stubbedSearch = stub(client, "search"); + const stubbedSearch = stub(jiraClient, "search"); stubbedSearch.resolves([ { expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", @@ -311,7 +318,7 @@ describe("the server issue repository", () => { describe("getDescriptions", () => { it("fetches descriptions", async () => { - stub(client, "getFields").resolves([ + stub(jiraClient, "getFields").resolves([ { id: "description", name: "Description", @@ -326,7 +333,7 @@ describe("the server issue repository", () => { }, }, ]); - const searchStub = stub(client, "search").resolves([ + const searchStub = stub(jiraClient, "search").resolves([ { expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", id: "1000", @@ -368,7 +375,7 @@ describe("the server issue repository", () => { }); it("fetches descriptions only for unknown issues", async () => { - const stubbedGetFields = stub(client, "getFields").resolves([ + const stubbedGetFields = stub(jiraClient, "getFields").resolves([ { id: "description", name: "Description", @@ -383,7 +390,7 @@ describe("the server issue repository", () => { }, }, ]); - const stubbedSearch = stub(client, "search"); + const stubbedSearch = stub(jiraClient, "search"); stubbedSearch.onFirstCall().resolves([ { expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", @@ -433,7 +440,7 @@ describe("the server issue repository", () => { }); it("displays an error for issues which do not exist", async () => { - stub(client, "getFields").resolves([ + stub(jiraClient, "getFields").resolves([ { id: "description", name: "Description", @@ -448,7 +455,7 @@ describe("the server issue repository", () => { }, }, ]); - const stubbedSearch = stub(client, "search"); + const stubbedSearch = stub(jiraClient, "search"); stubbedSearch.resolves([ { expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", @@ -475,8 +482,8 @@ describe("the server issue repository", () => { }); it("displays a warning when the description field does not exist", async () => { - stub(client, "getFields").resolves([]); - const stubbedSearch = stub(client, "search"); + stub(jiraClient, "getFields").resolves([]); + const stubbedSearch = stub(jiraClient, "search"); stubbedSearch.resolves([ { expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", @@ -501,8 +508,8 @@ describe("the server issue repository", () => { }); it("handles get field failures gracefully", async () => { - stub(client, "getFields").resolves(undefined); - const stubbedSearch = stub(client, "search"); + stub(jiraClient, "getFields").resolves(undefined); + const stubbedSearch = stub(jiraClient, "search"); stubbedSearch.resolves([ { expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", @@ -524,7 +531,7 @@ describe("the server issue repository", () => { }); it("handles unparseable field failures gracefully", async () => { - stub(client, "getFields").resolves([ + stub(jiraClient, "getFields").resolves([ { id: "description", name: "Description", @@ -539,7 +546,7 @@ describe("the server issue repository", () => { }, }, ]); - const stubbedSearch = stub(client, "search"); + const stubbedSearch = stub(jiraClient, "search"); stubbedSearch.resolves([ { expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", @@ -593,7 +600,7 @@ describe("the server issue repository", () => { describe("getTestTypes", () => { it("fetches test types", async () => { - stub(client, "getFields").resolves([ + stub(jiraClient, "getFields").resolves([ { id: "customfield_12100", name: "Test Type", @@ -609,7 +616,7 @@ describe("the server issue repository", () => { }, }, ]); - const searchStub = stub(client, "search").resolves([ + const searchStub = stub(jiraClient, "search").resolves([ { expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", id: "1000", @@ -666,7 +673,7 @@ describe("the server issue repository", () => { }); it("fetches test types only for unknown issues", async () => { - const stubbedGetFields = stub(client, "getFields").resolves([ + const stubbedGetFields = stub(jiraClient, "getFields").resolves([ { id: "customfield_12100", name: "Test Type", @@ -682,7 +689,7 @@ describe("the server issue repository", () => { }, }, ]); - const stubbedSearch = stub(client, "search"); + const stubbedSearch = stub(jiraClient, "search"); stubbedSearch.onFirstCall().resolves([ { expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", @@ -747,7 +754,7 @@ describe("the server issue repository", () => { }); it("displays an error for issues which do not exist", async () => { - stub(client, "getFields").resolves([ + stub(jiraClient, "getFields").resolves([ { id: "customfield_12100", name: "Test Type", @@ -763,7 +770,7 @@ describe("the server issue repository", () => { }, }, ]); - const stubbedSearch = stub(client, "search"); + const stubbedSearch = stub(jiraClient, "search"); stubbedSearch.resolves([ { expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", @@ -795,8 +802,8 @@ describe("the server issue repository", () => { }); it("displays a warning when the description field does not exist", async () => { - stub(client, "getFields").resolves([]); - const stubbedSearch = stub(client, "search"); + stub(jiraClient, "getFields").resolves([]); + const stubbedSearch = stub(jiraClient, "search"); stubbedSearch.resolves([ { expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", @@ -821,8 +828,8 @@ describe("the server issue repository", () => { }); it("handles get field failures gracefully", async () => { - stub(client, "getFields").resolves(undefined); - const stubbedSearch = stub(client, "search"); + stub(jiraClient, "getFields").resolves(undefined); + const stubbedSearch = stub(jiraClient, "search"); stubbedSearch.resolves([ { expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", @@ -844,7 +851,7 @@ describe("the server issue repository", () => { }); it("handles unparseable field failures gracefully", async () => { - stub(client, "getFields").resolves([ + stub(jiraClient, "getFields").resolves([ { id: "customfield_12100", name: "Test Type", @@ -860,7 +867,7 @@ describe("the server issue repository", () => { }, }, ]); - const stubbedSearch = stub(client, "search"); + const stubbedSearch = stub(jiraClient, "search"); stubbedSearch.resolves([ { expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", diff --git a/src/repository/jira/issueRepositoryServer.ts b/src/repository/jira/jiraRepositoryServer.ts similarity index 87% rename from src/repository/jira/issueRepositoryServer.ts rename to src/repository/jira/jiraRepositoryServer.ts index 2ae5809c..e046739c 100644 --- a/src/repository/jira/issueRepositoryServer.ts +++ b/src/repository/jira/jiraRepositoryServer.ts @@ -1,9 +1,9 @@ import { JiraClientServer } from "../../client/jira/jiraClientServer"; import { XrayClientServer } from "../../client/xray/xrayClientServer"; import { StringMap } from "../../types/util"; -import { IssueRepository } from "./issueRepository"; +import { JiraRepository } from "./jiraRepository"; -export class IssueRepositoryServer extends IssueRepository { +export class JiraRepositoryServer extends JiraRepository { protected async fetchSummaries(...issueKeys: string[]): Promise> { // Field property example: // summary: "Bug 12345" From 82509071afce3dc72841a8c09d72e23c1e94b4b5 Mon Sep 17 00:00:00 2001 From: Sebastian Vollbrecht Date: Thu, 27 Jul 2023 23:03:35 +0200 Subject: [PATCH 07/19] Update Jira repository workflows --- src/client/xray/xrayClient.spec.ts | 2 +- src/client/xray/xrayClient.ts | 27 +- src/client/xray/xrayClientCloud.spec.ts | 6 +- src/client/xray/xrayClientCloud.ts | 16 +- src/client/xray/xrayClientServer.spec.ts | 280 +------- src/client/xray/xrayClientServer.ts | 77 +-- src/context.ts | 57 +- src/hooks.ts | 14 +- src/plugin.ts | 18 +- src/repository/jira/jiraRepository.ts | 241 ++++--- .../jira/jiraRepositoryCloud.spec.ts | 651 ++++++++++++++++++ src/repository/jira/jiraRepositoryCloud.ts | 8 +- .../jira/jiraRepositoryServer.spec.ts | 30 +- src/repository/jira/jiraRepositoryServer.ts | 14 +- src/types/plugin.ts | 3 + src/types/util.ts | 20 + test/util.ts | 4 +- 17 files changed, 928 insertions(+), 540 deletions(-) create mode 100644 src/repository/jira/jiraRepositoryCloud.spec.ts diff --git a/src/client/xray/xrayClient.spec.ts b/src/client/xray/xrayClient.spec.ts index e10b8202..c2a4205f 100644 --- a/src/client/xray/xrayClient.spec.ts +++ b/src/client/xray/xrayClient.spec.ts @@ -35,7 +35,7 @@ describe("the xray clients", () => { new BasicAuthCredentials("user", "token"), new DummyJiraClient() ) - : new XrayClientCloud(RESOLVED_JWT_CREDENTIALS, new DummyJiraClient()); + : new XrayClientCloud(RESOLVED_JWT_CREDENTIALS); }); describe("import execution", () => { diff --git a/src/client/xray/xrayClient.ts b/src/client/xray/xrayClient.ts index d8f3af6b..f7636fec 100644 --- a/src/client/xray/xrayClient.ts +++ b/src/client/xray/xrayClient.ts @@ -8,7 +8,7 @@ import { } from "../../authentication/credentials"; import { Requests } from "../../https/requests"; import { logError, logInfo, logSuccess, logWarning, writeErrorFile } from "../../logging/logging"; -import { OneOf, StringMap } from "../../types/util"; +import { OneOf } from "../../types/util"; import { XrayTestExecutionResultsCloud, XrayTestExecutionResultsServer, @@ -16,33 +16,24 @@ import { import { CucumberMultipartFeature } from "../../types/xray/requests/importExecutionCucumberMultipart"; import { ExportCucumberTestsResponse } from "../../types/xray/responses/exportFeature"; import { Client } from "../client"; -import { JiraClientCloud } from "../jira/jiraClientCloud"; -import { JiraClientServer } from "../jira/jiraClientServer"; /** * An abstract Xray client class for communicating with Xray instances. */ export abstract class XrayClient< CredentialsType extends BasicAuthCredentials | PATCredentials | JWTCredentials, - JiraClientType extends JiraClientServer | JiraClientCloud, ImportFeatureResponseType, ImportExecutionResponseType, CucumberMultipartInfoType > extends Client { - /** - * The configured Jira client. - */ - protected readonly jiraClient: JiraClientType; /** * Construct a new client using the provided credentials. * * @param apiBaseUrl the base URL for all HTTP requests * @param credentials the credentials to use during authentication - * @param jiraClient the configured Jira client */ - constructor(apiBaseUrl: string, credentials: CredentialsType, jiraClient: JiraClientType) { + constructor(apiBaseUrl: string, credentials: CredentialsType) { super(apiBaseUrl, credentials); - this.jiraClient = jiraClient; } /** * Uploads test results to the Xray instance. @@ -292,18 +283,4 @@ export abstract class XrayClient< public abstract handleResponseImportExecutionCucumberMultipart( response: ImportExecutionResponseType ): string; - - /** - * Returns Xray test types for the provided test issues, such as `Manual`, `Cucumber` or - * `Generic`. - * - * @param projectKey key of the project containing the test issues - * @param issueKeys the keys of the test issues to retrieve test types for - * @returns a promise which will contain the mapping of issues to test types, `null` if the - * upload was skipped or `undefined` in case of errors - */ - public abstract getTestTypes( - projectKey: string, - ...issueKeys: string[] - ): Promise>; } diff --git a/src/client/xray/xrayClientCloud.spec.ts b/src/client/xray/xrayClientCloud.spec.ts index 4bf97a36..49eb1933 100644 --- a/src/client/xray/xrayClientCloud.spec.ts +++ b/src/client/xray/xrayClientCloud.spec.ts @@ -3,7 +3,6 @@ import { expect } from "chai"; import dedent from "dedent"; import fs from "fs"; import { - DummyJiraClient, RESOLVED_JWT_CREDENTIALS, resolveTestDirPath, stubLogging, @@ -13,10 +12,7 @@ import { GetTestsResponse } from "../../types/xray/responses/graphql/getTests"; import { XrayClientCloud } from "./xrayClientCloud"; describe("the xray cloud client", () => { - const client: XrayClientCloud = new XrayClientCloud( - RESOLVED_JWT_CREDENTIALS, - new DummyJiraClient() - ); + const client: XrayClientCloud = new XrayClientCloud(RESOLVED_JWT_CREDENTIALS); describe("import execution", () => { it("should handle successful responses", async () => { diff --git a/src/client/xray/xrayClientCloud.ts b/src/client/xray/xrayClientCloud.ts index 8267f235..9b18c6b0 100644 --- a/src/client/xray/xrayClientCloud.ts +++ b/src/client/xray/xrayClientCloud.ts @@ -7,7 +7,6 @@ import { CucumberMultipartInfoCloud } from "../../types/xray/requests/importExec import { GetTestsResponse } from "../../types/xray/responses/graphql/getTests"; import { ImportExecutionResponseCloud } from "../../types/xray/responses/importExecution"; import { ImportFeatureResponseCloud, IssueDetails } from "../../types/xray/responses/importFeature"; -import { JiraClientCloud } from "../jira/jiraClientCloud"; import { XrayClient } from "./xrayClient"; type GetTestsJiraData = { @@ -16,7 +15,6 @@ type GetTestsJiraData = { export class XrayClientCloud extends XrayClient< JWTCredentials, - JiraClientCloud, ImportFeatureResponseCloud, ImportExecutionResponseCloud, CucumberMultipartInfoCloud @@ -38,10 +36,9 @@ export class XrayClientCloud extends XrayClient< * Construct a new Xray cloud client using the provided credentials. * * @param credentials the credentials to use during authentication - * @param jiraClient the configured Jira client */ - constructor(credentials: JWTCredentials, jiraClient: JiraClientCloud) { - super(XrayClientCloud.URL, credentials, jiraClient); + constructor(credentials: JWTCredentials) { + super(XrayClientCloud.URL, credentials); } public getUrlImportExecution(): string { @@ -97,6 +94,15 @@ export class XrayClientCloud extends XrayClient< } } + /** + * Returns Xray test types for the provided test issues, such as `Manual`, `Cucumber` or + * `Generic`. + * + * @param projectKey key of the project containing the test issues + * @param issueKeys the keys of the test issues to retrieve test types for + * @returns a promise which will contain the mapping of issues to test types, `null` if the + * upload was skipped or `undefined` in case of errors + */ public async getTestTypes( projectKey: string, ...issueKeys: string[] diff --git a/src/client/xray/xrayClientServer.spec.ts b/src/client/xray/xrayClientServer.spec.ts index 5c5a9b81..1683ebac 100644 --- a/src/client/xray/xrayClientServer.spec.ts +++ b/src/client/xray/xrayClientServer.spec.ts @@ -1,8 +1,7 @@ -import { AxiosError, AxiosHeaders, HttpStatusCode } from "axios"; +import { HttpStatusCode } from "axios"; import { expect } from "chai"; -import dedent from "dedent"; import fs from "fs"; -import { resolveTestDirPath, stubLogging, stubRequests } from "../../../test/util"; +import { stubLogging, stubRequests } from "../../../test/util"; import { BasicAuthCredentials } from "../../authentication/credentials"; import { JiraClientServer } from "../jira/jiraClientServer"; import { XrayClientServer } from "./xrayClientServer"; @@ -105,281 +104,6 @@ describe("the xray server client", () => { }); }); - describe("get test types", () => { - it("should handle successful responses", async () => { - const { stubbedInfo, stubbedSuccess } = stubLogging(); - const { stubbedGet, stubbedPost } = stubRequests(); - stubbedGet.onFirstCall().resolves({ - status: HttpStatusCode.Ok, - data: JSON.parse( - fs.readFileSync( - "./test/resources/fixtures/jira/responses/getFields.json", - "utf-8" - ) - ), - headers: null, - statusText: HttpStatusCode[HttpStatusCode.Ok], - config: null, - }); - stubbedPost.onFirstCall().resolves({ - status: HttpStatusCode.Ok, - data: JSON.parse( - fs.readFileSync("./test/resources/fixtures/jira/responses/search.json", "utf-8") - ), - headers: null, - statusText: HttpStatusCode[HttpStatusCode.Ok], - config: null, - }); - const response = await client.getTestTypes("CYP", "CYP-237", "CYP-333", "CYP-338"); - expect(response["CYP-332"]).to.eq("Manual"); - expect(response["CYP-237"]).to.eq("Manual"); - expect(response["CYP-338"]).to.eq("Cucumber"); - expect(stubbedInfo).to.have.been.calledWithExactly("Retrieving test types..."); - expect(stubbedSuccess).to.have.been.calledWithExactly( - "Successfully retrieved test types for 3 issues" - ); - }); - - it("should throw for missing test types", async () => { - const { stubbedError } = stubLogging(); - const { stubbedGet, stubbedPost } = stubRequests(); - stubbedGet.onFirstCall().resolves({ - status: HttpStatusCode.Ok, - data: JSON.parse( - fs.readFileSync( - "./test/resources/fixtures/jira/responses/getFields.json", - "utf-8" - ) - ), - headers: null, - statusText: HttpStatusCode[HttpStatusCode.Ok], - config: null, - }); - stubbedPost.onFirstCall().resolves({ - status: HttpStatusCode.Ok, - data: JSON.parse( - fs.readFileSync("./test/resources/fixtures/jira/responses/search.json", "utf-8") - ), - headers: null, - statusText: HttpStatusCode[HttpStatusCode.Ok], - config: null, - }); - const response = await client.getTestTypes("CYP", "CYP-12345", "CYP-333", "CYP-67890"); - expect(response).to.be.undefined; - expect(stubbedError).to.have.been.calledTwice; - expect(stubbedError).to.have.been.calledWith( - dedent(` - Failed to get test types: Error: Failed to retrieve test types for issues: - - CYP-12345 - CYP-67890 - - Make sure these issues exist and are actually test issues - `) - ); - }); - - it("should handle bad field responses", async () => { - const { stubbedGet } = stubRequests(); - const { stubbedError } = stubLogging(); - stubbedGet.onFirstCall().rejects( - new AxiosError("Request failed with status code 401", "401", null, null, { - status: 401, - statusText: "Bad Request", - config: { headers: new AxiosHeaders() }, - headers: {}, - data: { - error: "Unauthorized", - }, - }) - ); - const response = await client.getTestTypes("CYP", "CYP-330", "CYP-331", "CYP-332"); - expect(response).to.be.undefined; - expect(stubbedError).to.have.callCount(4); - expect(stubbedError.getCall(0)).to.have.been.calledWith( - "Failed to get fields: AxiosError: Request failed with status code 401" - ); - expect(stubbedError.getCall(1)).to.have.been.calledWith( - `Complete error logs have been written to: ${resolveTestDirPath( - "getFieldsError.json" - )}` - ); - expect(stubbedError.getCall(2)).to.have.been.calledWith( - "Failed to get test types: Error: Failed to fetch Jira fields" - ); - expect(stubbedError.getCall(3)).to.have.been.calledWith( - `Complete error logs have been written to: ${resolveTestDirPath( - "getTestTypesError.json" - )}` - ); - }); - - it("should handle field responses without the test type field", async () => { - const { stubbedGet } = stubRequests(); - const { stubbedError } = stubLogging(); - stubbedGet.onFirstCall().resolves({ - status: HttpStatusCode.Ok, - data: JSON.parse( - fs.readFileSync( - "./test/resources/fixtures/jira/responses/getFieldsNoTestType.json", - "utf-8" - ) - ), - headers: null, - statusText: HttpStatusCode[HttpStatusCode.Ok], - config: null, - }); - const response = await client.getTestTypes("CYP", "CYP-330", "CYP-331", "CYP-332"); - expect(response).to.be.undefined; - expect(stubbedError).to.have.callCount(2); - expect(stubbedError.getCall(0)).to.have.been.calledWith( - "Failed to get test types: Error: Jira field does not exist: Test Type" - ); - expect(stubbedError.getCall(1)).to.have.been.calledWith( - `Complete error logs have been written to: ${resolveTestDirPath( - "getTestTypesError.json" - )}` - ); - }); - - it("should handle bad search responses", async () => { - const { stubbedGet, stubbedPost } = stubRequests(); - const { stubbedError } = stubLogging(); - stubbedGet.onFirstCall().resolves({ - status: HttpStatusCode.Ok, - data: JSON.parse( - fs.readFileSync( - "./test/resources/fixtures/jira/responses/getFields.json", - "utf-8" - ) - ), - headers: null, - statusText: HttpStatusCode[HttpStatusCode.Ok], - config: null, - }); - stubbedPost.onFirstCall().rejects( - new AxiosError("Request failed with status code 400", "400", null, null, { - status: 400, - statusText: "Bad Request", - config: { headers: new AxiosHeaders() }, - headers: {}, - data: { - error: "Must provide a project key", - }, - }) - ); - const response = await client.getTestTypes("CYP", "CYP-330", "CYP-331", "CYP-332"); - expect(response).to.be.undefined; - expect(stubbedError).to.have.callCount(4); - expect(stubbedError.getCall(0)).to.have.been.calledWith( - "Failed to search issues: AxiosError: Request failed with status code 400" - ); - let expectedPath = resolveTestDirPath("searchError.json"); - expect(stubbedError.getCall(1)).to.have.been.calledWith( - `Complete error logs have been written to: ${expectedPath}` - ); - expect(stubbedError.getCall(2)).to.have.been.calledWith( - "Failed to get test types: Error: Successfully retrieved test type field data, but failed to search issues" - ); - expectedPath = resolveTestDirPath("getTestTypesError.json"); - expect(stubbedError.getCall(3)).to.have.been.calledWith( - `Complete error logs have been written to: ${expectedPath}` - ); - }); - - it("should handle search responses without fields", async () => { - const { stubbedGet, stubbedPost } = stubRequests(); - const { stubbedError } = stubLogging(); - stubbedGet.onFirstCall().resolves({ - status: HttpStatusCode.Ok, - data: JSON.parse( - fs.readFileSync( - "./test/resources/fixtures/jira/responses/getFields.json", - "utf-8" - ) - ), - headers: null, - statusText: HttpStatusCode[HttpStatusCode.Ok], - config: null, - }); - stubbedPost.onFirstCall().resolves({ - status: HttpStatusCode.Ok, - data: JSON.parse( - fs.readFileSync( - "./test/resources/fixtures/jira/responses/searchNoFields.json", - "utf-8" - ) - ), - headers: null, - statusText: HttpStatusCode[HttpStatusCode.Ok], - config: null, - }); - const response = await client.getTestTypes("CYP", "CYP-333"); - expect(response).to.be.undefined; - expect(stubbedError).to.have.been.calledTwice; - expect(stubbedError).to.have.been.calledWith( - dedent(` - Failed to get test types: Error: Failed to retrieve test types for issues: - - CYP-333 - - Make sure these issues exist and are actually test issues - `) - ); - }); - - it("should handle search responses with incorrect fields", async () => { - const { stubbedGet, stubbedPost } = stubRequests(); - const { stubbedError } = stubLogging(); - stubbedGet.onFirstCall().resolves({ - status: HttpStatusCode.Ok, - data: JSON.parse( - fs.readFileSync( - "./test/resources/fixtures/jira/responses/getFields.json", - "utf-8" - ) - ), - headers: null, - statusText: HttpStatusCode[HttpStatusCode.Ok], - config: null, - }); - stubbedPost.onFirstCall().resolves({ - status: HttpStatusCode.Ok, - data: JSON.parse( - fs.readFileSync( - "./test/resources/fixtures/jira/responses/searchIncorrectFields.json", - "utf-8" - ) - ), - headers: null, - statusText: HttpStatusCode[HttpStatusCode.Ok], - config: null, - }); - const response = await client.getTestTypes("CYP", "CYP-123", "CYP-456"); - expect(response).to.be.undefined; - expect(stubbedError).to.have.been.calledTwice; - expect(stubbedError).to.have.been.calledWith( - dedent(` - Failed to get test types: Error: Failed to retrieve test types for issues: - - CYP-123 - CYP-456 - - Make sure these issues exist and are actually test issues - `) - ); - }); - - it("should skip empty issues", async () => { - const { stubbedWarning } = stubLogging(); - const response = await client.getTestTypes("CYP"); - expect(response).to.be.null; - expect(stubbedWarning).to.have.been.calledWithExactly( - "No issue keys provided. Skipping test type retrieval" - ); - }); - }); - describe("the urls", () => { describe("export cucumber", () => { it("keys", () => { diff --git a/src/client/xray/xrayClientServer.ts b/src/client/xray/xrayClientServer.ts index 904f954a..b0bbfe4c 100644 --- a/src/client/xray/xrayClientServer.ts +++ b/src/client/xray/xrayClientServer.ts @@ -1,8 +1,5 @@ -import dedent from "dedent"; import { BasicAuthCredentials, PATCredentials } from "../../authentication/credentials"; -import { logError, logInfo, logSuccess, logWarning, writeErrorFile } from "../../logging/logging"; -import { FieldDetailServer } from "../../types/jira/responses/fieldDetail"; -import { StringMap } from "../../types/util"; +import { logError, logSuccess } from "../../logging/logging"; import { CucumberMultipartInfoServer } from "../../types/xray/requests/importExecutionCucumberMultipartInfo"; import { ImportExecutionResponseServer } from "../../types/xray/responses/importExecution"; import { @@ -14,11 +11,15 @@ import { XrayClient } from "./xrayClient"; export class XrayClientServer extends XrayClient< BasicAuthCredentials | PATCredentials, - JiraClientServer, ImportFeatureResponseServer, ImportExecutionResponseServer, CucumberMultipartInfoServer > { + /** + * The configured Jira client. + */ + protected readonly jiraClient: JiraClientServer; + /** * Construct a new Xray Server client using the provided credentials. * @@ -31,7 +32,8 @@ export class XrayClientServer extends XrayClient< credentials: BasicAuthCredentials | PATCredentials, jiraClient: JiraClientServer ) { - super(apiBaseUrl, credentials, jiraClient); + super(apiBaseUrl, credentials); + this.jiraClient = jiraClient; } public getUrlImportExecution(): string { @@ -88,69 +90,6 @@ export class XrayClientServer extends XrayClient< } } - public async getTestTypes( - projectKey: string, - ...issueKeys: string[] - ): Promise> { - try { - if (!issueKeys || issueKeys.length === 0) { - logWarning("No issue keys provided. Skipping test type retrieval"); - return null; - } - logInfo("Retrieving test types..."); - const progressInterval = this.startResponseInterval(this.apiBaseURL); - try { - const types = {}; - const fields = await this.jiraClient.getFields(); - if (!fields) { - throw new Error("Failed to fetch Jira fields"); - } - const testTypeField = fields.find((field: FieldDetailServer) => { - return field.name === "Test Type"; - }); - if (!testTypeField) { - throw new Error("Jira field does not exist: Test Type"); - } - const searchResults = await this.jiraClient.search({ - jql: `project = ${projectKey} AND issue in (${issueKeys.join(",")})`, - fields: [testTypeField.id], - }); - if (!searchResults) { - throw new Error( - "Successfully retrieved test type field data, but failed to search issues" - ); - } - for (const issue of searchResults) { - if (issue.fields && testTypeField.id in issue.fields) { - const testTypeData = issue.fields[testTypeField.id]; - if (typeof testTypeData === "object" && "value" in testTypeData) { - types[issue.key] = testTypeData.value; - } - } - } - const missingTypes: string[] = issueKeys.filter((key: string) => !(key in types)); - if (missingTypes.length > 0) { - throw new Error( - dedent(` - Failed to retrieve test types for issues: - - ${missingTypes.join("\n")} - - Make sure these issues exist and are actually test issues - `) - ); - } - logSuccess(`Successfully retrieved test types for ${issueKeys.length} issues`); - return types; - } finally { - clearInterval(progressInterval); - } - } catch (error: unknown) { - logError(`Failed to get test types: ${error}`); - writeErrorFile(error, "getTestTypesError"); - } - } - public getUrlImportExecutionCucumberMultipart(): string { return `${this.apiBaseURL}/rest/raven/latest/import/execution/cucumber/multipart`; } diff --git a/src/context.ts b/src/context.ts index 11472ca5..54bdc28b 100644 --- a/src/context.ts +++ b/src/context.ts @@ -38,7 +38,10 @@ import { ENV_XRAY_UPLOAD_SCREENSHOTS, } from "./constants"; import { logInfo } from "./logging/logging"; +import { JiraRepositoryCloud } from "./repository/jira/jiraRepositoryCloud"; +import { JiraRepositoryServer } from "./repository/jira/jiraRepositoryServer"; import { InternalOptions, Options, XrayStepOptions } from "./types/plugin"; +import { ClientCombination } from "./types/util"; import { asBoolean, asInt, asString, parse } from "./util/parsing"; export function initOptions(env: Cypress.ObjectLike, options: Options): InternalOptions { @@ -168,13 +171,7 @@ function verifyXraySteps(steps: XrayStepOptions) { } } -export function initClients( - options: InternalOptions, - env: Cypress.ObjectLike -): { - jiraClient: JiraClientServer | JiraClientCloud; - xrayClient: XrayClientServer | XrayClientCloud; -} { +export function initClients(options: InternalOptions, env: Cypress.ObjectLike): ClientCombination { if (!options.jira.url) { throw new Error( dedent(` @@ -183,12 +180,10 @@ export function initClients( `) ); } - let jiraClient: JiraClientServer | JiraClientCloud; - let xrayClient: XrayClientServer | XrayClientCloud; if (ENV_JIRA_USERNAME in env && ENV_JIRA_API_TOKEN in env) { // Jira cloud authentication: username (Email) and token. logInfo("Jira username and API token found. Setting up Jira cloud basic auth credentials"); - jiraClient = new JiraClientCloud( + const jiraClient = new JiraClientCloud( options.jira.url, new BasicAuthCredentials(env[ENV_JIRA_USERNAME], env[ENV_JIRA_API_TOKEN]) ); @@ -197,10 +192,14 @@ export function initClients( logInfo( "Xray client ID and client secret found. Setting up Xray cloud JWT credentials" ); - xrayClient = new XrayClientCloud( - new JWTCredentials(env[ENV_XRAY_CLIENT_ID], env[ENV_XRAY_CLIENT_SECRET]), - jiraClient + const xrayClient = new XrayClientCloud( + new JWTCredentials(env[ENV_XRAY_CLIENT_ID], env[ENV_XRAY_CLIENT_SECRET]) ); + return { + kind: "cloud", + jiraClient: jiraClient, + xrayClient: xrayClient, + }; } else { throw new Error( dedent(` @@ -212,29 +211,39 @@ export function initClients( } else if (ENV_JIRA_API_TOKEN in env && options.jira.url) { // Jira server authentication: no username, only token. logInfo("Jira PAT found. Setting up Jira server PAT credentials"); - jiraClient = new JiraClientServer( + const jiraClient = new JiraClientServer( options.jira.url, new PATCredentials(env[ENV_JIRA_API_TOKEN]) ); // Xray server authentication: no username, only token. logInfo("Jira PAT found. Setting up Xray server PAT credentials"); - xrayClient = new XrayClientServer( + const xrayClient = new XrayClientServer( options.jira.url, new PATCredentials(env[ENV_JIRA_API_TOKEN]), jiraClient ); + return { + kind: "server", + jiraClient: jiraClient, + xrayClient: xrayClient, + }; } else if (ENV_JIRA_USERNAME in env && ENV_JIRA_PASSWORD in env && options.jira.url) { logInfo("Jira username and password found. Setting up Jira server basic auth credentials"); - jiraClient = new JiraClientServer( + const jiraClient = new JiraClientServer( options.jira.url, new BasicAuthCredentials(env[ENV_JIRA_USERNAME], env[ENV_JIRA_PASSWORD]) ); logInfo("Jira username and password found. Setting up Xray server basic auth credentials"); - xrayClient = new XrayClientServer( + const xrayClient = new XrayClientServer( options.jira.url, new BasicAuthCredentials(env[ENV_JIRA_USERNAME], env[ENV_JIRA_PASSWORD]), jiraClient ); + return { + kind: "server", + jiraClient: jiraClient, + xrayClient: xrayClient, + }; } else { throw new Error( dedent(` @@ -243,8 +252,14 @@ export function initClients( `) ); } - return { - jiraClient: jiraClient, - xrayClient: xrayClient, - }; +} +export function initJiraRepository( + clients: ClientCombination, + options: Options +): JiraRepositoryServer | JiraRepositoryCloud { + if (clients.kind === "cloud") { + return new JiraRepositoryCloud(clients.jiraClient, clients.xrayClient, options); + } else { + return new JiraRepositoryServer(clients.jiraClient, clients.xrayClient, options); + } } diff --git a/src/hooks.ts b/src/hooks.ts index 06c70601..0337d064 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -17,6 +17,8 @@ import { getNativeTestIssueKeys, preprocessFeatureFile, } from "./preprocessing/preprocessing"; +import { JiraRepositoryCloud } from "./repository/jira/jiraRepositoryCloud"; +import { JiraRepositoryServer } from "./repository/jira/jiraRepositoryServer"; import { IssueTypeDetailsCloud, IssueTypeDetailsServer, @@ -179,7 +181,8 @@ export async function afterRunHook( results: CypressCommandLine.CypressRunResult | CypressCommandLine.CypressFailedRunResult, options?: InternalOptions, xrayClient?: XrayClientServer | XrayClientCloud, - jiraClient?: JiraClientServer | JiraClientCloud + jiraClient?: JiraClientServer | JiraClientCloud, + jiraRepository?: JiraRepositoryServer | JiraRepositoryCloud ) { if (!options) { // Don't throw here in case someone simply doesn't want the plugin to run but forgot to @@ -232,7 +235,7 @@ export async function afterRunHook( } let issueKey: string = null; if (containsNativeTest(runResult, options)) { - issueKey = await uploadCypressResults(runResult, options, xrayClient); + issueKey = await uploadCypressResults(runResult, options, xrayClient, jiraRepository); if ( options.jira.testExecutionIssueKey && issueKey && @@ -291,11 +294,12 @@ export async function afterRunHook( async function uploadCypressResults( runResult: CypressCommandLine.CypressRunResult, - options?: InternalOptions, - xrayClient?: XrayClientServer | XrayClientCloud + options: InternalOptions, + xrayClient: XrayClientServer | XrayClientCloud, + jiraRepository: JiraRepositoryServer | JiraRepositoryCloud ) { const issueKeys = getNativeTestIssueKeys(runResult, options); - const testTypes = await xrayClient.getTestTypes(options.jira.projectKey, ...issueKeys); + const testTypes = await jiraRepository.getTestTypes(options.jira.projectKey, ...issueKeys); options.xray.testTypes = testTypes; let cypressExecution: XrayTestExecutionResultsServer | XrayTestExecutionResultsCloud; if (xrayClient instanceof XrayClientServer) { diff --git a/src/plugin.ts b/src/plugin.ts index 92b446de..9d21dedb 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,4 +1,4 @@ -import { initClients, initOptions, verifyOptions } from "./context"; +import { initClients, initJiraRepository, initOptions, verifyOptions } from "./context"; import { afterRunHook, beforeRunHook, synchronizeFile } from "./hooks"; import { Requests } from "./https/requests"; import { initLogging, logInfo } from "./logging/logging"; @@ -13,12 +13,14 @@ export async function configureXrayPlugin(config: Cypress.PluginConfigOptions, o return; } verifyOptions(internalOptions); - const { jiraClient, xrayClient } = initClients(internalOptions, config.env); + const clients = initClients(internalOptions, config.env); + const jiraRepository = initJiraRepository(clients, options); context = { internal: internalOptions, cypress: config, - xrayClient: xrayClient, - jiraClient: jiraClient, + xrayClient: clients.xrayClient, + jiraClient: clients.jiraClient, + jiraRepository: jiraRepository, }; Requests.init(internalOptions); initLogging({ @@ -42,7 +44,13 @@ export async function addXrayResultUpload(on: Cypress.PluginEvents) { async ( results: CypressCommandLine.CypressRunResult | CypressCommandLine.CypressFailedRunResult ) => { - await afterRunHook(results, context.internal, context.xrayClient, context.jiraClient); + await afterRunHook( + results, + context.internal, + context.xrayClient, + context.jiraClient, + context.jiraRepository + ); } ); } diff --git a/src/repository/jira/jiraRepository.ts b/src/repository/jira/jiraRepository.ts index 8a792233..78afc93f 100644 --- a/src/repository/jira/jiraRepository.ts +++ b/src/repository/jira/jiraRepository.ts @@ -1,12 +1,18 @@ +import dedent from "dedent"; import { JiraClientCloud } from "../../client/jira/jiraClientCloud"; import { JiraClientServer } from "../../client/jira/jiraClientServer"; import { XrayClientCloud } from "../../client/xray/xrayClientCloud"; import { XrayClientServer } from "../../client/xray/xrayClientServer"; -import { logError, logWarning } from "../../logging/logging"; +import { logError } from "../../logging/logging"; import { IssueCloud, IssueServer } from "../../types/jira/responses/issue"; import { Options } from "../../types/plugin"; import { StringMap } from "../../types/util"; +export type FieldExtractor = { + extractorFunction: (value: unknown) => T | undefined; + expectedType: string; +}; + export abstract class JiraRepository< JiraClientType extends JiraClientServer | JiraClientCloud, XrayClientType extends XrayClientServer | XrayClientCloud @@ -15,6 +21,24 @@ export abstract class JiraRepository< protected readonly xrayClient: XrayClientType; protected readonly options: Options; + protected static readonly STRING_EXTRACTOR: FieldExtractor = { + extractorFunction: (value: unknown): string | undefined => { + if (typeof value === "string") { + return value; + } + }, + expectedType: "a string", + }; + + protected static readonly OBJECT_VALUE_EXTRACTOR: FieldExtractor = { + extractorFunction: (data: unknown): string | undefined => { + if (typeof data === "object" && data !== null) { + return data["value"]; + } + }, + expectedType: "an object with a value property", + }; + private readonly fieldIds: StringMap = {}; private readonly summaries: StringMap = {}; private readonly descriptions: StringMap = {}; @@ -26,57 +50,101 @@ export abstract class JiraRepository< this.options = options; } - public async getSummaries(...issueKeys: string[]): Promise> { - const missingSummaries: string[] = await this.fetchFields( - this.summaries, - this.fetchSummaries.bind(this), - ...issueKeys - ); - if (missingSummaries.length > 0) { - logError(`Failed to fetch summaries of issues:\n${missingSummaries.join("\n")}`); + public async getFieldId(fieldName: string): Promise { + if (!(fieldName in this.fieldIds)) { + const jiraFields = await this.jiraClient.getFields(); + if (jiraFields) { + jiraFields.forEach((jiraField) => { + this.fieldIds[jiraField.name] = jiraField.id; + }); + } + if (!(fieldName in this.fieldIds)) { + throw new Error( + `Failed to fetch Jira field ID for field: ${fieldName}\nMake sure the field actually exists` + ); + } } - const result: StringMap = {}; - issueKeys.forEach((key: string) => { - if (key in this.summaries) { - result[key] = this.summaries[key]; + return this.fieldIds[fieldName]; + } + + public async getSummaries(...issueKeys: string[]): Promise> { + let result: StringMap = {}; + try { + result = await this.fetchFields( + this.summaries, + this.fetchSummaries.bind(this), + ...issueKeys + ); + const missingSummaries: string[] = issueKeys.filter( + (key: string) => !(key in this.summaries) + ); + if (missingSummaries.length > 0) { + throw new Error(`Make sure these issues exist:\n\n${missingSummaries.join("\n")}`); } - }); + } catch (error: unknown) { + logError( + dedent(` + Failed to fetch issue summaries + ${error instanceof Error ? error.message : JSON.stringify(error)} + `) + ); + } return result; } public async getDescriptions(...issueKeys: string[]): Promise> { - const missingDescriptions: string[] = await this.fetchFields( - this.descriptions, - this.fetchDescriptions.bind(this), - ...issueKeys - ); - if (missingDescriptions.length > 0) { - logError(`Failed to fetch descriptions of issues:\n${missingDescriptions.join("\n")}`); - } - const result: StringMap = {}; - issueKeys.forEach((key: string) => { - if (key in this.descriptions) { - result[key] = this.descriptions[key]; + let result: StringMap = {}; + try { + result = await this.fetchFields( + this.descriptions, + this.fetchDescriptions.bind(this), + ...issueKeys + ); + const missingDescriptions: string[] = issueKeys.filter( + (key: string) => !(key in this.descriptions) + ); + if (missingDescriptions.length > 0) { + throw new Error( + `Make sure these issues exist:\n\n${missingDescriptions.join("\n")}` + ); } - }); + } catch (error: unknown) { + logError( + dedent(` + Failed to fetch issue descriptions + ${error instanceof Error ? error.message : JSON.stringify(error)} + `) + ); + } return result; } public async getTestTypes(...issueKeys: string[]): Promise> { - const missingTestTypes: string[] = await this.fetchFields( - this.testTypes, - this.fetchTestTypes.bind(this), - ...issueKeys - ); - if (missingTestTypes.length > 0) { - logError(`Failed to fetch test types of issues:\n${missingTestTypes.join("\n")}`); - } - const result: StringMap = {}; - issueKeys.forEach((key: string) => { - if (key in this.testTypes) { - result[key] = this.testTypes[key]; + let result: StringMap = {}; + try { + result = await this.fetchFields( + this.testTypes, + this.fetchTestTypes.bind(this), + ...issueKeys + ); + const missingTestTypes: string[] = issueKeys.filter( + (key: string) => !(key in this.testTypes) + ); + if (missingTestTypes.length > 0) { + throw new Error( + `Make sure these issues exist and are test issues:\n\n${missingTestTypes.join( + "\n" + )}` + ); } - }); + } catch (error: unknown) { + logError( + dedent(` + Failed to fetch issue test types + ${error instanceof Error ? error.message : JSON.stringify(error)} + `) + ); + } return result; } @@ -86,81 +154,70 @@ export abstract class JiraRepository< protected abstract fetchTestTypes(...issueKeys: string[]): Promise>; + protected valueExtractor(data: unknown): string | undefined { + if (typeof data === "object" && data !== null) { + return data["value"]; + } + } + protected async getJiraField( fieldName: string, - extractor: (value: unknown) => T, + extractor: FieldExtractor, ...issueKeys: string[] ): Promise> { + const fieldId = await this.getFieldId(fieldName); + const issues: IssueServer[] | IssueCloud[] = await this.jiraClient.search({ + jql: `project = ${this.options.jira.projectKey} AND issue in (${issueKeys.join(",")})`, + fields: [fieldId], + }); const results: StringMap = {}; - if (!(fieldName in this.fieldIds)) { - const jiraFields = await this.jiraClient.getFields(); - if (!jiraFields) { - return results; - } - jiraFields.forEach((jiraField) => { - this.fieldIds[jiraField.name] = jiraField.id; - }); - } - const fieldId = this.fieldIds[fieldName]; - if (fieldId !== undefined) { - const issues: IssueServer[] | IssueCloud[] = await this.jiraClient.search({ - jql: `project = ${this.options.jira.projectKey} AND issue in (${issueKeys.join( - "," - )})`, - fields: [fieldId], - }); - const issuesWithUnparseableField: string[] = []; - issues.forEach((issue: IssueServer | IssueCloud) => { - const value = extractor(issue.fields[fieldId]); - if (value !== undefined) { - results[issue.key] = value; - } else { - issuesWithUnparseableField.push(issue.key); - } - }); - if (issuesWithUnparseableField.length > 0) { - logWarning( - `Failed to parse the following Jira field of the following issues: ${fieldName}\n${issuesWithUnparseableField.join( - "\n" - )}` + const issuesWithUnparseableField: string[] = []; + issues.forEach((issue: IssueServer | IssueCloud) => { + const value = extractor.extractorFunction(issue.fields[fieldId]); + if (value !== undefined) { + results[issue.key] = value; + } else { + issuesWithUnparseableField.push( + `${issue.key}: ${JSON.stringify(issue.fields[fieldId])}` ); } - } else { - logWarning(`Failed to fetch Jira field ID for field: ${fieldName}`); - } - return results; - } - - protected stringExtractor(value: unknown): string | undefined { - if (typeof value === "string") { - return value; - } - } + }); + if (issuesWithUnparseableField.length > 0) { + throw new Error( + dedent(` + Failed to parse the following Jira field of some issues: ${fieldName} + Expected the field to be: ${extractor.expectedType} + Make sure the correct field is present on the following issues: - protected valueExtractor(data: unknown): string | undefined { - if (typeof data === "object" && data !== null) { - return data["value"]; + ${issuesWithUnparseableField.join("\n")} + `) + ); } + return results; } private async fetchFields( existingFields: StringMap, fetcher: (...issueKeys: string[]) => Promise>, ...issueKeys: string[] - ): Promise { + ): Promise> { const issuesWithMissingField: string[] = issueKeys.filter( (key: string) => !(key in existingFields) ); if (issuesWithMissingField.length > 0) { const fetchedFields = await fetcher(...issuesWithMissingField); - for (let i = issuesWithMissingField.length - 1; i >= 0; i--) { - const key = issuesWithMissingField[i]; + issueKeys.forEach((key: string) => { if (key in fetchedFields) { existingFields[key] = fetchedFields[key]; - issuesWithMissingField.splice(i, 1); } - } + }); } - return issuesWithMissingField; + const result: StringMap = {}; + issueKeys.forEach((key: string) => { + if (key in existingFields) { + result[key] = existingFields[key]; + } + }); + return result; } } diff --git a/src/repository/jira/jiraRepositoryCloud.spec.ts b/src/repository/jira/jiraRepositoryCloud.spec.ts new file mode 100644 index 00000000..cf4f545b --- /dev/null +++ b/src/repository/jira/jiraRepositoryCloud.spec.ts @@ -0,0 +1,651 @@ +import { expect } from "chai"; +import dedent from "dedent"; +import { stub } from "sinon"; +import { RESOLVED_JWT_CREDENTIALS, stubLogging } from "../../../test/util"; +import { BasicAuthCredentials } from "../../authentication/credentials"; +import { JiraClientCloud } from "../../client/jira/jiraClientCloud"; +import { XrayClientCloud } from "../../client/xray/xrayClientCloud"; +import { initOptions } from "../../context"; +import { InternalOptions } from "../../types/plugin"; +import { JiraRepositoryCloud } from "./jiraRepositoryCloud"; + +describe.only("the cloud issue repository", () => { + let options: InternalOptions; + let xrayClient: XrayClientCloud; + let jiraClient: JiraClientCloud; + let repository: JiraRepositoryCloud; + + beforeEach(() => { + options = initOptions( + {}, + { + jira: { + projectKey: "CYP", + url: "https://example.org", + }, + } + ); + jiraClient = new JiraClientCloud( + "https://example.org", + new BasicAuthCredentials("user", "xyz") + ); + xrayClient = new XrayClientCloud(RESOLVED_JWT_CREDENTIALS); + repository = new JiraRepositoryCloud(jiraClient, xrayClient, options); + }); + + describe("getSummaries", () => { + it("fetches summaries", async () => { + stub(jiraClient, "getFields").resolves([ + { + id: "summary", + name: "summary", + custom: false, + orderable: true, + navigable: true, + searchable: true, + clauseNames: ["summary"], + schema: { + type: "string", + system: "summary", + }, + }, + ]); + const searchStub = stub(jiraClient, "search").resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1000", + self: "https://example.org/rest/api/2/issue/1000", + key: "CYP-123", + fields: { + summary: "Hello", + }, + }, + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1001", + self: "https://example.org/rest/api/2/issue/1001", + key: "CYP-456", + fields: { + summary: "Good Morning", + }, + }, + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1002", + self: "https://example.org/rest/api/2/issue/1002", + key: "CYP-789", + fields: { + summary: "Goodbye", + }, + }, + ]); + const summaries = await repository.getSummaries("CYP-123", "CYP-456", "CYP-789"); + expect(summaries).to.deep.eq({ + "CYP-123": "Hello", + "CYP-456": "Good Morning", + "CYP-789": "Goodbye", + }); + expect(searchStub).to.have.been.calledOnceWithExactly({ + jql: "project = CYP AND issue in (CYP-123,CYP-456,CYP-789)", + fields: ["summary"], + }); + }); + + it("fetches summaries only for unknown issues", async () => { + const stubbedGetFields = stub(jiraClient, "getFields").resolves([ + { + id: "summary", + name: "summary", + custom: false, + orderable: true, + navigable: true, + searchable: true, + clauseNames: ["summary"], + schema: { + type: "string", + system: "summary", + }, + }, + ]); + const stubbedSearch = stub(jiraClient, "search"); + stubbedSearch.onFirstCall().resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1000", + self: "https://example.org/rest/api/2/issue/1000", + key: "CYP-123", + fields: { + summary: "Hello", + }, + }, + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1002", + self: "https://example.org/rest/api/2/issue/1002", + key: "CYP-789", + fields: { + summary: "Goodbye", + }, + }, + ]); + stubbedSearch.onSecondCall().resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1001", + self: "https://example.org/rest/api/2/issue/1001", + key: "CYP-456", + fields: { + summary: "Good Morning", + }, + }, + ]); + await repository.getSummaries("CYP-123", "CYP-789"); + const summaries = await repository.getSummaries("CYP-123", "CYP-456", "CYP-789"); + expect(summaries).to.deep.eq({ + "CYP-123": "Hello", + "CYP-456": "Good Morning", + "CYP-789": "Goodbye", + }); + // Everything's fetched already, should not fetch anything again. + await repository.getSummaries("CYP-123", "CYP-456", "CYP-789"); + expect(stubbedGetFields).to.have.been.calledOnce; + expect(stubbedSearch).to.have.been.calledTwice; + expect(stubbedSearch.secondCall).to.have.been.calledWithExactly({ + jql: "project = CYP AND issue in (CYP-456)", + fields: ["summary"], + }); + }); + + it("displays an error for issues which do not exist", async () => { + stub(jiraClient, "getFields").resolves([ + { + id: "summary", + name: "summary", + custom: false, + orderable: true, + navigable: true, + searchable: true, + clauseNames: ["summary"], + schema: { + type: "string", + system: "summary", + }, + }, + ]); + const stubbedSearch = stub(jiraClient, "search"); + stubbedSearch.resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1000", + self: "https://example.org/rest/api/2/issue/1000", + key: "CYP-123", + fields: { + summary: "Hello", + }, + }, + ]); + const { stubbedError } = stubLogging(); + const summaries = await repository.getSummaries("CYP-123", "CYP-456", "CYP-789"); + expect(stubbedError).to.have.been.calledOnceWithExactly( + dedent(` + Failed to fetch issue summaries + Make sure these issues exist: + + CYP-456 + CYP-789 + `) + ); + expect(summaries).to.deep.eq({ + "CYP-123": "Hello", + }); + }); + + it("displays an error when the summary field does not exist", async () => { + stub(jiraClient, "getFields").resolves([]); + const stubbedSearch = stub(jiraClient, "search"); + const { stubbedError } = stubLogging(); + const summaries = await repository.getSummaries("CYP-123"); + expect(stubbedSearch).to.not.have.been.called; + expect(stubbedError).to.have.been.calledOnceWithExactly( + dedent(` + Failed to fetch issue summaries + Failed to fetch Jira field ID for field: summary + Make sure the field actually exists + `) + ); + expect(summaries).to.deep.eq({}); + }); + + it("handles get field failures gracefully", async () => { + stub(jiraClient, "getFields").resolves(undefined); + const stubbedSearch = stub(jiraClient, "search"); + const { stubbedError } = stubLogging(); + const summaries = await repository.getSummaries("CYP-123"); + expect(stubbedSearch).to.not.have.been.called; + expect(stubbedError).to.have.been.calledOnceWithExactly( + dedent(` + Failed to fetch issue summaries + Failed to fetch Jira field ID for field: summary + Make sure the field actually exists + `) + ); + expect(summaries).to.deep.eq({}); + }); + + it("handles unparseable field failures gracefully", async () => { + stub(jiraClient, "getFields").resolves([ + { + id: "summary", + name: "summary", + custom: false, + orderable: true, + navigable: true, + searchable: true, + clauseNames: ["summary"], + schema: { + type: "string", + system: "summary", + }, + }, + ]); + const stubbedSearch = stub(jiraClient, "search"); + stubbedSearch.resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1000", + self: "https://example.org/rest/api/2/issue/1000", + key: "CYP-123", + fields: { + summary: ["Good Morning", "Summary 2"], + }, + }, + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1001", + self: "https://example.org/rest/api/2/issue/1001", + key: "CYP-456", + fields: { + summary: { + Something: 5, + }, + }, + }, + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1002", + self: "https://example.org/rest/api/2/issue/1002", + key: "CYP-789", + fields: { + summary: "Bonjour", + }, + }, + ]); + const { stubbedError } = stubLogging(); + const summaries = await repository.getSummaries("CYP-123", "CYP-456", "CYP-789"); + expect(stubbedError).to.have.been.calledOnceWithExactly( + dedent(` + Failed to fetch issue summaries + Failed to parse the following Jira field of some issues: summary + Expected the field to be: a string + Make sure the correct field is present on the following issues: + + CYP-123: ["Good Morning","Summary 2"] + CYP-456: {"Something":5} + `) + ); + expect(summaries).to.deep.eq({}); + }); + }); + + describe("getDescriptions", () => { + it("fetches descriptions", async () => { + stub(jiraClient, "getFields").resolves([ + { + id: "description", + name: "description", + custom: false, + orderable: true, + navigable: true, + searchable: true, + clauseNames: ["description"], + schema: { + type: "string", + system: "description", + }, + }, + ]); + const searchStub = stub(jiraClient, "search").resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1000", + self: "https://example.org/rest/api/2/issue/1000", + key: "CYP-123", + fields: { + description: "Very informative", + }, + }, + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1001", + self: "https://example.org/rest/api/2/issue/1001", + key: "CYP-456", + fields: { + description: "Even more informative", + }, + }, + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1002", + self: "https://example.org/rest/api/2/issue/1002", + key: "CYP-789", + fields: { + description: "Not that informative", + }, + }, + ]); + const descriptions = await repository.getDescriptions("CYP-123", "CYP-456", "CYP-789"); + expect(descriptions).to.deep.eq({ + "CYP-123": "Very informative", + "CYP-456": "Even more informative", + "CYP-789": "Not that informative", + }); + expect(searchStub).to.have.been.calledOnceWithExactly({ + jql: "project = CYP AND issue in (CYP-123,CYP-456,CYP-789)", + fields: ["description"], + }); + }); + + it("fetches descriptions only for unknown issues", async () => { + const stubbedGetFields = stub(jiraClient, "getFields").resolves([ + { + id: "description", + name: "description", + custom: false, + orderable: true, + navigable: true, + searchable: true, + clauseNames: ["description"], + schema: { + type: "string", + system: "description", + }, + }, + ]); + const stubbedSearch = stub(jiraClient, "search"); + stubbedSearch.onFirstCall().resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1000", + self: "https://example.org/rest/api/2/issue/1000", + key: "CYP-123", + fields: { + description: "Very informative", + }, + }, + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1002", + self: "https://example.org/rest/api/2/issue/1002", + key: "CYP-789", + fields: { + description: "Not that informative", + }, + }, + ]); + stubbedSearch.onSecondCall().resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1001", + self: "https://example.org/rest/api/2/issue/1001", + key: "CYP-456", + fields: { + description: "Even more informative", + }, + }, + ]); + await repository.getDescriptions("CYP-123", "CYP-789"); + const descriptions = await repository.getDescriptions("CYP-123", "CYP-456", "CYP-789"); + expect(descriptions).to.deep.eq({ + "CYP-123": "Very informative", + "CYP-456": "Even more informative", + "CYP-789": "Not that informative", + }); + // Everything's fetched already, should not fetch anything again. + await repository.getDescriptions("CYP-123", "CYP-456", "CYP-789"); + expect(stubbedGetFields).to.have.been.calledOnce; + expect(stubbedSearch).to.have.been.calledTwice; + expect(stubbedSearch.secondCall).to.have.been.calledWithExactly({ + jql: "project = CYP AND issue in (CYP-456)", + fields: ["description"], + }); + }); + + it("displays an error for issues which do not exist", async () => { + stub(jiraClient, "getFields").resolves([ + { + id: "description", + name: "description", + custom: false, + orderable: true, + navigable: true, + searchable: true, + clauseNames: ["description"], + schema: { + type: "string", + system: "description", + }, + }, + ]); + const stubbedSearch = stub(jiraClient, "search"); + stubbedSearch.resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1000", + self: "https://example.org/rest/api/2/issue/1000", + key: "CYP-123", + fields: { + description: "I am a description", + }, + }, + ]); + const { stubbedError } = stubLogging(); + const descriptions = await repository.getDescriptions("CYP-123", "CYP-456", "CYP-789"); + expect(stubbedError).to.have.been.calledOnceWithExactly( + dedent(` + Failed to fetch issue descriptions + Make sure these issues exist: + + CYP-456 + CYP-789 + `) + ); + expect(descriptions).to.deep.eq({ + "CYP-123": "I am a description", + }); + }); + + it("displays an error when the description field does not exist", async () => { + stub(jiraClient, "getFields").resolves([]); + const stubbedSearch = stub(jiraClient, "search"); + const { stubbedError } = stubLogging(); + const descriptions = await repository.getDescriptions("CYP-123"); + expect(stubbedSearch).to.not.have.been.called; + expect(stubbedError).to.have.been.calledOnceWithExactly( + dedent(` + Failed to fetch issue descriptions + Failed to fetch Jira field ID for field: description + Make sure the field actually exists + `) + ); + expect(descriptions).to.deep.eq({}); + }); + + it("handles get field failures gracefully", async () => { + stub(jiraClient, "getFields").resolves(undefined); + const stubbedSearch = stub(jiraClient, "search"); + stubbedSearch.resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1000", + self: "https://example.org/rest/api/2/issue/1000", + key: "CYP-123", + }, + ]); + const { stubbedError } = stubLogging(); + const descriptions = await repository.getDescriptions("CYP-123"); + expect(stubbedSearch).to.not.have.been.called; + expect(stubbedError).to.have.been.calledOnceWithExactly( + dedent(` + Failed to fetch issue descriptions + Failed to fetch Jira field ID for field: description + Make sure the field actually exists + `) + ); + expect(descriptions).to.deep.eq({}); + }); + + it("handles unparseable field failures gracefully", async () => { + stub(jiraClient, "getFields").resolves([ + { + id: "description", + name: "description", + custom: false, + orderable: true, + navigable: true, + searchable: true, + clauseNames: ["description"], + schema: { + type: "string", + system: "description", + }, + }, + ]); + const stubbedSearch = stub(jiraClient, "search"); + stubbedSearch.resolves([ + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1000", + self: "https://example.org/rest/api/2/issue/1000", + key: "CYP-123", + fields: { + description: ["This is a somewhat unexpected", "description"], + }, + }, + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1001", + self: "https://example.org/rest/api/2/issue/1001", + key: "CYP-456", + fields: { + description: { + Something: 5, + }, + }, + }, + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1002", + self: "https://example.org/rest/api/2/issue/1002", + key: "CYP-789", + fields: { + description: "Bonjour (encore)", + }, + }, + ]); + const { stubbedError } = stubLogging(); + const summaries = await repository.getDescriptions("CYP-123", "CYP-456", "CYP-789"); + expect(stubbedError).to.have.been.calledOnceWithExactly( + dedent(` + Failed to fetch issue descriptions + Failed to parse the following Jira field of some issues: description + Expected the field to be: a string + Make sure the correct field is present on the following issues: + + CYP-123: ["This is a somewhat unexpected","description"] + CYP-456: {"Something":5} + `) + ); + expect(summaries).to.deep.eq({}); + }); + }); + + describe("getTestTypes", () => { + it("fetches test types", async () => { + const getTestTypesStub = stub(xrayClient, "getTestTypes").resolves({ + "CYP-123": "Cucumber", + "CYP-456": "Generic", + "CYP-789": "Manual", + }); + const testTypes = await repository.getTestTypes("CYP-123", "CYP-456", "CYP-789"); + expect(testTypes).to.deep.eq({ + "CYP-123": "Cucumber", + "CYP-456": "Generic", + "CYP-789": "Manual", + }); + expect(getTestTypesStub).to.have.been.calledOnceWithExactly( + "CYP", + "CYP-123", + "CYP-456", + "CYP-789" + ); + }); + + it("fetches test types only for unknown issues", async () => { + const getTestTypesStub = stub(xrayClient, "getTestTypes"); + getTestTypesStub.onFirstCall().resolves({ + "CYP-123": "Cucumber", + "CYP-456": "Generic", + "CYP-789": "Manual", + }); + getTestTypesStub.onSecondCall().resolves({ + "CYP-456": "Generic", + }); + await repository.getTestTypes("CYP-123", "CYP-789"); + const testTypes = await repository.getTestTypes("CYP-123", "CYP-456", "CYP-789"); + expect(testTypes).to.deep.eq({ + "CYP-123": "Cucumber", + "CYP-456": "Generic", + "CYP-789": "Manual", + }); + // Everything's fetched already, should not fetch anything again. + await repository.getTestTypes("CYP-123", "CYP-456", "CYP-789"); + expect(getTestTypesStub).to.have.been.calledTwice; + expect(getTestTypesStub.secondCall).to.have.been.calledWithExactly("CYP", "CYP-456"); + }); + + it("displays an error for issues which do not exist", async () => { + stub(xrayClient, "getTestTypes").resolves({ + "CYP-123": "Cucumber", + }); + const { stubbedError } = stubLogging(); + const testTypes = await repository.getTestTypes("CYP-123", "CYP-456", "CYP-789"); + expect(stubbedError).to.have.been.calledOnceWithExactly( + dedent(` + Failed to fetch issue test types + Make sure these issues exist and are test issues: + + CYP-456 + CYP-789 + `) + ); + expect(testTypes).to.deep.eq({ "CYP-123": "Cucumber" }); + }); + + it("handles failed test type requests gracefully", async () => { + stub(xrayClient, "getTestTypes").resolves(undefined); + const { stubbedError } = stubLogging(); + const testTypes = await repository.getTestTypes("CYP-123", "CYP-456", "CYP-789"); + expect(stubbedError).to.have.been.calledOnceWithExactly( + dedent(` + Failed to fetch issue test types + Make sure these issues exist and are test issues: + + CYP-123 + CYP-456 + CYP-789 + `) + ); + expect(testTypes).to.deep.eq({}); + }); + }); +}); diff --git a/src/repository/jira/jiraRepositoryCloud.ts b/src/repository/jira/jiraRepositoryCloud.ts index e9c63a6e..ab28bfe6 100644 --- a/src/repository/jira/jiraRepositoryCloud.ts +++ b/src/repository/jira/jiraRepositoryCloud.ts @@ -7,13 +7,17 @@ export class JiraRepositoryCloud extends JiraRepository> { // Field property example: // summary: "Bug 12345" - return await this.getJiraField("summary", this.stringExtractor, ...issueKeys); + return await this.getJiraField("summary", JiraRepository.STRING_EXTRACTOR, ...issueKeys); } protected async fetchDescriptions(...issueKeys: string[]): Promise> { // Field property example: // description: "This is a description" - return await this.getJiraField("description", this.stringExtractor, ...issueKeys); + return await this.getJiraField( + "description", + JiraRepository.STRING_EXTRACTOR, + ...issueKeys + ); } protected async fetchTestTypes(...issueKeys: string[]): Promise> { diff --git a/src/repository/jira/jiraRepositoryServer.spec.ts b/src/repository/jira/jiraRepositoryServer.spec.ts index 621394e9..06219a19 100644 --- a/src/repository/jira/jiraRepositoryServer.spec.ts +++ b/src/repository/jira/jiraRepositoryServer.spec.ts @@ -199,17 +199,9 @@ describe("the server issue repository", () => { }); }); - it("displays a warning when the summary field does not exist", async () => { + it("displays an error when the summary field does not exist", async () => { stub(jiraClient, "getFields").resolves([]); const stubbedSearch = stub(jiraClient, "search"); - stubbedSearch.resolves([ - { - expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", - id: "1000", - self: "https://example.org/rest/api/2/issue/1000", - key: "CYP-123", - }, - ]); const { stubbedError, stubbedWarning } = stubLogging(); const summaries = await repository.getSummaries("CYP-123"); expect(stubbedSearch).to.not.have.been.called; @@ -481,17 +473,9 @@ describe("the server issue repository", () => { }); }); - it("displays a warning when the description field does not exist", async () => { + it("displays an error when the description field does not exist", async () => { stub(jiraClient, "getFields").resolves([]); const stubbedSearch = stub(jiraClient, "search"); - stubbedSearch.resolves([ - { - expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", - id: "1000", - self: "https://example.org/rest/api/2/issue/1000", - key: "CYP-123", - }, - ]); const { stubbedError, stubbedWarning } = stubLogging(); const descriptions = await repository.getDescriptions("CYP-123"); expect(stubbedSearch).to.not.have.been.called; @@ -801,17 +785,9 @@ describe("the server issue repository", () => { }); }); - it("displays a warning when the description field does not exist", async () => { + it("displays an error when the description field does not exist", async () => { stub(jiraClient, "getFields").resolves([]); const stubbedSearch = stub(jiraClient, "search"); - stubbedSearch.resolves([ - { - expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", - id: "1000", - self: "https://example.org/rest/api/2/issue/1000", - key: "CYP-123", - }, - ]); const { stubbedError, stubbedWarning } = stubLogging(); const testTypes = await repository.getTestTypes("CYP-123"); expect(stubbedSearch).to.not.have.been.called; diff --git a/src/repository/jira/jiraRepositoryServer.ts b/src/repository/jira/jiraRepositoryServer.ts index e046739c..d64f9f68 100644 --- a/src/repository/jira/jiraRepositoryServer.ts +++ b/src/repository/jira/jiraRepositoryServer.ts @@ -7,13 +7,17 @@ export class JiraRepositoryServer extends JiraRepository> { // Field property example: // summary: "Bug 12345" - return await this.getJiraField("Summary", this.stringExtractor, ...issueKeys); + return await this.getJiraField("Summary", JiraRepository.STRING_EXTRACTOR, ...issueKeys); } protected async fetchDescriptions(...issueKeys: string[]): Promise> { // Field property example: // description: "This is a description" - return await this.getJiraField("Description", this.stringExtractor, ...issueKeys); + return await this.getJiraField( + "Description", + JiraRepository.STRING_EXTRACTOR, + ...issueKeys + ); } protected async fetchTestTypes(...issueKeys: string[]): Promise> { @@ -23,6 +27,10 @@ export class JiraRepositoryServer extends JiraRepository = T extends T ? keyof T : never; type Expand = T extends T ? { [K in keyof T]: T[K] } : never; @@ -33,3 +38,18 @@ export function getEnumKeyByEnumValue = { [key: string]: T; }; + +/** + * Type describing the possible client combinations. + */ +export type ClientCombination = + | { + kind: "server"; + jiraClient: JiraClientServer; + xrayClient: XrayClientServer; + } + | { + kind: "cloud"; + jiraClient: JiraClientCloud; + xrayClient: XrayClientCloud; + }; diff --git a/test/util.ts b/test/util.ts index 471afb0b..3e4e0b8f 100644 --- a/test/util.ts +++ b/test/util.ts @@ -64,9 +64,9 @@ after(async () => { } }); -export class DummyXrayClient extends XrayClient { +export class DummyXrayClient extends XrayClient { constructor() { - super("https://example.org", null, null); + super("https://example.org", null); } public getUrlImportExecution(): string { throw new Error("Method not implemented."); From bd7d8034d059400a5fcc9fb236324c87ea166fe6 Mon Sep 17 00:00:00 2001 From: Sebastian Vollbrecht Date: Thu, 27 Jul 2023 23:14:54 +0200 Subject: [PATCH 08/19] Update Jira repository tests --- .../jira/jiraRepositoryCloud.spec.ts | 2 +- .../jira/jiraRepositoryServer.spec.ts | 120 +++++++++--------- 2 files changed, 58 insertions(+), 64 deletions(-) diff --git a/src/repository/jira/jiraRepositoryCloud.spec.ts b/src/repository/jira/jiraRepositoryCloud.spec.ts index cf4f545b..1e290191 100644 --- a/src/repository/jira/jiraRepositoryCloud.spec.ts +++ b/src/repository/jira/jiraRepositoryCloud.spec.ts @@ -9,7 +9,7 @@ import { initOptions } from "../../context"; import { InternalOptions } from "../../types/plugin"; import { JiraRepositoryCloud } from "./jiraRepositoryCloud"; -describe.only("the cloud issue repository", () => { +describe("the cloud issue repository", () => { let options: InternalOptions; let xrayClient: XrayClientCloud; let jiraClient: JiraClientCloud; diff --git a/src/repository/jira/jiraRepositoryServer.spec.ts b/src/repository/jira/jiraRepositoryServer.spec.ts index 06219a19..f3ffc5f7 100644 --- a/src/repository/jira/jiraRepositoryServer.spec.ts +++ b/src/repository/jira/jiraRepositoryServer.spec.ts @@ -189,7 +189,9 @@ describe("the server issue repository", () => { const summaries = await repository.getSummaries("CYP-123", "CYP-456", "CYP-789"); expect(stubbedError).to.have.been.calledOnceWithExactly( dedent(` - Failed to fetch summaries of issues: + Failed to fetch issue summaries + Make sure these issues exist: + CYP-456 CYP-789 `) @@ -202,16 +204,14 @@ describe("the server issue repository", () => { it("displays an error when the summary field does not exist", async () => { stub(jiraClient, "getFields").resolves([]); const stubbedSearch = stub(jiraClient, "search"); - const { stubbedError, stubbedWarning } = stubLogging(); + const { stubbedError } = stubLogging(); const summaries = await repository.getSummaries("CYP-123"); expect(stubbedSearch).to.not.have.been.called; - expect(stubbedWarning).to.have.been.calledOnceWithExactly( - "Failed to fetch Jira field ID for field: Summary" - ); expect(stubbedError).to.have.been.calledOnceWithExactly( dedent(` - Failed to fetch summaries of issues: - CYP-123 + Failed to fetch issue summaries + Failed to fetch Jira field ID for field: Summary + Make sure the field actually exists `) ); expect(summaries).to.deep.eq({}); @@ -233,8 +233,9 @@ describe("the server issue repository", () => { expect(stubbedSearch).to.not.have.been.called; expect(stubbedError).to.have.been.calledOnceWithExactly( dedent(` - Failed to fetch summaries of issues: - CYP-123 + Failed to fetch issue summaries + Failed to fetch Jira field ID for field: Summary + Make sure the field actually exists `) ); expect(summaries).to.deep.eq({}); @@ -288,23 +289,20 @@ describe("the server issue repository", () => { }, }, ]); - const { stubbedError, stubbedWarning } = stubLogging(); + const { stubbedError } = stubLogging(); const summaries = await repository.getSummaries("CYP-123", "CYP-456", "CYP-789"); - expect(stubbedWarning).to.have.been.calledOnceWithExactly( - dedent(` - Failed to parse the following Jira field of the following issues: Summary - CYP-123 - CYP-456 - `) - ); expect(stubbedError).to.have.been.calledOnceWithExactly( dedent(` - Failed to fetch summaries of issues: - CYP-123 - CYP-456 + Failed to fetch issue summaries + Failed to parse the following Jira field of some issues: Summary + Expected the field to be: a string + Make sure the correct field is present on the following issues: + + CYP-123: ["Good Morning","Summary 2"] + CYP-456: {"Something":5} `) ); - expect(summaries).to.deep.eq({ "CYP-789": "Bonjour" }); + expect(summaries).to.deep.eq({}); }); }); @@ -463,7 +461,9 @@ describe("the server issue repository", () => { const descriptions = await repository.getDescriptions("CYP-123", "CYP-456", "CYP-789"); expect(stubbedError).to.have.been.calledOnceWithExactly( dedent(` - Failed to fetch descriptions of issues: + Failed to fetch issue descriptions + Make sure these issues exist: + CYP-456 CYP-789 `) @@ -476,16 +476,14 @@ describe("the server issue repository", () => { it("displays an error when the description field does not exist", async () => { stub(jiraClient, "getFields").resolves([]); const stubbedSearch = stub(jiraClient, "search"); - const { stubbedError, stubbedWarning } = stubLogging(); + const { stubbedError } = stubLogging(); const descriptions = await repository.getDescriptions("CYP-123"); expect(stubbedSearch).to.not.have.been.called; - expect(stubbedWarning).to.have.been.calledOnceWithExactly( - "Failed to fetch Jira field ID for field: Description" - ); expect(stubbedError).to.have.been.calledOnceWithExactly( dedent(` - Failed to fetch descriptions of issues: - CYP-123 + Failed to fetch issue descriptions + Failed to fetch Jira field ID for field: Description + Make sure the field actually exists `) ); expect(descriptions).to.deep.eq({}); @@ -507,8 +505,9 @@ describe("the server issue repository", () => { expect(stubbedSearch).to.not.have.been.called; expect(stubbedError).to.have.been.calledOnceWithExactly( dedent(` - Failed to fetch descriptions of issues: - CYP-123 + Failed to fetch issue descriptions + Failed to fetch Jira field ID for field: Description + Make sure the field actually exists `) ); expect(descriptions).to.deep.eq({}); @@ -562,23 +561,20 @@ describe("the server issue repository", () => { }, }, ]); - const { stubbedError, stubbedWarning } = stubLogging(); + const { stubbedError } = stubLogging(); const summaries = await repository.getDescriptions("CYP-123", "CYP-456", "CYP-789"); - expect(stubbedWarning).to.have.been.calledOnceWithExactly( - dedent(` - Failed to parse the following Jira field of the following issues: Description - CYP-123 - CYP-456 - `) - ); expect(stubbedError).to.have.been.calledOnceWithExactly( dedent(` - Failed to fetch descriptions of issues: - CYP-123 - CYP-456 + Failed to fetch issue descriptions + Failed to parse the following Jira field of some issues: Description + Expected the field to be: a string + Make sure the correct field is present on the following issues: + + CYP-123: ["This is a somewhat unexpected","description"] + CYP-456: {"Something":5} `) ); - expect(summaries).to.deep.eq({ "CYP-789": "Bonjour (encore)" }); + expect(summaries).to.deep.eq({}); }); }); @@ -775,7 +771,9 @@ describe("the server issue repository", () => { const testTypes = await repository.getTestTypes("CYP-123", "CYP-456", "CYP-789"); expect(stubbedError).to.have.been.calledOnceWithExactly( dedent(` - Failed to fetch test types of issues: + Failed to fetch issue test types + Make sure these issues exist and are test issues: + CYP-456 CYP-789 `) @@ -788,16 +786,14 @@ describe("the server issue repository", () => { it("displays an error when the description field does not exist", async () => { stub(jiraClient, "getFields").resolves([]); const stubbedSearch = stub(jiraClient, "search"); - const { stubbedError, stubbedWarning } = stubLogging(); + const { stubbedError } = stubLogging(); const testTypes = await repository.getTestTypes("CYP-123"); expect(stubbedSearch).to.not.have.been.called; - expect(stubbedWarning).to.have.been.calledOnceWithExactly( - "Failed to fetch Jira field ID for field: Test Type" - ); expect(stubbedError).to.have.been.calledOnceWithExactly( dedent(` - Failed to fetch test types of issues: - CYP-123 + Failed to fetch issue test types + Failed to fetch Jira field ID for field: Test Type + Make sure the field actually exists `) ); expect(testTypes).to.deep.eq({}); @@ -819,8 +815,9 @@ describe("the server issue repository", () => { expect(stubbedSearch).to.not.have.been.called; expect(stubbedError).to.have.been.calledOnceWithExactly( dedent(` - Failed to fetch test types of issues: - CYP-123 + Failed to fetch issue test types + Failed to fetch Jira field ID for field: Test Type + Make sure the field actually exists `) ); expect(testTypes).to.deep.eq({}); @@ -880,23 +877,20 @@ describe("the server issue repository", () => { }, }, ]); - const { stubbedError, stubbedWarning } = stubLogging(); + const { stubbedError } = stubLogging(); const testTypes = await repository.getTestTypes("CYP-123", "CYP-456", "CYP-789"); - expect(stubbedWarning).to.have.been.calledOnceWithExactly( - dedent(` - Failed to parse the following Jira field of the following issues: Test Type - CYP-123 - CYP-456 - `) - ); expect(stubbedError).to.have.been.calledOnceWithExactly( dedent(` - Failed to fetch test types of issues: - CYP-123 - CYP-456 + Failed to fetch issue test types + Failed to parse the following Jira field of some issues: Test Type + Expected the field to be: an object with a value property + Make sure the correct field is present on the following issues: + + CYP-123: ["This is a somewhat unexpected","description"] + CYP-456: {"Something":5} `) ); - expect(testTypes).to.deep.eq({ "CYP-789": "Generic" }); + expect(testTypes).to.deep.eq({}); }); }); }); From 2244b5a1bebc3cdc198c973147c7aa37952bc6d4 Mon Sep 17 00:00:00 2001 From: Sebastian Vollbrecht Date: Thu, 27 Jul 2023 23:23:18 +0200 Subject: [PATCH 09/19] Handle additional field corner case --- src/repository/jira/jiraRepository.ts | 6 ------ .../jira/jiraRepositoryServer.spec.ts | 17 ++++++++++++++++- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/repository/jira/jiraRepository.ts b/src/repository/jira/jiraRepository.ts index 78afc93f..e943ad8a 100644 --- a/src/repository/jira/jiraRepository.ts +++ b/src/repository/jira/jiraRepository.ts @@ -154,12 +154,6 @@ export abstract class JiraRepository< protected abstract fetchTestTypes(...issueKeys: string[]): Promise>; - protected valueExtractor(data: unknown): string | undefined { - if (typeof data === "object" && data !== null) { - return data["value"]; - } - } - protected async getJiraField( fieldName: string, extractor: FieldExtractor, diff --git a/src/repository/jira/jiraRepositoryServer.spec.ts b/src/repository/jira/jiraRepositoryServer.spec.ts index f3ffc5f7..b0f2c3e3 100644 --- a/src/repository/jira/jiraRepositoryServer.spec.ts +++ b/src/repository/jira/jiraRepositoryServer.spec.ts @@ -876,9 +876,23 @@ describe("the server issue repository", () => { }, }, }, + { + expand: "operations,versionedRepresentations,editmeta,changelog,renderedFields", + id: "1003", + self: "https://example.org/rest/api/2/issue/1003", + key: "CYP-420", + fields: { + customfield_12100: null, + }, + }, ]); const { stubbedError } = stubLogging(); - const testTypes = await repository.getTestTypes("CYP-123", "CYP-456", "CYP-789"); + const testTypes = await repository.getTestTypes( + "CYP-123", + "CYP-456", + "CYP-789", + "CYP-420" + ); expect(stubbedError).to.have.been.calledOnceWithExactly( dedent(` Failed to fetch issue test types @@ -888,6 +902,7 @@ describe("the server issue repository", () => { CYP-123: ["This is a somewhat unexpected","description"] CYP-456: {"Something":5} + CYP-420: null `) ); expect(testTypes).to.deep.eq({}); From 6611735a15f1b77ab43d7f14b2439472440ab7d6 Mon Sep 17 00:00:00 2001 From: Sebastian Vollbrecht Date: Fri, 28 Jul 2023 00:43:00 +0200 Subject: [PATCH 10/19] Remove interal `testTypes` bookkeeping --- src/context.ts | 1 - .../importExecutionConverter.spec.ts | 380 +++++++++++++++--- .../importExecutionConverter.ts | 36 +- .../importExecutionConverterCloud.spec.ts | 59 ++- .../importExecutionConverterCloud.ts | 10 +- .../importExecutionConverterServer.spec.ts | 60 ++- .../importExecutionConverterServer.ts | 10 +- src/hooks.ts | 14 +- src/types/plugin.ts | 10 +- 9 files changed, 468 insertions(+), 112 deletions(-) diff --git a/src/context.ts b/src/context.ts index 54bdc28b..546c8f90 100644 --- a/src/context.ts +++ b/src/context.ts @@ -102,7 +102,6 @@ export function initOptions(env: Cypress.ObjectLike, options: Options): Internal options.xray?.steps?.update ?? true, }, - testTypes: {}, uploadResults: parse(env, ENV_XRAY_UPLOAD_RESULTS, asBoolean) ?? options.xray?.uploadResults ?? diff --git a/src/conversion/importExecution/importExecutionConverter.spec.ts b/src/conversion/importExecution/importExecutionConverter.spec.ts index 7d795c55..24b94f0c 100644 --- a/src/conversion/importExecution/importExecutionConverter.spec.ts +++ b/src/conversion/importExecution/importExecutionConverter.spec.ts @@ -4,6 +4,7 @@ import { readFileSync } from "fs"; import { stubLogging } from "../../../test/util"; import { initOptions } from "../../context"; import { InternalOptions } from "../../types/plugin"; +import { TestIssueData } from "./importExecutionConverter"; import { ImportExecutionConverterCloud } from "./importExecutionConverterCloud"; import { ImportExecutionConverterServer } from "./importExecutionConverterServer"; @@ -12,6 +13,7 @@ describe("the import execution converters", () => { describe(converterType, () => { let options: InternalOptions; let converter: ImportExecutionConverterServer | ImportExecutionConverterCloud; + let testIssueData: TestIssueData; beforeEach(() => { options = initOptions( {}, @@ -32,22 +34,33 @@ describe("the import execution converters", () => { converterType === "server" ? new ImportExecutionConverterServer(options) : new ImportExecutionConverterCloud(options); + testIssueData = { summaries: {}, testTypes: {} }; }); - it("should convert test results into xray json", () => { + it("converts test results into xray json", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); - const json = converter.convert(result); + testIssueData.summaries = { + "CYP-40": "This is", + "CYP-41": "a distributed", + "CYP-49": "summary", + }; + testIssueData.testTypes = { + "CYP-40": "Generic", + "CYP-41": "Manual", + "CYP-49": "Cucumber", + }; + const json = converter.convert(result, testIssueData); expect(json.tests).to.have.length(3); }); - it("should skip tests when encountering unknown statuses", () => { + it("skips tests when encountering unknown statuses", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultUnknownStatus.json", "utf-8") ); const { stubbedWarning } = stubLogging(); - const json = converter.convert(result); + const json = converter.convert(result, testIssueData); expect(stubbedWarning.firstCall).to.have.been.calledWith( dedent(` Skipping result upload for test: TodoMVC hides footer initially @@ -65,96 +78,178 @@ describe("the import execution converters", () => { expect(json.tests).to.be.undefined; }); - it("should erase milliseconds from timestamps", () => { + it("erases milliseconds from timestamps", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); - const json = converter.convert(result); + testIssueData.summaries = { + "CYP-40": "This is", + "CYP-41": "a distributed", + "CYP-49": "summary", + }; + testIssueData.testTypes = { + "CYP-40": "Generic", + "CYP-41": "Manual", + "CYP-49": "Cucumber", + }; + const json = converter.convert(result, testIssueData); expect(json.info?.startDate).to.eq("2022-11-28T17:41:12Z"); expect(json.info?.finishDate).to.eq("2022-11-28T17:41:19Z"); }); - it("should upload screenshots by default", () => { + it("uploads screenshots by default", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); - const json = converter.convert(result); + testIssueData.summaries = { + "CYP-40": "This is", + "CYP-41": "a distributed", + "CYP-49": "summary", + }; + testIssueData.testTypes = { + "CYP-40": "Generic", + "CYP-41": "Manual", + "CYP-49": "Cucumber", + }; + const json = converter.convert(result, testIssueData); expect(json.tests[0].evidence).to.be.undefined; expect(json.tests[1].evidence).to.be.undefined; expect(json.tests[2].evidence).to.be.an("array").with.length(1); expect(json.tests[2].evidence[0].filename).to.eq("turtle.png"); }); - it("should skip screenshot upload if disabled", () => { + it("skips screenshot upload if disabled", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); + testIssueData.summaries = { + "CYP-40": "This is", + "CYP-41": "a distributed", + "CYP-49": "summary", + }; + testIssueData.testTypes = { + "CYP-40": "Generic", + "CYP-41": "Manual", + "CYP-49": "Cucumber", + }; options.xray.uploadScreenshots = false; - const json = converter.convert(result); + const json = converter.convert(result, testIssueData); expect(json.tests).to.have.length(3); expect(json.tests[0].evidence).to.be.undefined; expect(json.tests[1].evidence).to.be.undefined; expect(json.tests[2].evidence).to.be.undefined; }); - it("should normalize screenshot filenames if enabled", () => { + it("normalizes screenshot filenames if enabled", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultProblematicScreenshot.json", "utf-8") ); + testIssueData.summaries = { + "CYP-123": "Test issue", + }; + testIssueData.testTypes = { + "CYP-123": "Manual", + }; options.plugin.normalizeScreenshotNames = true; - const json = converter.convert(result); + const json = converter.convert(result, testIssueData); expect(json.tests[0].evidence[0].filename).to.eq( "t_rtle_with_problem_tic_name.png" ); }); - it("should not normalize screenshot filenames by default", () => { + it("does not normalize screenshot filenames by default", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultProblematicScreenshot.json", "utf-8") ); - const json = converter.convert(result); + testIssueData.summaries = { + "CYP-123": "Big test", + }; + testIssueData.testTypes = { + "CYP-123": "Generic", + }; + const json = converter.convert(result, testIssueData); expect(json.tests[0].evidence[0].filename).to.eq( "tûrtle with problemätic name.png" ); }); - it("should use custom passed statuses", () => { + it("uses custom passed statuses", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); + testIssueData.summaries = { + "CYP-40": "This is", + "CYP-41": "a distributed", + "CYP-49": "summary", + }; + testIssueData.testTypes = { + "CYP-40": "Generic", + "CYP-41": "Manual", + "CYP-49": "Cucumber", + }; options.xray.statusPassed = "it worked"; - const json = converter.convert(result); + const json = converter.convert(result, testIssueData); expect(json.tests[0].status).to.eq("it worked"); expect(json.tests[1].status).to.eq("it worked"); }); - it("should use custom failed statuses", () => { + it("uses custom failed statuses", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); + testIssueData.summaries = { + "CYP-40": "This is", + "CYP-41": "a distributed", + "CYP-49": "summary", + }; + testIssueData.testTypes = { + "CYP-40": "Generic", + "CYP-41": "Manual", + "CYP-49": "Cucumber", + }; options.xray.statusFailed = "it did not work"; - const json = converter.convert(result); + const json = converter.convert(result, testIssueData); expect(json.tests[2].status).to.eq("it did not work"); }); - it("should use custom pending statuses", () => { + it("uses custom pending statuses", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultPending.json", "utf-8") ); + testIssueData.summaries = { + "CYP-123": "This is", + "CYP-456": "a distributed", + "CYP-789": "summary", + "CYP-987": "!!!", + }; + testIssueData.testTypes = { + "CYP-123": "Generic", + "CYP-456": "Manual", + "CYP-789": "Cucumber", + "CYP-987": "No idea", + }; options.xray.statusPending = "still pending"; - const json = converter.convert(result); + const json = converter.convert(result, testIssueData); expect(json.tests[0].status).to.eq("still pending"); expect(json.tests[1].status).to.eq("still pending"); expect(json.tests[2].status).to.eq("still pending"); expect(json.tests[3].status).to.eq("still pending"); }); - it("should use custom skipped statuses", () => { + it("uses custom skipped statuses", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultSkipped.json", "utf-8") ); + testIssueData.summaries = { + "CYP-123": "This is", + "CYP-456": "a summary", + }; + testIssueData.testTypes = { + "CYP-123": "Generic", + "CYP-456": "Manual", + }; options.xray.statusSkipped = "omit"; - const json = converter.convert(result); + const json = converter.convert(result, testIssueData); expect(json.tests[1].status).to.eq("omit"); }); @@ -162,12 +257,17 @@ describe("the import execution converters", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); - options.xray.testTypes = { - "CYP-40": "Manual", + testIssueData.summaries = { + "CYP-40": "This is", + "CYP-41": "a distributed", + "CYP-49": "summary", + }; + testIssueData.testTypes = { + "CYP-40": "Generic", "CYP-41": "Manual", - "CYP-49": "Manual", + "CYP-49": "Cucumber", }; - const json = converter.convert(result); + const json = converter.convert(result, testIssueData); expect(json.tests).to.have.length(3); expect(json.tests[0].testInfo.steps).to.have.length(1); expect(json.tests[0].testInfo.steps[0].action).to.be.a("string"); @@ -177,154 +277,308 @@ describe("the import execution converters", () => { expect(json.tests[2].testInfo.steps[0].action).to.be.a("string"); }); - it("should skip step updates if disabled", () => { + it("skips step updates if disabled", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); - options.xray.steps.update = false; - options.xray.testTypes = { - "CYP-40": "Manual", + testIssueData.summaries = { + "CYP-40": "This is", + "CYP-41": "a distributed", + "CYP-49": "summary", + }; + testIssueData.testTypes = { + "CYP-40": "Generic", "CYP-41": "Manual", - "CYP-49": "Manual", + "CYP-49": "Cucumber", }; - const json = converter.convert(result); + options.xray.steps.update = false; + const json = converter.convert(result, testIssueData); expect(json.tests).to.have.length(3); expect(json.tests[0].testInfo.steps).to.be.undefined; expect(json.tests[1].testInfo.steps).to.be.undefined; expect(json.tests[2].testInfo.steps).to.be.undefined; }); - it("should truncate step actions to 8000 characters by default", () => { + it("truncates step actions to 8000 characters by default", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultLongBodies.json", "utf-8") ); - options.xray.testTypes = { + testIssueData.summaries = { + "CYP-123": "Summary 1", + "CYP-456": "Summary 2", + "CYP-789": "Summary 3", + }; + testIssueData.testTypes = { "CYP-123": "Manual", "CYP-456": "Manual", "CYP-789": "Manual", }; - const json = converter.convert(result); + const json = converter.convert(result, testIssueData); expect(json.tests[0].testInfo.steps[0].action).to.eq(`${"x".repeat(7997)}...`); expect(json.tests[1].testInfo.steps[0].action).to.eq(`${"x".repeat(8000)}`); expect(json.tests[2].testInfo.steps[0].action).to.eq(`${"x".repeat(2000)}`); }); - it("should truncate step actions to custom lengths if enabled", () => { + it("truncates step actions to custom lengths if enabled", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultLongBodies.json", "utf-8") ); options.xray.steps.maxLengthAction = 5; - options.xray.testTypes = { + testIssueData.summaries = { + "CYP-123": "1st summary", + "CYP-456": "2nd summary", + "CYP-789": "3rd summary", + }; + testIssueData.testTypes = { "CYP-123": "Manual", "CYP-456": "Manual", "CYP-789": "Manual", }; - const json = converter.convert(result); + const json = converter.convert(result, testIssueData); expect(json.tests[0].testInfo.steps[0].action).to.eq("xx..."); expect(json.tests[1].testInfo.steps[0].action).to.eq("xx..."); expect(json.tests[2].testInfo.steps[0].action).to.eq("xx..."); }); - it("should detect re-use of existing test issues", () => { + it("includes issue summaries", () => { + const result: CypressCommandLine.CypressRunResult = JSON.parse( + readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") + ); + testIssueData.summaries = { + "CYP-40": "This is", + "CYP-41": "a distributed", + "CYP-49": "summary", + }; + testIssueData.testTypes = { + "CYP-40": "Generic", + "CYP-41": "Manual", + "CYP-49": "Cucumber", + }; + const json = converter.convert(result, testIssueData); + expect(json.tests).to.have.length(3); + expect(json.tests[0].testInfo.summary).to.eq("This is"); + expect(json.tests[1].testInfo.summary).to.eq("a distributed"); + expect(json.tests[2].testInfo.summary).to.eq("summary"); + }); + + it("includes test issue keys", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); - const json = converter.convert(result); + testIssueData.summaries = { + "CYP-40": "This is", + "CYP-41": "a distributed", + "CYP-49": "summary", + }; + testIssueData.testTypes = { + "CYP-40": "Generic", + "CYP-41": "Manual", + "CYP-49": "Cucumber", + }; + const json = converter.convert(result, testIssueData); expect(json.tests).to.have.length(3); expect(json.tests[0].testKey).to.eq("CYP-40"); expect(json.tests[1].testKey).to.eq("CYP-41"); expect(json.tests[2].testKey).to.eq("CYP-49"); - expect(json.tests[0].testInfo).to.be.undefined; - expect(json.tests[1].testInfo).to.be.undefined; - expect(json.tests[2].testInfo).to.be.undefined; }); - it("should skip tests with unknown test type", () => { + it("skips tests with missing test type", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); + testIssueData.summaries = { + "CYP-40": "This is", + "CYP-41": "a distributed", + "CYP-49": "summary", + }; + testIssueData.testTypes = { + "CYP-40": "Manual", + "CYP-41": "Manual", + }; options.xray.steps.update = false; - options.xray.testTypes = { + const { stubbedWarning } = stubLogging(); + const json = converter.convert(result, testIssueData); + expect(json.tests).to.be.an("array").with.length(2); + expect(stubbedWarning).to.have.been.calledWith( + dedent(` + Skipping result upload for test: cypress xray plugin CYP-49 failling test case with test issue key + + Test type of corresponding issue is missing: CYP-49 + `) + ); + }); + + it("skips tests with missing summaries", () => { + const result: CypressCommandLine.CypressRunResult = JSON.parse( + readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") + ); + testIssueData.summaries = { + "CYP-40": "This is", + "CYP-41": "a summary", + }; + testIssueData.testTypes = { "CYP-40": "Manual", "CYP-41": "Manual", + "CYP-49": "Cucumber", }; + options.xray.steps.update = false; const { stubbedWarning } = stubLogging(); - const json = converter.convert(result); + const json = converter.convert(result, testIssueData); expect(json.tests).to.be.an("array").with.length(2); expect(stubbedWarning).to.have.been.calledWith( dedent(` Skipping result upload for test: cypress xray plugin CYP-49 failling test case with test issue key - Failed to find test type for issue: CYP-49 + Summary of corresponding issue is missing: CYP-49 `) ); }); - it("should add test execution issue keys", () => { + it("adds test execution issue keys", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); + testIssueData.summaries = { + "CYP-40": "This is", + "CYP-41": "a distributed", + "CYP-49": "summary", + }; + testIssueData.testTypes = { + "CYP-40": "Generic", + "CYP-41": "Manual", + "CYP-49": "Cucumber", + }; options.jira.testExecutionIssueKey = "CYP-123"; - const json = converter.convert(result); + const json = converter.convert(result, testIssueData); expect(json.testExecutionKey).to.eq("CYP-123"); }); - it("should add test plan issue keys", () => { + it("adds test plan issue keys", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); + testIssueData.summaries = { + "CYP-40": "This is", + "CYP-41": "a distributed", + "CYP-49": "summary", + }; + testIssueData.testTypes = { + "CYP-40": "Generic", + "CYP-41": "Manual", + "CYP-49": "Cucumber", + }; options.jira.testPlanIssueKey = "CYP-123"; - const json = converter.convert(result); + const json = converter.convert(result, testIssueData); expect(json.info.testPlanKey).to.eq("CYP-123"); }); - it("should not add test execution issue keys on its own", () => { + it("does not add test execution issue keys on its own", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); - const json = converter.convert(result); + testIssueData.summaries = { + "CYP-40": "This is", + "CYP-41": "a distributed", + "CYP-49": "summary", + }; + testIssueData.testTypes = { + "CYP-40": "Generic", + "CYP-41": "Manual", + "CYP-49": "Cucumber", + }; + const json = converter.convert(result, testIssueData); expect(json.testExecutionKey).to.be.undefined; }); - it("should not add test plan issue keys on its own", () => { + it("does not add test plan issue keys on its own", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); - const json = converter.convert(result); + testIssueData.summaries = { + "CYP-40": "This is", + "CYP-41": "a distributed", + "CYP-49": "summary", + }; + testIssueData.testTypes = { + "CYP-40": "Generic", + "CYP-41": "Manual", + "CYP-49": "Cucumber", + }; + const json = converter.convert(result, testIssueData); expect(json.info.testPlanKey).to.be.undefined; }); - it("should include a custom test execution summary if provided", () => { + it("includes a custom test execution summary if provided", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); + testIssueData.summaries = { + "CYP-40": "This is", + "CYP-41": "a distributed", + "CYP-49": "summary", + }; + testIssueData.testTypes = { + "CYP-40": "Generic", + "CYP-41": "Manual", + "CYP-49": "Cucumber", + }; options.jira.testExecutionIssueSummary = "Jeffrey's Test"; - const json = converter.convert(result); + const json = converter.convert(result, testIssueData); expect(json.info.summary).to.eq("Jeffrey's Test"); }); - it("should use a timestamp as test execution summary by default", () => { + it("uses a timestamp as test execution summary by default", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); - const json = converter.convert(result); + testIssueData.summaries = { + "CYP-40": "This is", + "CYP-41": "a distributed", + "CYP-49": "summary", + }; + testIssueData.testTypes = { + "CYP-40": "Generic", + "CYP-41": "Manual", + "CYP-49": "Cucumber", + }; + const json = converter.convert(result, testIssueData); expect(json.info.summary).to.eq("Execution Results [1669657272234]"); }); - it("should include a custom test execution description if provided", () => { + it("includes a custom test execution description if provided", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); + testIssueData.summaries = { + "CYP-40": "This is", + "CYP-41": "a distributed", + "CYP-49": "summary", + }; + testIssueData.testTypes = { + "CYP-40": "Generic", + "CYP-41": "Manual", + "CYP-49": "Cucumber", + }; options.jira.testExecutionIssueDescription = "Very Useful Text"; - const json = converter.convert(result); + const json = converter.convert(result, testIssueData); expect(json.info.description).to.eq("Very Useful Text"); }); - it("should use versions as test execution description by default", () => { + it("uses versions as test execution description by default", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); - const json = converter.convert(result); + testIssueData.summaries = { + "CYP-40": "This is", + "CYP-41": "a distributed", + "CYP-49": "summary", + }; + testIssueData.testTypes = { + "CYP-40": "Generic", + "CYP-41": "Manual", + "CYP-49": "Cucumber", + }; + const json = converter.convert(result, testIssueData); expect(json.info.description).to.eq( "Cypress version: 11.1.0 Browser: electron (106.0.5249.51)" ); diff --git a/src/conversion/importExecution/importExecutionConverter.ts b/src/conversion/importExecution/importExecutionConverter.ts index c7ed2c57..d285ea4e 100644 --- a/src/conversion/importExecution/importExecutionConverter.ts +++ b/src/conversion/importExecution/importExecutionConverter.ts @@ -1,7 +1,7 @@ import { logWarning } from "../../logging/logging"; import { getNativeTestIssueKey } from "../../preprocessing/preprocessing"; import { Status } from "../../types/testStatus"; -import { DateTimeISO, OneOf, getEnumKeyByEnumValue } from "../../types/util"; +import { DateTimeISO, OneOf, StringMap, getEnumKeyByEnumValue } from "../../types/util"; import { XrayTestCloud, XrayTestExecutionInfo, @@ -13,6 +13,11 @@ import { } from "../../types/xray/importTestExecutionResults"; import { Converter } from "../converter"; +export type TestIssueData = { + summaries: StringMap; + testTypes: StringMap; +}; + /** * @template XrayTestType - the Xray test type * @template XrayTestInfoType - the Xray test information type @@ -24,8 +29,15 @@ export abstract class ImportExecutionConverter< XrayTestExecutionResultsType extends OneOf< [XrayTestExecutionResultsServer, XrayTestExecutionResultsCloud] > -> extends Converter { - public convert(results: CypressCommandLine.CypressRunResult): XrayTestExecutionResultsType { +> extends Converter< + CypressCommandLine.CypressRunResult, + XrayTestExecutionResultsType, + TestIssueData +> { + public convert( + results: CypressCommandLine.CypressRunResult, + issueData: TestIssueData + ): XrayTestExecutionResultsType { const runs: CypressCommandLine.RunResult[] = results.runs.filter( (run: CypressCommandLine.RunResult) => { return !run.spec.absolute.endsWith(this.options.cucumber.featureFileExtension); @@ -45,7 +57,17 @@ export abstract class ImportExecutionConverter< test = this.getTest(attempts[attempts.length - 1]); const issueKey = getNativeTestIssueKey(title, this.options.jira.projectKey); test.testKey = issueKey; - test.testInfo = this.getTestInfo(issueKey, testResult); + if (!issueData.summaries[issueKey]) { + throw new Error(`Summary of corresponding issue is missing: ${issueKey}`); + } + if (!issueData.testTypes[issueKey]) { + throw new Error(`Test type of corresponding issue is missing: ${issueKey}`); + } + test.testInfo = this.getTestInfo( + issueData.summaries[issueKey], + issueData.testTypes[issueKey], + testResult + ); this.addTest(json, test); } catch (error: unknown) { let reason = error; @@ -92,12 +114,14 @@ export abstract class ImportExecutionConverter< * Constructs an {@link XrayTestInfoType} object based on a single * {@link CypressCommandLine.TestResult}. * - * @param issueKey the test issue key + * @param issueSummary the test issue summary + * @param issueTestType the test issue test type * @param testResult the Cypress test result * @returns the test information */ protected abstract getTestInfo( - issueKey: string, + issueSummary: string, + issueTestType: string, testResult: CypressCommandLine.TestResult ): XrayTestInfoType; diff --git a/src/conversion/importExecution/importExecutionConverterCloud.spec.ts b/src/conversion/importExecution/importExecutionConverterCloud.spec.ts index b79e4bbb..454b850e 100644 --- a/src/conversion/importExecution/importExecutionConverterCloud.spec.ts +++ b/src/conversion/importExecution/importExecutionConverterCloud.spec.ts @@ -2,10 +2,12 @@ import { expect } from "chai"; import { readFileSync } from "fs"; import { initOptions } from "../../context"; import { InternalOptions } from "../../types/plugin"; +import { TestIssueData } from "./importExecutionConverter"; import { ImportExecutionConverterCloud } from "./importExecutionConverterCloud"; describe("the import execution results converter (cloud)", () => { let options: InternalOptions; + let testIssueData: TestIssueData; beforeEach(() => { options = initOptions( {}, @@ -22,45 +24,86 @@ describe("the import execution results converter (cloud)", () => { }, } ); + testIssueData = { summaries: {}, testTypes: {} }; }); - it("should use PASSED as default status name for passed tests", () => { + it("uses PASSED as default status name for passed tests", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); + testIssueData.summaries = { + "CYP-40": "This is", + "CYP-41": "a distributed", + "CYP-49": "summary", + }; + testIssueData.testTypes = { + "CYP-40": "Generic", + "CYP-41": "Manual", + "CYP-49": "Cucumber", + }; const converter = new ImportExecutionConverterCloud(options); - const json = converter.convert(result); + const json = converter.convert(result, testIssueData); expect(json.tests[0].status).to.eq("PASSED"); expect(json.tests[1].status).to.eq("PASSED"); }); - it("should use FAILED as default status name for failed tests", () => { + it("uses FAILED as default status name for failed tests", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); + testIssueData.summaries = { + "CYP-40": "This is", + "CYP-41": "a distributed", + "CYP-49": "summary", + }; + testIssueData.testTypes = { + "CYP-40": "Generic", + "CYP-41": "Manual", + "CYP-49": "Cucumber", + }; const converter = new ImportExecutionConverterCloud(options); - const json = converter.convert(result); + const json = converter.convert(result, testIssueData); expect(json.tests[2].status).to.eq("FAILED"); }); - it("should use TODO as default status name for pending tests", () => { + it("uses TODO as default status name for pending tests", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultPending.json", "utf-8") ); + testIssueData.summaries = { + "CYP-123": "This is", + "CYP-456": "a distributed", + "CYP-789": "summary", + "CYP-987": "!!!", + }; + testIssueData.testTypes = { + "CYP-123": "Generic", + "CYP-456": "Manual", + "CYP-789": "Cucumber", + "CYP-987": "No idea", + }; const converter = new ImportExecutionConverterCloud(options); - const json = converter.convert(result); + const json = converter.convert(result, testIssueData); expect(json.tests[0].status).to.eq("TODO"); expect(json.tests[1].status).to.eq("TODO"); expect(json.tests[2].status).to.eq("TODO"); expect(json.tests[3].status).to.eq("TODO"); }); - it("should use FAILED as default status name for skipped tests", () => { + it("uses FAILED as default status name for skipped tests", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultSkipped.json", "utf-8") ); + testIssueData.summaries = { + "CYP-123": "Summary #0", + "CYP-456": "Summary #1", + }; + testIssueData.testTypes = { + "CYP-123": "Generic", + "CYP-456": "Cucumber", + }; const converter = new ImportExecutionConverterCloud(options); - const json = converter.convert(result); + const json = converter.convert(result, testIssueData); expect(json.tests[0].status).to.eq("FAILED"); expect(json.tests[1].status).to.eq("FAILED"); }); diff --git a/src/conversion/importExecution/importExecutionConverterCloud.ts b/src/conversion/importExecution/importExecutionConverterCloud.ts index 45183e34..4275a81c 100644 --- a/src/conversion/importExecution/importExecutionConverterCloud.ts +++ b/src/conversion/importExecution/importExecutionConverterCloud.ts @@ -73,17 +73,15 @@ export class ImportExecutionConverterCloud extends ImportExecutionConverter< } protected getTestInfo( - issueKey: string, + issueSummary: string, + issueTestType: string, testResult: CypressCommandLine.TestResult ): XrayTestInfoCloud { const testInfo: XrayTestInfoCloud = { projectKey: this.options.jira.projectKey, - summary: testResult.title.join(" "), - type: this.options.xray.testTypes[issueKey], + summary: issueSummary, + type: issueTestType, }; - if (!testInfo.type) { - throw new Error(`Failed to find test type for issue: ${issueKey}`); - } if (this.options.xray.steps.update) { testInfo.steps = [{ action: this.truncateStepAction(testResult.body) }]; } diff --git a/src/conversion/importExecution/importExecutionConverterServer.spec.ts b/src/conversion/importExecution/importExecutionConverterServer.spec.ts index 78ceb011..2b0674cc 100644 --- a/src/conversion/importExecution/importExecutionConverterServer.spec.ts +++ b/src/conversion/importExecution/importExecutionConverterServer.spec.ts @@ -2,10 +2,11 @@ import { expect } from "chai"; import { readFileSync } from "fs"; import { initOptions } from "../../context"; import { InternalOptions } from "../../types/plugin"; +import { TestIssueData } from "./importExecutionConverter"; import { ImportExecutionConverterServer } from "./importExecutionConverterServer"; describe("the import execution results converter (server)", () => { let options: InternalOptions; - + let testIssueData: TestIssueData; beforeEach(() => { options = initOptions( {}, @@ -22,45 +23,86 @@ describe("the import execution results converter (server)", () => { }, } ); + testIssueData = { summaries: {}, testTypes: {} }; }); - it("should use PASS as default status name for passed tests", () => { + it("uses PASS as default status name for passed tests", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); + testIssueData.summaries = { + "CYP-40": "This is", + "CYP-41": "a distributed", + "CYP-49": "summary", + }; + testIssueData.testTypes = { + "CYP-40": "Generic", + "CYP-41": "Manual", + "CYP-49": "Cucumber", + }; const converter = new ImportExecutionConverterServer(options); - const json = converter.convert(result); + const json = converter.convert(result, testIssueData); expect(json.tests[0].status).to.eq("PASS"); expect(json.tests[1].status).to.eq("PASS"); }); - it("should use FAIL as default status name for failed tests", () => { + it("uses FAIL as default status name for failed tests", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); + testIssueData.summaries = { + "CYP-40": "This is", + "CYP-41": "a distributed", + "CYP-49": "summary", + }; + testIssueData.testTypes = { + "CYP-40": "Generic", + "CYP-41": "Manual", + "CYP-49": "Cucumber", + }; const converter = new ImportExecutionConverterServer(options); - const json = converter.convert(result); + const json = converter.convert(result, testIssueData); expect(json.tests[2].status).to.eq("FAIL"); }); - it("should use TODO as default status name for pending tests", () => { + it("uses TODO as default status name for pending tests", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultPending.json", "utf-8") ); + testIssueData.summaries = { + "CYP-123": "This is", + "CYP-456": "a distributed", + "CYP-789": "summary", + "CYP-987": "!!!", + }; + testIssueData.testTypes = { + "CYP-123": "Generic", + "CYP-456": "Manual", + "CYP-789": "Cucumber", + "CYP-987": "No idea", + }; const converter = new ImportExecutionConverterServer(options); - const json = converter.convert(result); + const json = converter.convert(result, testIssueData); expect(json.tests[0].status).to.eq("TODO"); expect(json.tests[1].status).to.eq("TODO"); expect(json.tests[2].status).to.eq("TODO"); expect(json.tests[3].status).to.eq("TODO"); }); - it("should use FAIL as default status name for skipped tests", () => { + it("uses FAIL as default status name for skipped tests", () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultSkipped.json", "utf-8") ); + testIssueData.summaries = { + "CYP-123": "Summary #0", + "CYP-456": "Summary #1", + }; + testIssueData.testTypes = { + "CYP-123": "Generic", + "CYP-456": "Cucumber", + }; const converter = new ImportExecutionConverterServer(options); - const json = converter.convert(result); + const json = converter.convert(result, testIssueData); expect(json.tests[0].status).to.eq("FAIL"); expect(json.tests[1].status).to.eq("FAIL"); }); diff --git a/src/conversion/importExecution/importExecutionConverterServer.ts b/src/conversion/importExecution/importExecutionConverterServer.ts index e6999ffe..f18e7ac4 100644 --- a/src/conversion/importExecution/importExecutionConverterServer.ts +++ b/src/conversion/importExecution/importExecutionConverterServer.ts @@ -73,17 +73,15 @@ export class ImportExecutionConverterServer extends ImportExecutionConverter< } protected getTestInfo( - issueKey: string, + issueSummary: string, + issueTestType: string, testResult: CypressCommandLine.TestResult ): XrayTestInfoServer { const testInfo: XrayTestInfoServer = { projectKey: this.options.jira.projectKey, - summary: testResult.title.join(" "), - testType: this.options.xray.testTypes[issueKey], + summary: issueSummary, + testType: issueTestType, }; - if (!testInfo.testType) { - throw new Error(`Failed to find test type for issue: ${issueKey}`); - } if (this.options.xray.steps.update) { testInfo.steps = [{ action: this.truncateStepAction(testResult.body) }]; } diff --git a/src/hooks.ts b/src/hooks.ts index 0337d064..283dbdb1 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -299,13 +299,19 @@ async function uploadCypressResults( jiraRepository: JiraRepositoryServer | JiraRepositoryCloud ) { const issueKeys = getNativeTestIssueKeys(runResult, options); - const testTypes = await jiraRepository.getTestTypes(options.jira.projectKey, ...issueKeys); - options.xray.testTypes = testTypes; + const issueSummaries = await jiraRepository.getSummaries(options.jira.projectKey, ...issueKeys); + const issueTestTypes = await jiraRepository.getTestTypes(options.jira.projectKey, ...issueKeys); let cypressExecution: XrayTestExecutionResultsServer | XrayTestExecutionResultsCloud; if (xrayClient instanceof XrayClientServer) { - cypressExecution = new ImportExecutionConverterServer(options).convert(runResult); + cypressExecution = new ImportExecutionConverterServer(options).convert(runResult, { + summaries: issueSummaries, + testTypes: issueTestTypes, + }); } else { - cypressExecution = new ImportExecutionConverterCloud(options).convert(runResult); + cypressExecution = new ImportExecutionConverterCloud(options).convert(runResult, { + summaries: issueSummaries, + testTypes: issueTestTypes, + }); } return await xrayClient.importExecution(cypressExecution); } diff --git a/src/types/plugin.ts b/src/types/plugin.ts index d95dae1e..0d84bb8b 100644 --- a/src/types/plugin.ts +++ b/src/types/plugin.ts @@ -6,7 +6,7 @@ import { XrayClientServer } from "../client/xray/xrayClientServer"; import { JiraRepositoryCloud } from "../repository/jira/jiraRepositoryCloud"; import { JiraRepositoryServer } from "../repository/jira/jiraRepositoryServer"; import { IssueTypeDetailsCloud, IssueTypeDetailsServer } from "./jira/responses/issueTypeDetails"; -import { OneOf, StringMap } from "./util"; +import { OneOf } from "./util"; export interface Options { jira: JiraOptions; @@ -236,14 +236,6 @@ export type InternalOptions = Options & { */ testPlanIssueDetails?: OneOf<[IssueTypeDetailsServer, IssueTypeDetailsCloud]>; }; - xray?: { - /** - * A mapping of issue keys to test types. Required for Cypress execution import, since the - * `testType` (Xray Server) or `type` (Xray Cloud) properties are required by Xray's JSON - * scheme for uploading results. - */ - testTypes?: StringMap; - }; cucumber?: { preprocessor?: Awaited>; }; From 989305c96d636c41b7299e2e29ec3be4276bca64 Mon Sep 17 00:00:00 2001 From: Sebastian Vollbrecht Date: Fri, 28 Jul 2023 01:15:41 +0200 Subject: [PATCH 11/19] Lowercase field names --- src/repository/jira/jiraRepository.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/repository/jira/jiraRepository.ts b/src/repository/jira/jiraRepository.ts index e943ad8a..75987bbf 100644 --- a/src/repository/jira/jiraRepository.ts +++ b/src/repository/jira/jiraRepository.ts @@ -51,20 +51,23 @@ export abstract class JiraRepository< } public async getFieldId(fieldName: string): Promise { - if (!(fieldName in this.fieldIds)) { + // Lowercase everything to work around case sensitivities. + // Jira sometimes returns field names capitalized, sometimes it doesn't. + const lowerCasedName = fieldName.toLowerCase(); + if (!(lowerCasedName in this.fieldIds)) { const jiraFields = await this.jiraClient.getFields(); if (jiraFields) { jiraFields.forEach((jiraField) => { - this.fieldIds[jiraField.name] = jiraField.id; + this.fieldIds[jiraField.name.toLowerCase()] = jiraField.id; }); } - if (!(fieldName in this.fieldIds)) { + if (!(lowerCasedName in this.fieldIds)) { throw new Error( - `Failed to fetch Jira field ID for field: ${fieldName}\nMake sure the field actually exists` + `Failed to fetch Jira field ID for field with name: ${lowerCasedName}\nMake sure the field actually exists` ); } } - return this.fieldIds[fieldName]; + return this.fieldIds[lowerCasedName]; } public async getSummaries(...issueKeys: string[]): Promise> { From 9e1df0c416397a128cd693a2671f385074534178 Mon Sep 17 00:00:00 2001 From: Sebastian Vollbrecht Date: Fri, 28 Jul 2023 01:15:58 +0200 Subject: [PATCH 12/19] Remove unnecessary project key argument --- src/hooks.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks.ts b/src/hooks.ts index 283dbdb1..c79310f3 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -299,8 +299,8 @@ async function uploadCypressResults( jiraRepository: JiraRepositoryServer | JiraRepositoryCloud ) { const issueKeys = getNativeTestIssueKeys(runResult, options); - const issueSummaries = await jiraRepository.getSummaries(options.jira.projectKey, ...issueKeys); - const issueTestTypes = await jiraRepository.getTestTypes(options.jira.projectKey, ...issueKeys); + const issueSummaries = await jiraRepository.getSummaries(...issueKeys); + const issueTestTypes = await jiraRepository.getTestTypes(...issueKeys); let cypressExecution: XrayTestExecutionResultsServer | XrayTestExecutionResultsCloud; if (xrayClient instanceof XrayClientServer) { cypressExecution = new ImportExecutionConverterServer(options).convert(runResult, { From bab06d21296b02547aac52c5f551c22c559a3cc5 Mon Sep 17 00:00:00 2001 From: Sebastian Vollbrecht Date: Fri, 28 Jul 2023 01:21:07 +0200 Subject: [PATCH 13/19] Fix error messages --- src/repository/jira/jiraRepository.ts | 16 ++++++++++++++-- .../jira/jiraRepositoryCloud.spec.ts | 8 ++++---- src/repository/jira/jiraRepositoryCloud.ts | 16 ---------------- .../jira/jiraRepositoryServer.spec.ts | 18 +++++++++--------- src/repository/jira/jiraRepositoryServer.ts | 18 +----------------- 5 files changed, 28 insertions(+), 48 deletions(-) diff --git a/src/repository/jira/jiraRepository.ts b/src/repository/jira/jiraRepository.ts index 75987bbf..577ddb59 100644 --- a/src/repository/jira/jiraRepository.ts +++ b/src/repository/jira/jiraRepository.ts @@ -151,9 +151,21 @@ export abstract class JiraRepository< return result; } - protected abstract fetchSummaries(...issueKeys: string[]): Promise>; + protected async fetchSummaries(...issueKeys: string[]): Promise> { + // Field property example: + // summary: "Bug 12345" + return await this.getJiraField("summary", JiraRepository.STRING_EXTRACTOR, ...issueKeys); + } - protected abstract fetchDescriptions(...issueKeys: string[]): Promise>; + protected async fetchDescriptions(...issueKeys: string[]): Promise> { + // Field property example: + // description: "This is a description" + return await this.getJiraField( + "description", + JiraRepository.STRING_EXTRACTOR, + ...issueKeys + ); + } protected abstract fetchTestTypes(...issueKeys: string[]): Promise>; diff --git a/src/repository/jira/jiraRepositoryCloud.spec.ts b/src/repository/jira/jiraRepositoryCloud.spec.ts index 1e290191..e52584d9 100644 --- a/src/repository/jira/jiraRepositoryCloud.spec.ts +++ b/src/repository/jira/jiraRepositoryCloud.spec.ts @@ -209,7 +209,7 @@ describe("the cloud issue repository", () => { expect(stubbedError).to.have.been.calledOnceWithExactly( dedent(` Failed to fetch issue summaries - Failed to fetch Jira field ID for field: summary + Failed to fetch Jira field ID for field with name: summary Make sure the field actually exists `) ); @@ -225,7 +225,7 @@ describe("the cloud issue repository", () => { expect(stubbedError).to.have.been.calledOnceWithExactly( dedent(` Failed to fetch issue summaries - Failed to fetch Jira field ID for field: summary + Failed to fetch Jira field ID for field with name: summary Make sure the field actually exists `) ); @@ -473,7 +473,7 @@ describe("the cloud issue repository", () => { expect(stubbedError).to.have.been.calledOnceWithExactly( dedent(` Failed to fetch issue descriptions - Failed to fetch Jira field ID for field: description + Failed to fetch Jira field ID for field with name: description Make sure the field actually exists `) ); @@ -497,7 +497,7 @@ describe("the cloud issue repository", () => { expect(stubbedError).to.have.been.calledOnceWithExactly( dedent(` Failed to fetch issue descriptions - Failed to fetch Jira field ID for field: description + Failed to fetch Jira field ID for field with name: description Make sure the field actually exists `) ); diff --git a/src/repository/jira/jiraRepositoryCloud.ts b/src/repository/jira/jiraRepositoryCloud.ts index ab28bfe6..d1a95471 100644 --- a/src/repository/jira/jiraRepositoryCloud.ts +++ b/src/repository/jira/jiraRepositoryCloud.ts @@ -4,22 +4,6 @@ import { StringMap } from "../../types/util"; import { JiraRepository } from "./jiraRepository"; export class JiraRepositoryCloud extends JiraRepository { - protected async fetchSummaries(...issueKeys: string[]): Promise> { - // Field property example: - // summary: "Bug 12345" - return await this.getJiraField("summary", JiraRepository.STRING_EXTRACTOR, ...issueKeys); - } - - protected async fetchDescriptions(...issueKeys: string[]): Promise> { - // Field property example: - // description: "This is a description" - return await this.getJiraField( - "description", - JiraRepository.STRING_EXTRACTOR, - ...issueKeys - ); - } - protected async fetchTestTypes(...issueKeys: string[]): Promise> { const testTypes = await this.xrayClient.getTestTypes( this.options.jira.projectKey, diff --git a/src/repository/jira/jiraRepositoryServer.spec.ts b/src/repository/jira/jiraRepositoryServer.spec.ts index b0f2c3e3..a72a41da 100644 --- a/src/repository/jira/jiraRepositoryServer.spec.ts +++ b/src/repository/jira/jiraRepositoryServer.spec.ts @@ -210,7 +210,7 @@ describe("the server issue repository", () => { expect(stubbedError).to.have.been.calledOnceWithExactly( dedent(` Failed to fetch issue summaries - Failed to fetch Jira field ID for field: Summary + Failed to fetch Jira field ID for field with name: summary Make sure the field actually exists `) ); @@ -234,7 +234,7 @@ describe("the server issue repository", () => { expect(stubbedError).to.have.been.calledOnceWithExactly( dedent(` Failed to fetch issue summaries - Failed to fetch Jira field ID for field: Summary + Failed to fetch Jira field ID for field with name: summary Make sure the field actually exists `) ); @@ -294,7 +294,7 @@ describe("the server issue repository", () => { expect(stubbedError).to.have.been.calledOnceWithExactly( dedent(` Failed to fetch issue summaries - Failed to parse the following Jira field of some issues: Summary + Failed to parse the following Jira field of some issues: summary Expected the field to be: a string Make sure the correct field is present on the following issues: @@ -482,7 +482,7 @@ describe("the server issue repository", () => { expect(stubbedError).to.have.been.calledOnceWithExactly( dedent(` Failed to fetch issue descriptions - Failed to fetch Jira field ID for field: Description + Failed to fetch Jira field ID for field with name: description Make sure the field actually exists `) ); @@ -506,7 +506,7 @@ describe("the server issue repository", () => { expect(stubbedError).to.have.been.calledOnceWithExactly( dedent(` Failed to fetch issue descriptions - Failed to fetch Jira field ID for field: Description + Failed to fetch Jira field ID for field with name: description Make sure the field actually exists `) ); @@ -566,7 +566,7 @@ describe("the server issue repository", () => { expect(stubbedError).to.have.been.calledOnceWithExactly( dedent(` Failed to fetch issue descriptions - Failed to parse the following Jira field of some issues: Description + Failed to parse the following Jira field of some issues: description Expected the field to be: a string Make sure the correct field is present on the following issues: @@ -792,7 +792,7 @@ describe("the server issue repository", () => { expect(stubbedError).to.have.been.calledOnceWithExactly( dedent(` Failed to fetch issue test types - Failed to fetch Jira field ID for field: Test Type + Failed to fetch Jira field ID for field with name: test type Make sure the field actually exists `) ); @@ -816,7 +816,7 @@ describe("the server issue repository", () => { expect(stubbedError).to.have.been.calledOnceWithExactly( dedent(` Failed to fetch issue test types - Failed to fetch Jira field ID for field: Test Type + Failed to fetch Jira field ID for field with name: test type Make sure the field actually exists `) ); @@ -896,7 +896,7 @@ describe("the server issue repository", () => { expect(stubbedError).to.have.been.calledOnceWithExactly( dedent(` Failed to fetch issue test types - Failed to parse the following Jira field of some issues: Test Type + Failed to parse the following Jira field of some issues: test type Expected the field to be: an object with a value property Make sure the correct field is present on the following issues: diff --git a/src/repository/jira/jiraRepositoryServer.ts b/src/repository/jira/jiraRepositoryServer.ts index d64f9f68..b40f65a9 100644 --- a/src/repository/jira/jiraRepositoryServer.ts +++ b/src/repository/jira/jiraRepositoryServer.ts @@ -4,22 +4,6 @@ import { StringMap } from "../../types/util"; import { JiraRepository } from "./jiraRepository"; export class JiraRepositoryServer extends JiraRepository { - protected async fetchSummaries(...issueKeys: string[]): Promise> { - // Field property example: - // summary: "Bug 12345" - return await this.getJiraField("Summary", JiraRepository.STRING_EXTRACTOR, ...issueKeys); - } - - protected async fetchDescriptions(...issueKeys: string[]): Promise> { - // Field property example: - // description: "This is a description" - return await this.getJiraField( - "Description", - JiraRepository.STRING_EXTRACTOR, - ...issueKeys - ); - } - protected async fetchTestTypes(...issueKeys: string[]): Promise> { // Field property example: // customfield_12100: { @@ -28,7 +12,7 @@ export class JiraRepositoryServer extends JiraRepository Date: Fri, 28 Jul 2023 01:29:29 +0200 Subject: [PATCH 14/19] Make debug output more readable --- src/client/jira/jiraClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/jira/jiraClient.ts b/src/client/jira/jiraClient.ts index bb04f2de..c60ee728 100644 --- a/src/client/jira/jiraClient.ts +++ b/src/client/jira/jiraClient.ts @@ -149,7 +149,7 @@ export abstract class JiraClient< "Received data for issue types:", ...response.data.map( (issueType: IssueTypeDetailsResponse) => - `${issueType.name} (id: ${issueType.id})` + `\n${issueType.name} (id: ${issueType.id})` ) ); return response.data; From ac3f58037a61328d517fccb196638cfff1093303 Mon Sep 17 00:00:00 2001 From: Sebastian Vollbrecht Date: Sat, 29 Jul 2023 08:32:22 +0200 Subject: [PATCH 15/19] Fix `plugin.disabled` behaviour --- src/hooks.spec.ts | 32 ++++++++++++++++---------------- src/hooks.ts | 2 +- src/plugin.ts | 30 ++++++++++++++---------------- src/types/plugin.ts | 8 ++++---- 4 files changed, 35 insertions(+), 37 deletions(-) diff --git a/src/hooks.spec.ts b/src/hooks.spec.ts index d393cb6a..5c1d5f05 100644 --- a/src/hooks.spec.ts +++ b/src/hooks.spec.ts @@ -40,7 +40,7 @@ describe("the before run hook", () => { it("should throw if the plugin was not configured", async () => { const { stubbedError } = stubLogging(); - await beforeRunHook(config, beforeRunDetails); + await beforeRunHook(beforeRunDetails, config); expect(stubbedError).to.have.been.calledOnceWith( dedent(` Plugin misconfigured: configureXrayPlugin() was not called. Skipping before:run hook @@ -53,7 +53,7 @@ describe("the before run hook", () => { it("should not do anything if disabled", async () => { const { stubbedInfo } = stubLogging(); options.plugin.enabled = false; - await beforeRunHook(config, beforeRunDetails, options); + await beforeRunHook(beforeRunDetails, config, options); expect(stubbedInfo).to.have.been.calledOnceWith( "Plugin disabled. Skipping before:run hook" ); @@ -61,7 +61,7 @@ describe("the before run hook", () => { it("should throw if the xray client was not configured", async () => { await expect( - beforeRunHook(config, beforeRunDetails, options) + beforeRunHook(beforeRunDetails, config, options) ).to.eventually.be.rejectedWith( dedent(` Plugin misconfigured: Xray client was not configured @@ -73,7 +73,7 @@ describe("the before run hook", () => { it("should throw if the jira client was not configured", async () => { await expect( - beforeRunHook(config, beforeRunDetails, options, new DummyXrayClient()) + beforeRunHook(beforeRunDetails, config, options, new DummyXrayClient()) ).to.eventually.be.rejectedWith( dedent(` Plugin misconfigured: Jira client was not configured @@ -89,8 +89,8 @@ describe("the before run hook", () => { ); options.xray.uploadResults = false; await beforeRunHook( - config, beforeRunDetails, + config, options, new DummyXrayClient(), new DummyJiraClient() @@ -105,8 +105,8 @@ describe("the before run hook", () => { config.env["jsonEnabled"] = false; await expect( beforeRunHook( - config, beforeRunDetails, + config, options, new DummyXrayClient(), new DummyJiraClient() @@ -127,8 +127,8 @@ describe("the before run hook", () => { config.env["jsonOutput"] = ""; await expect( beforeRunHook( - config, beforeRunDetails, + config, options, new DummyXrayClient(), new DummyJiraClient() @@ -166,8 +166,8 @@ describe("the before run hook", () => { config: null, }); await beforeRunHook( - config, beforeRunDetails, + config, options, new DummyXrayClient(), new JiraClientCloud("https://example.org", new BasicAuthCredentials("user", "token")) @@ -189,8 +189,8 @@ describe("the before run hook", () => { const { stubbedInfo } = stubLogging(); beforeRunDetails = JSON.parse(readFileSync("./test/resources/beforeRun.json", "utf-8")); await beforeRunHook( - config, beforeRunDetails, + config, options, new DummyXrayClient(), new DummyJiraClient() @@ -219,8 +219,8 @@ describe("the before run hook", () => { }); await expect( beforeRunHook( - config, beforeRunDetails, + config, options, new DummyXrayClient(), new JiraClientCloud( @@ -266,8 +266,8 @@ describe("the before run hook", () => { }); await expect( beforeRunHook( - config, beforeRunDetails, + config, options, new DummyXrayClient(), new JiraClientCloud( @@ -317,8 +317,8 @@ describe("the before run hook", () => { }); await expect( beforeRunHook( - config, beforeRunDetails, + config, options, new DummyXrayClient(), new JiraClientCloud( @@ -361,8 +361,8 @@ describe("the before run hook", () => { }); await expect( beforeRunHook( - config, beforeRunDetails, + config, options, new DummyXrayClient(), new JiraClientCloud( @@ -402,8 +402,8 @@ describe("the before run hook", () => { config: null, }); await beforeRunHook( - config, beforeRunDetails, + config, options, new DummyXrayClient(), new JiraClientCloud("https://example.org", new BasicAuthCredentials("user", "token")) @@ -439,8 +439,8 @@ describe("the before run hook", () => { config: null, }); await beforeRunHook( - config, beforeRunDetails, + config, options, new DummyXrayClient(), new JiraClientCloud("https://example.org", new BasicAuthCredentials("user", "token")) @@ -474,8 +474,8 @@ describe("the before run hook", () => { ); await expect( beforeRunHook( - config, beforeRunDetails, + config, options, new DummyXrayClient(), new JiraClientCloud( diff --git a/src/hooks.ts b/src/hooks.ts index c79310f3..a5b2bcfc 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -35,8 +35,8 @@ import { } from "./types/xray/requests/importExecutionCucumberMultipart"; export async function beforeRunHook( - config: Cypress.PluginConfigOptions, runDetails: Cypress.BeforeRunDetails, + config?: Cypress.PluginConfigOptions, options?: InternalOptions, xrayClient?: XrayClientServer | XrayClientCloud, jiraClient?: JiraClientServer | JiraClientCloud diff --git a/src/plugin.ts b/src/plugin.ts index 9d21dedb..f00cbbd7 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -7,33 +7,31 @@ import { Options, PluginContext } from "./types/plugin"; let context: PluginContext; export async function configureXrayPlugin(config: Cypress.PluginConfigOptions, options: Options) { - const internalOptions = initOptions(config.env, options); - if (!internalOptions.plugin.enabled) { - logInfo("Plugin disabled. Skipping configuration verification."); - return; - } - verifyOptions(internalOptions); - const clients = initClients(internalOptions, config.env); - const jiraRepository = initJiraRepository(clients, options); context = { - internal: internalOptions, cypress: config, - xrayClient: clients.xrayClient, - jiraClient: clients.jiraClient, - jiraRepository: jiraRepository, + internal: initOptions(config.env, options), }; - Requests.init(internalOptions); + if (!context.internal.plugin.enabled) { + logInfo("Plugin disabled. Skipping configuration verification."); + return; + } + verifyOptions(context.internal); + const clients = initClients(context.internal, config.env); + context.xrayClient = clients.xrayClient; + context.jiraClient = clients.jiraClient; + context.jiraRepository = initJiraRepository(clients, options); + Requests.init(context.internal); initLogging({ - debug: internalOptions.plugin.debug, - logDirectory: internalOptions.plugin.logDirectory, + debug: context.internal.plugin.debug, + logDirectory: context.internal.plugin.logDirectory, }); } export async function addXrayResultUpload(on: Cypress.PluginEvents) { on("before:run", async (runDetails: Cypress.BeforeRunDetails) => { await beforeRunHook( - context.cypress, runDetails, + context.cypress, context.internal, context.xrayClient, context.jiraClient diff --git a/src/types/plugin.ts b/src/types/plugin.ts index 0d84bb8b..e9732432 100644 --- a/src/types/plugin.ts +++ b/src/types/plugin.ts @@ -242,9 +242,9 @@ export type InternalOptions = Options & { }; export interface PluginContext { - xrayClient: XrayClientServer | XrayClientCloud; - jiraClient: JiraClientServer | JiraClientCloud; - jiraRepository: JiraRepositoryServer | JiraRepositoryCloud; - internal: InternalOptions; cypress: Cypress.PluginConfigOptions; + internal: InternalOptions; + xrayClient?: XrayClientServer | XrayClientCloud; + jiraClient?: JiraClientServer | JiraClientCloud; + jiraRepository?: JiraRepositoryServer | JiraRepositoryCloud; } From 986b36c8c0fd27a9184ab6e64f37edb6abd9acaf Mon Sep 17 00:00:00 2001 From: Sebastian Vollbrecht Date: Sat, 29 Jul 2023 09:57:18 +0200 Subject: [PATCH 16/19] Reset summaries after successful feature import --- src/client/jira/jiraClient.spec.ts | 14 ++-- src/client/jira/jiraClient.ts | 89 ++++++++++++++++++++----- src/client/jira/jiraClientCloud.ts | 8 ++- src/client/jira/jiraClientServer.ts | 8 ++- src/client/xray/xrayClient.ts | 5 +- src/hooks.spec.ts | 8 +-- src/hooks.ts | 80 ++++++++++++++++++++-- src/https/requests.ts | 11 +++ src/logging/logging.ts | 2 +- src/plugin.ts | 4 +- src/preprocessing/preprocessing.spec.ts | 18 ++--- src/preprocessing/preprocessing.ts | 22 +++++- test/util.ts | 5 +- 13 files changed, 227 insertions(+), 47 deletions(-) diff --git a/src/client/jira/jiraClient.spec.ts b/src/client/jira/jiraClient.spec.ts index bfe711d9..b593e32d 100644 --- a/src/client/jira/jiraClient.spec.ts +++ b/src/client/jira/jiraClient.spec.ts @@ -1,5 +1,6 @@ import { AxiosError, AxiosHeaders, HttpStatusCode } from "axios"; import { expect } from "chai"; +import dedent from "dedent"; import fs from "fs"; import { expectToExist, resolveTestDirPath, stubLogging, stubRequests } from "../../../test/util"; import { BasicAuthCredentials } from "../../authentication/credentials"; @@ -73,8 +74,10 @@ describe("the jira clients", () => { "./test/resources/turtle.png" ); expect(stubbedSuccess).to.have.been.calledOnceWith( - "Successfully attached files to issue CYP-123:", - "turtle.png" + dedent(` + Successfully attached files to issue: CYP-123 + turtle.png + `) ); }); it("returns the correct values", async () => { @@ -128,8 +131,11 @@ describe("the jira clients", () => { "./test/resources/greetings.txt" ); expect(stubbedSuccess).to.have.been.calledOnceWith( - "Successfully attached files to issue CYP-123:", - "turtle.png, greetings.txt" + dedent(` + Successfully attached files to issue: CYP-123 + turtle.png + greetings.txt + `) ); }); it("returns the correct values", async () => { diff --git a/src/client/jira/jiraClient.ts b/src/client/jira/jiraClient.ts index c60ee728..dc11b1e7 100644 --- a/src/client/jira/jiraClient.ts +++ b/src/client/jira/jiraClient.ts @@ -1,4 +1,5 @@ import { AxiosResponse } from "axios"; +import dedent from "dedent"; import FormData from "form-data"; import fs from "fs"; import { BasicAuthCredentials, HTTPHeader, PATCredentials } from "../../authentication/credentials"; @@ -19,9 +20,9 @@ import { IssueTypeDetailsCloud, IssueTypeDetailsServer, } from "../../types/jira/responses/issueTypeDetails"; +import { IssueUpdateCloud, IssueUpdateServer } from "../../types/jira/responses/issueUpdate"; import { JsonTypeCloud, JsonTypeServer } from "../../types/jira/responses/jsonType"; import { SearchResults } from "../../types/jira/responses/searchResults"; -import { OneOf } from "../../types/util"; import { Client } from "../client"; /** @@ -33,8 +34,9 @@ export abstract class JiraClient< FieldDetailType extends FieldDetailServer | FieldDetailCloud, JsonType extends JsonTypeServer | JsonTypeCloud, IssueType extends IssueServer | IssueCloud, - IssueTypeDetailsResponse extends OneOf<[IssueTypeDetailsServer, IssueTypeDetailsCloud]>, - SearchRequestType extends SearchRequestServer | SearchRequestCloud + IssueTypeDetailsResponse extends IssueTypeDetailsServer | IssueTypeDetailsCloud, + SearchRequestType extends SearchRequestServer | SearchRequestCloud, + IssueUpdateType extends IssueUpdateServer | IssueUpdateCloud > extends Client { /** * Construct a new Jira client using the provided credentials. @@ -99,10 +101,12 @@ export abstract class JiraClient< } ); logSuccess( - `Successfully attached files to issue ${issueIdOrKey}:`, - response.data - .map((attachment: AttachmentType) => attachment.filename) - .join(", ") + dedent(` + Successfully attached files to issue: ${issueIdOrKey} + ${response.data + .map((attachment: AttachmentType) => attachment.filename) + .join("\n")} + `) ); return response.data; } finally { @@ -146,11 +150,15 @@ export abstract class JiraClient< ); logSuccess(`Successfully retrieved data for ${response.data.length} issue types.`); logDebug( - "Received data for issue types:", - ...response.data.map( - (issueType: IssueTypeDetailsResponse) => - `\n${issueType.name} (id: ${issueType.id})` - ) + dedent(` + Received data for issue types: + ${response.data + .map( + (issueType: IssueTypeDetailsResponse) => + `${issueType.name} (id: ${issueType.id})` + ) + .join("\n")} + `) ); return response.data; } finally { @@ -199,10 +207,12 @@ export abstract class JiraClient< ); logSuccess(`Successfully retrieved data for ${response.data.length} fields`); logDebug( - "Received data for fields:", - ...response.data.map( - (field: FieldDetailType) => `\n${field.name} (id: ${field.id})` - ) + dedent(` + Received data for fields: + ${response.data + .map((field: FieldDetailType) => `${field.name} (id: ${field.id})`) + .join("\n")} + `) ); return response.data; } finally { @@ -275,4 +285,51 @@ export abstract class JiraClient< * @returns the endpoint */ public abstract getUrlPostSearch(): string; + + /** + * Edits an issue. A transition may be applied and issue properties updated as part of the edit. + * The edits to the issue's fields are defined using `update` and `fields`. + * + * The parent field may be set by key or ID. For standard issue types, the parent may be removed + * by setting `update.parent.set.none` to `true`. + * + * @param issueIdOrKey the ID or key of the issue + * @param issueUpdateData the edit data + * @returns the ID or key of the edited issue or `undefined` in case of errors + * @see https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-issueidorkey-put + * @see https://docs.atlassian.com/software/jira/docs/api/REST/9.10.0/#api/2/issue-editIssue + */ + public async editIssue( + issueIdOrKey: string, + issueUpdateData: IssueUpdateType + ): Promise { + try { + await this.credentials.getAuthenticationHeader().then(async (header: HTTPHeader) => { + logInfo(`Editing issue...`); + const progressInterval = this.startResponseInterval(this.apiBaseURL); + try { + await Requests.put(this.getUrlEditIssue(issueIdOrKey), issueUpdateData, { + headers: { + ...header, + }, + }); + logSuccess(`Successfully edited issue: ${issueIdOrKey}`); + } finally { + clearInterval(progressInterval); + } + }); + return issueIdOrKey; + } catch (error: unknown) { + logError(`Failed to edit issue: ${error}`); + writeErrorFile(error, "editIssue"); + } + } + /** + * + * Returns the endpoint to use for editing issues. + * + * @param issueIdOrKey the ID or key of the issue + * @returns the endpoint + */ + public abstract getUrlEditIssue(issueIdOrKey: string): string; } diff --git a/src/client/jira/jiraClientCloud.ts b/src/client/jira/jiraClientCloud.ts index 3c40cbaf..a9ccc90d 100644 --- a/src/client/jira/jiraClientCloud.ts +++ b/src/client/jira/jiraClientCloud.ts @@ -4,6 +4,7 @@ import { AttachmentCloud } from "../../types/jira/responses/attachment"; import { FieldDetailCloud } from "../../types/jira/responses/fieldDetail"; import { IssueCloud } from "../../types/jira/responses/issue"; import { IssueTypeDetailsCloud } from "../../types/jira/responses/issueTypeDetails"; +import { IssueUpdateCloud } from "../../types/jira/responses/issueUpdate"; import { JsonTypeCloud } from "../../types/jira/responses/jsonType"; import { JiraClient } from "./jiraClient"; @@ -17,7 +18,8 @@ export class JiraClientCloud extends JiraClient< JsonTypeCloud, IssueCloud, IssueTypeDetailsCloud, - SearchRequestCloud + SearchRequestCloud, + IssueUpdateCloud > { public getUrlAddAttachment(issueIdOrKey: string): string { return `${this.apiBaseURL}/rest/api/3/issue/${issueIdOrKey}/attachments`; @@ -34,4 +36,8 @@ export class JiraClientCloud extends JiraClient< public getUrlPostSearch(): string { return `${this.apiBaseURL}/rest/api/3/search`; } + + public getUrlEditIssue(issueIdOrKey: string): string { + return `${this.apiBaseURL}/rest/api/3/issue/${issueIdOrKey}`; + } } diff --git a/src/client/jira/jiraClientServer.ts b/src/client/jira/jiraClientServer.ts index 5300811d..a38fcdb0 100644 --- a/src/client/jira/jiraClientServer.ts +++ b/src/client/jira/jiraClientServer.ts @@ -4,6 +4,7 @@ import { AttachmentServer } from "../../types/jira/responses/attachment"; import { FieldDetailServer } from "../../types/jira/responses/fieldDetail"; import { IssueServer } from "../../types/jira/responses/issue"; import { IssueTypeDetailsServer } from "../../types/jira/responses/issueTypeDetails"; +import { IssueUpdateServer } from "../../types/jira/responses/issueUpdate"; import { JsonTypeServer } from "../../types/jira/responses/jsonType"; import { JiraClient } from "./jiraClient"; @@ -17,7 +18,8 @@ export class JiraClientServer extends JiraClient< JsonTypeServer, IssueServer, IssueTypeDetailsServer, - SearchRequestServer + SearchRequestServer, + IssueUpdateServer > { public getUrlAddAttachment(issueIdOrKey: string): string { return `${this.apiBaseURL}/rest/api/2/issue/${issueIdOrKey}/attachments`; @@ -34,4 +36,8 @@ export class JiraClientServer extends JiraClient< public getUrlPostSearch(): string { return `${this.apiBaseURL}/rest/api/2/search`; } + + public getUrlEditIssue(issueIdOrKey: string): string { + return `${this.apiBaseURL}/rest/api/2/issue/${issueIdOrKey}`; + } } diff --git a/src/client/xray/xrayClient.ts b/src/client/xray/xrayClient.ts index f7636fec..b32e1656 100644 --- a/src/client/xray/xrayClient.ts +++ b/src/client/xray/xrayClient.ts @@ -151,6 +151,7 @@ export abstract class XrayClient< * @param projectKey key of the project where the tests and pre-conditions are going to be created * @param projectId id of the project where the tests and pre-conditions are going to be created * @param source a name designating the source of the features being imported (e.g. the source project name) + * @returns `true` if the import was successful, `false` otherwise * @see https://docs.getxray.app/display/XRAY/Importing+Cucumber+Tests+-+REST * @see https://docs.getxray.app/display/XRAYCLOUD/Importing+Cucumber+Tests+-+REST+v2 */ @@ -159,7 +160,7 @@ export abstract class XrayClient< projectKey?: string, projectId?: string, source?: string - ): Promise { + ): Promise { try { const authenticationHeader = await this.credentials.getAuthenticationHeader( `${this.apiBaseURL}/authenticate` @@ -182,6 +183,7 @@ export abstract class XrayClient< } ); this.handleResponseImportFeature(response.data); + return true; } finally { clearInterval(progressInterval); } @@ -189,6 +191,7 @@ export abstract class XrayClient< logError(`Failed to import cucumber features: ${error}`); writeErrorFile(error, "importFeatureError"); } + return false; } /** diff --git a/src/hooks.spec.ts b/src/hooks.spec.ts index 5c1d5f05..5a6e2f65 100644 --- a/src/hooks.spec.ts +++ b/src/hooks.spec.ts @@ -651,7 +651,7 @@ describe("the synchronize file hook", () => { it("should display errors if the plugin was not configured", async () => { const { stubbedError } = stubLogging(); - await synchronizeFile(file, "."); + await synchronizeFile(file, ".", null, null, null, null); expect(stubbedError).to.have.been.calledOnce; expect(stubbedError).to.have.been.calledWith( dedent(` @@ -666,7 +666,7 @@ describe("the synchronize file hook", () => { file.filePath = "./test/resources/features/taggedCloud.feature"; const { stubbedInfo } = stubLogging(); options.plugin = { enabled: false }; - await synchronizeFile(file, ".", options); + await synchronizeFile(file, ".", options, null, null, null); expect(stubbedInfo).to.have.been.calledOnce; expect(stubbedInfo).to.have.been.calledWith( "Plugin disabled. Skipping feature file synchronization triggered by: ./test/resources/features/taggedCloud.feature" @@ -677,7 +677,7 @@ describe("the synchronize file hook", () => { file.filePath = "./test/resources/features/invalid.feature"; const { stubbedInfo, stubbedError } = stubLogging(); options.cucumber.uploadFeatures = true; - await synchronizeFile(file, ".", options); + await synchronizeFile(file, ".", options, null, null, null); expect(stubbedError).to.have.been.calledOnce; expect(stubbedError).to.have.been.calledWith( "Feature file invalid, skipping synchronization: Error: Parser errors:\n" + @@ -697,7 +697,7 @@ describe("the synchronize file hook", () => { it("should not try to parse mismatched feature files", async () => { file.filePath = "./test/resources/greetings.txt"; const { stubbedError } = stubLogging(); - await synchronizeFile(file, ".", options); + await synchronizeFile(file, ".", options, null, null, null); expect(stubbedError).to.not.have.been.called; }); }); diff --git a/src/hooks.ts b/src/hooks.ts index a5b2bcfc..03c40293 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -10,12 +10,13 @@ import { ImportExecutionConverterCloud } from "./conversion/importExecution/impo import { ImportExecutionConverterServer } from "./conversion/importExecution/importExecutionConverterServer"; import { ImportExecutionCucumberMultipartConverterCloud } from "./conversion/importExecutionCucumberMultipart/importExecutionCucumberMultipartConverterCloud"; import { ImportExecutionCucumberMultipartConverterServer } from "./conversion/importExecutionCucumberMultipart/importExecutionCucumberMultipartConverterServer"; -import { logError, logInfo, logWarning } from "./logging/logging"; +import { logDebug, logError, logInfo, logWarning } from "./logging/logging"; import { + FeatureFileIssueData, containsCucumberTest, containsNativeTest, + getCucumberIssueData, getNativeTestIssueKeys, - preprocessFeatureFile, } from "./preprocessing/preprocessing"; import { JiraRepositoryCloud } from "./repository/jira/jiraRepositoryCloud"; import { JiraRepositoryServer } from "./repository/jira/jiraRepositoryServer"; @@ -23,7 +24,9 @@ import { IssueTypeDetailsCloud, IssueTypeDetailsServer, } from "./types/jira/responses/issueTypeDetails"; +import { IssueUpdateCloud, IssueUpdateServer } from "./types/jira/responses/issueUpdate"; import { InternalOptions } from "./types/plugin"; +import { StringMap } from "./types/util"; import { XrayTestExecutionResultsCloud, XrayTestExecutionResultsServer, @@ -360,8 +363,10 @@ async function attachVideos( export async function synchronizeFile( file: Cypress.FileObject, projectRoot: string, - options?: InternalOptions, - xrayClient?: XrayClientServer | XrayClientCloud + options: InternalOptions, + xrayClient: XrayClientServer | XrayClientCloud, + jiraClient: JiraClientServer | JiraClientCloud, + jiraRepository: JiraRepositoryServer | JiraRepositoryCloud ): Promise { if (!options) { logError( @@ -388,12 +393,31 @@ export async function synchronizeFile( throw new Error("feature not yet implemented"); } if (options.cucumber.uploadFeatures) { - preprocessFeatureFile( + const issueData = getCucumberIssueData( file.filePath, options, xrayClient instanceof XrayClientCloud ); - await xrayClient.importFeature(file.filePath, options.jira.projectKey); + // Xray currently does not allow keeping the test issues' summaries when importing + // feature files to existing issues. Therefore, we manually need to backup and + // reset the summary once the import is done. + // See: https://docs.getxray.app/display/XRAY/Importing+Cucumber+Tests+-+REST + // See: https://docs.getxray.app/display/XRAYCLOUD/Importing+Cucumber+Tests+-+REST+v2 + const testIssueKeys = issueData.tests.map((data) => data.key); + logDebug( + dedent(` + Creating issue summary backups for issues: + ${testIssueKeys.join("\n")} + `) + ); + const testSummaries = await jiraRepository.getSummaries(...testIssueKeys); + const wasImportSuccessful = await xrayClient.importFeature( + file.filePath, + options.jira.projectKey + ); + if (wasImportSuccessful) { + await resetSummaries(issueData, testSummaries, jiraClient, jiraRepository); + } } } catch (error: unknown) { logError(`Feature file invalid, skipping synchronization: ${error}`); @@ -401,3 +425,47 @@ export async function synchronizeFile( } return file.filePath; } + +async function resetSummaries( + issueData: FeatureFileIssueData, + testSummaries: StringMap, + jiraClient: JiraClientServer | JiraClientCloud, + jiraRepository: JiraRepositoryServer | JiraRepositoryCloud +) { + for (let i = 0; i < issueData.tests.length; i++) { + const issueKey = issueData.tests[i].key; + const oldSummary = testSummaries[issueKey]; + const newSummary = issueData.tests[i].summary; + if (oldSummary !== newSummary) { + const issueUpdate: IssueUpdateServer | IssueUpdateCloud = { + fields: {}, + }; + const summaryFieldId = await jiraRepository.getFieldId("Summary"); + issueUpdate.fields[summaryFieldId] = oldSummary; + logDebug( + dedent(` + Resetting issue summary of issue: ${issueKey} + + Old summary (pre sync): ${oldSummary} + New summary (post sync): ${newSummary} + `) + ); + if (!(await jiraClient.editIssue(issueKey, issueUpdate))) { + logError( + dedent(` + Failed to reset issue summary of issue to its old summary: ${issueKey} + + Old summary (pre sync): ${oldSummary} + New summary (post sync): ${newSummary} + + Make sure to reset it manually if needed + `) + ); + } + } else { + logDebug( + `Issue summary is identical to scenario (outline) name already: ${issueKey} (${oldSummary})` + ); + } + } +} diff --git a/src/https/requests.ts b/src/https/requests.ts index 5592099d..cc530be7 100644 --- a/src/https/requests.ts +++ b/src/https/requests.ts @@ -144,4 +144,15 @@ export class Requests { httpsAgent: Requests.agent(), }); } + + public static async put( + url: string, + data?: D, + config?: RawAxiosRequestConfig + ): Promise { + return Requests.axios().put(url, data, { + ...config, + httpsAgent: Requests.agent(), + }); + } } diff --git a/src/logging/logging.ts b/src/logging/logging.ts index 6d351673..2ae4810c 100644 --- a/src/logging/logging.ts +++ b/src/logging/logging.ts @@ -70,7 +70,7 @@ function log( } // Pad multiline log messages with an extra new line to cleanly separate them from the // following line. - if (index > 1 && index === lines.length - 1) { + if (index > 0 && index === lines.length - 1) { logger(`${prefix} ┊`); } }); diff --git a/src/plugin.ts b/src/plugin.ts index f00cbbd7..f0e376a1 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -58,6 +58,8 @@ export async function syncFeatureFile(file: Cypress.FileObject): Promise file, context.cypress.projectRoot, context.internal, - context.xrayClient + context.xrayClient, + context.jiraClient, + context.jiraRepository ); } diff --git a/src/preprocessing/preprocessing.spec.ts b/src/preprocessing/preprocessing.spec.ts index 0dd7944e..7f537fb5 100644 --- a/src/preprocessing/preprocessing.spec.ts +++ b/src/preprocessing/preprocessing.spec.ts @@ -7,12 +7,12 @@ import { InternalOptions } from "../types/plugin"; import { containsCucumberTest, containsNativeTest, + getCucumberIssueData, getCucumberPreconditionIssueTags, getCucumberScenarioIssueTags, getNativeTestIssueKey, getNativeTestIssueKeys, parseFeatureFile, - preprocessFeatureFile, } from "./preprocessing"; describe("cypress preprocessing", () => { @@ -199,7 +199,7 @@ describe("cucumber preprocessing", () => { describe("server", () => { it("should throw for missing scenario tags", () => { expect(() => - preprocessFeatureFile( + getCucumberIssueData( "./test/resources/features/taggedServerMissingScenario.feature", options, false @@ -222,7 +222,7 @@ describe("cucumber preprocessing", () => { it("should throw for multiple scenario tags", async () => { expect(() => - preprocessFeatureFile( + getCucumberIssueData( "./test/resources/features/taggedServerMultipleScenario.feature", options, false @@ -246,7 +246,7 @@ describe("cucumber preprocessing", () => { it("should throw for missing background tags", async () => { expect(() => - preprocessFeatureFile( + getCucumberIssueData( "./test/resources/features/taggedServerMissingBackground.feature", options, false @@ -269,7 +269,7 @@ describe("cucumber preprocessing", () => { it("should throw for multiple background tags", async () => { expect(() => - preprocessFeatureFile( + getCucumberIssueData( "./test/resources/features/taggedServerMultipleBackground.feature", options, false @@ -320,7 +320,7 @@ describe("cucumber preprocessing", () => { describe("cloud", () => { it("should throw for missing scenario tags", async () => { expect(() => - preprocessFeatureFile( + getCucumberIssueData( "./test/resources/features/taggedCloudMissingScenario.feature", options, true @@ -343,7 +343,7 @@ describe("cucumber preprocessing", () => { it("should throw for multiple scenario tags", async () => { expect(() => - preprocessFeatureFile( + getCucumberIssueData( "./test/resources/features/taggedCloudMultipleScenario.feature", options, true @@ -367,7 +367,7 @@ describe("cucumber preprocessing", () => { it("should throw for missing background tags", async () => { expect(() => - preprocessFeatureFile( + getCucumberIssueData( "./test/resources/features/taggedCloudMissingBackground.feature", options, true @@ -390,7 +390,7 @@ describe("cucumber preprocessing", () => { it("should throw for multiple background tags", async () => { expect(() => - preprocessFeatureFile( + getCucumberIssueData( "./test/resources/features/taggedCloudMultipleBackground.feature", options, true diff --git a/src/preprocessing/preprocessing.ts b/src/preprocessing/preprocessing.ts index e5441a89..89be748c 100644 --- a/src/preprocessing/preprocessing.ts +++ b/src/preprocessing/preprocessing.ts @@ -126,11 +126,23 @@ export function containsCucumberTest( }); } -export function preprocessFeatureFile( +export interface FeatureFileIssueData { + tests: { + key: string; + summary: string; + }[]; + preconditions: string[]; +} + +export function getCucumberIssueData( filePath: string, options: InternalOptions, isCloudClient: boolean -) { +): FeatureFileIssueData { + const featureFileIssueKeys: FeatureFileIssueData = { + tests: [], + preconditions: [], + }; const document = parseFeatureFile(filePath); for (const child of document.feature.children) { if (child.scenario) { @@ -167,6 +179,10 @@ export function preprocessFeatureFile( `) ); } + featureFileIssueKeys.tests.push({ + key: issueKeys[0], + summary: child.scenario.name, + }); } else if (child.background) { const preconditionKeys = getCucumberPreconditionIssueTags( child.background, @@ -207,8 +223,10 @@ export function preprocessFeatureFile( ]; throw new Error(lines.join("\n")); } + featureFileIssueKeys.preconditions.push(preconditionKeys[0]); } } + return featureFileIssueKeys; } function getHelpUrl(isCloudClient: boolean): string { diff --git a/test/util.ts b/test/util.ts index 3e4e0b8f..da0437ca 100644 --- a/test/util.ts +++ b/test/util.ts @@ -94,7 +94,7 @@ export class DummyXrayClient extends XrayClient { } } -export class DummyJiraClient extends JiraClient { +export class DummyJiraClient extends JiraClient { constructor() { super("https://example.org", null); } @@ -110,6 +110,9 @@ export class DummyJiraClient extends JiraClient Date: Sat, 29 Jul 2023 13:39:37 +0200 Subject: [PATCH 17/19] Rework client handling, fix conversion errors --- src/client/jira/jiraClient.spec.ts | 55 + src/client/jira/jiraClient.ts | 4 +- src/client/jira/jiraClientCloud.spec.ts | 5 + src/client/jira/jiraClientServer.spec.ts | 5 + src/client/xray/xrayClient.ts | 43 +- src/client/xray/xrayClientCloud.ts | 37 +- src/client/xray/xrayClientServer.ts | 29 +- src/context.ts | 6 +- src/conversion/converter.ts | 2 +- .../importExecutionConverter.spec.ts | 108 +- .../importExecutionConverter.ts | 4 +- .../importExecutionConverterCloud.spec.ts | 16 +- .../importExecutionConverterServer.spec.ts | 16 +- ...xecutionCucumberMultipartConverter.spec.ts | 34 +- ...portExecutionCucumberMultipartConverter.ts | 8 +- ...ionCucumberMultipartConverterCloud.spec.ts | 4 +- ...xecutionCucumberMultipartConverterCloud.ts | 4 +- ...onCucumberMultipartConverterServer.spec.ts | 15 +- ...ecutionCucumberMultipartConverterServer.ts | 20 +- src/hooks.spec.ts | 968 +++++++----------- src/hooks.ts | 87 +- src/https/requests.ts | 22 +- src/logging/logging.ts | 3 +- src/plugin.ts | 27 +- src/preprocessing/preprocessing.spec.ts | 38 +- src/preprocessing/preprocessing.ts | 45 +- src/types/plugin.ts | 25 +- src/types/util.ts | 20 - .../taggedServerMissingScenario.feature | 2 +- .../taggedServerMultipleBackground.feature | 4 +- .../taggedServerMultipleScenario.feature | 2 +- test/util.ts | 6 +- 32 files changed, 757 insertions(+), 907 deletions(-) diff --git a/src/client/jira/jiraClient.spec.ts b/src/client/jira/jiraClient.spec.ts index b593e32d..f35a3555 100644 --- a/src/client/jira/jiraClient.spec.ts +++ b/src/client/jira/jiraClient.spec.ts @@ -493,6 +493,61 @@ describe("the jira clients", () => { ); }); }); + + describe("editIssue", () => { + it("logs correct messages", async () => { + const { stubbedInfo, stubbedSuccess } = stubLogging(); + const { stubbedPut } = stubRequests(); + stubbedPut.onFirstCall().resolves({ + status: HttpStatusCode.NoContent, + data: null, + headers: null, + statusText: HttpStatusCode[HttpStatusCode.NoContent], + config: null, + }); + await client.editIssue("CYP-123", { + fields: { summary: "Hello" }, + }); + expect(stubbedInfo).to.have.been.calledOnceWithExactly("Editing issue..."); + expect(stubbedSuccess).to.have.been.calledOnceWithExactly( + "Successfully edited issue: CYP-123" + ); + }); + + it("should handle bad responses", async () => { + const { stubbedError } = stubLogging(); + const { stubbedPut } = stubRequests(); + stubbedPut.onFirstCall().rejects( + new AxiosError( + "Request failed with status code 400", + HttpStatusCode.BadRequest.toString(), + undefined, + null, + { + status: HttpStatusCode.BadRequest, + statusText: HttpStatusCode[HttpStatusCode.BadRequest], + config: { headers: new AxiosHeaders() }, + headers: {}, + data: { + errorMessages: ["issue CYP-XYZ does not exist"], + }, + } + ) + ); + const response = await client.editIssue("CYP-XYZ", { + fields: { summary: "Hi" }, + }); + expect(response).to.be.undefined; + expect(stubbedError).to.have.been.calledTwice; + expect(stubbedError).to.have.been.calledWithExactly( + "Failed to edit issue: AxiosError: Request failed with status code 400" + ); + const expectedPath = resolveTestDirPath("editIssue.json"); + expect(stubbedError).to.have.been.calledWithExactly( + `Complete error logs have been written to: ${expectedPath}` + ); + }); + }); }); }); }); diff --git a/src/client/jira/jiraClient.ts b/src/client/jira/jiraClient.ts index dc11b1e7..b27e81f3 100644 --- a/src/client/jira/jiraClient.ts +++ b/src/client/jira/jiraClient.ts @@ -244,7 +244,7 @@ export abstract class JiraClient< return await this.credentials .getAuthenticationHeader() .then(async (header: HTTPHeader) => { - logInfo(`Searching issues...`); + logInfo("Searching issues..."); const progressInterval = this.startResponseInterval(this.apiBaseURL); try { let total = 0; @@ -305,7 +305,7 @@ export abstract class JiraClient< ): Promise { try { await this.credentials.getAuthenticationHeader().then(async (header: HTTPHeader) => { - logInfo(`Editing issue...`); + logInfo("Editing issue..."); const progressInterval = this.startResponseInterval(this.apiBaseURL); try { await Requests.put(this.getUrlEditIssue(issueIdOrKey), issueUpdateData, { diff --git a/src/client/jira/jiraClientCloud.spec.ts b/src/client/jira/jiraClientCloud.spec.ts index f571b0a9..a0fb92bf 100644 --- a/src/client/jira/jiraClientCloud.spec.ts +++ b/src/client/jira/jiraClientCloud.spec.ts @@ -19,5 +19,10 @@ describe("the jira cloud client", () => { it("issue types", () => { expect(client.getUrlGetIssueTypes()).to.eq("https://example.org/rest/api/3/issuetype"); }); + it("edit issue", () => { + expect(client.getUrlEditIssue("CYP-123")).to.eq( + "https://example.org/rest/api/3/issue/CYP-123" + ); + }); }); }); diff --git a/src/client/jira/jiraClientServer.spec.ts b/src/client/jira/jiraClientServer.spec.ts index 996b7831..97eabfb6 100644 --- a/src/client/jira/jiraClientServer.spec.ts +++ b/src/client/jira/jiraClientServer.spec.ts @@ -19,5 +19,10 @@ describe("the jira server client", () => { it("issue types", () => { expect(client.getUrlGetIssueTypes()).to.eq("https://example.org/rest/api/2/issuetype"); }); + it("edit issue", () => { + expect(client.getUrlEditIssue("CYP-123")).to.eq( + "https://example.org/rest/api/2/issue/CYP-123" + ); + }); }); }); diff --git a/src/client/xray/xrayClient.ts b/src/client/xray/xrayClient.ts index b32e1656..45e83eb9 100644 --- a/src/client/xray/xrayClient.ts +++ b/src/client/xray/xrayClient.ts @@ -6,7 +6,7 @@ import { JWTCredentials, PATCredentials, } from "../../authentication/credentials"; -import { Requests } from "../../https/requests"; +import { RequestConfigPost, Requests } from "../../https/requests"; import { logError, logInfo, logSuccess, logWarning, writeErrorFile } from "../../logging/logging"; import { OneOf } from "../../types/util"; import { @@ -218,7 +218,8 @@ export abstract class XrayClient< /** * Uploads Cucumber test results to the Xray instance. * - * @param results the test results as provided by the `cypress-cucumber-preprocessor` + * @param cucumberJson the test results as provided by the `cypress-cucumber-preprocessor` + * @param cucumberInfo the test execution information * @returns the key of the test execution issue, `null` if the upload was skipped or `undefined` * in case of errors * @see https://docs.getxray.app/display/XRAY/Import+Execution+Results+-+REST#ImportExecutionResultsREST-CucumberJSONresultsMultipart @@ -233,30 +234,17 @@ export abstract class XrayClient< logWarning("No Cucumber tests were executed. Skipping Cucumber upload."); return null; } - const formData = new FormData(); - const resultString = JSON.stringify(cucumberJson); - const infoString = JSON.stringify(cucumberInfo); - formData.append("results", resultString, { - filename: "results.json", - }); - formData.append("info", infoString, { - filename: "info.json", - }); - const authenticationHeader = await this.credentials.getAuthenticationHeader( - `${this.apiBaseURL}/authenticate` - ); logInfo("Importing execution (Cucumber)..."); + const request = await this.prepareRequestImportExecutionCucumberMultipart( + cucumberJson, + cucumberInfo + ); const progressInterval = this.startResponseInterval(this.apiBaseURL); try { const response: AxiosResponse = await Requests.post( - this.getUrlImportExecutionCucumberMultipart(), - formData, - { - headers: { - ...authenticationHeader, - ...formData.getHeaders(), - }, - } + request.url, + request.data, + request.config ); const key = this.handleResponseImportExecutionCucumberMultipart(response.data); logSuccess(`Successfully uploaded Cucumber test execution results to ${key}.`); @@ -271,11 +259,16 @@ export abstract class XrayClient< } /** - * Returns the endpoint to use for importing Cucumber multipart execution results. + * Prepares the Cucumber multipart import execution request. * - * @returns the URL + * @param cucumberJson the test results as provided by the `cypress-cucumber-preprocessor` + * @param cucumberInfo the test execution information + * @returns the import execution request */ - public abstract getUrlImportExecutionCucumberMultipart(): string; + public abstract prepareRequestImportExecutionCucumberMultipart( + cucumberJson: CucumberMultipartFeature[], + cucumberInfo: CucumberMultipartInfoType + ): Promise>; /** * Returns the test execution key from the Cucumber multipart import execution response. diff --git a/src/client/xray/xrayClientCloud.ts b/src/client/xray/xrayClientCloud.ts index 9b18c6b0..8b3dd6ec 100644 --- a/src/client/xray/xrayClientCloud.ts +++ b/src/client/xray/xrayClientCloud.ts @@ -1,9 +1,14 @@ import dedent from "dedent"; +import FormData from "form-data"; import { JWTCredentials } from "../../authentication/credentials"; -import { Requests } from "../../https/requests"; +import { RequestConfigPost, Requests } from "../../https/requests"; import { logError, logInfo, logSuccess, logWarning, writeErrorFile } from "../../logging/logging"; import { StringMap } from "../../types/util"; -import { CucumberMultipartInfoCloud } from "../../types/xray/requests/importExecutionCucumberMultipartInfo"; +import { CucumberMultipartFeature } from "../../types/xray/requests/importExecutionCucumberMultipart"; +import { + CucumberMultipartInfoCloud, + CucumberMultipartInfoServer, +} from "../../types/xray/requests/importExecutionCucumberMultipartInfo"; import { GetTestsResponse } from "../../types/xray/responses/graphql/getTests"; import { ImportExecutionResponseCloud } from "../../types/xray/responses/importExecution"; import { ImportFeatureResponseCloud, IssueDetails } from "../../types/xray/responses/importFeature"; @@ -187,8 +192,32 @@ export class XrayClientCloud extends XrayClient< } } - public getUrlImportExecutionCucumberMultipart(): string { - return `${this.apiBaseURL}/import/execution/cucumber/multipart`; + public async prepareRequestImportExecutionCucumberMultipart( + cucumberJson: CucumberMultipartFeature[], + cucumberInfo: CucumberMultipartInfoServer + ): Promise> { + const formData = new FormData(); + const resultString = JSON.stringify(cucumberJson); + const infoString = JSON.stringify(cucumberInfo); + formData.append("results", resultString, { + filename: "results.json", + }); + formData.append("info", infoString, { + filename: "info.json", + }); + const authenticationHeader = await this.credentials.getAuthenticationHeader( + `${this.apiBaseURL}/authenticate` + ); + return { + url: `${this.apiBaseURL}/import/execution/cucumber/multipart`, + data: formData, + config: { + headers: { + ...authenticationHeader, + ...formData.getHeaders(), + }, + }, + }; } public handleResponseImportExecutionCucumberMultipart( diff --git a/src/client/xray/xrayClientServer.ts b/src/client/xray/xrayClientServer.ts index b0bbfe4c..1cdfc757 100644 --- a/src/client/xray/xrayClientServer.ts +++ b/src/client/xray/xrayClientServer.ts @@ -1,5 +1,8 @@ +import FormData from "form-data"; import { BasicAuthCredentials, PATCredentials } from "../../authentication/credentials"; +import { RequestConfigPost } from "../../https/requests"; import { logError, logSuccess } from "../../logging/logging"; +import { CucumberMultipartFeature } from "../../types/xray/requests/importExecutionCucumberMultipart"; import { CucumberMultipartInfoServer } from "../../types/xray/requests/importExecutionCucumberMultipartInfo"; import { ImportExecutionResponseServer } from "../../types/xray/responses/importExecution"; import { @@ -90,8 +93,30 @@ export class XrayClientServer extends XrayClient< } } - public getUrlImportExecutionCucumberMultipart(): string { - return `${this.apiBaseURL}/rest/raven/latest/import/execution/cucumber/multipart`; + public async prepareRequestImportExecutionCucumberMultipart( + cucumberJson: CucumberMultipartFeature[], + cucumberInfo: CucumberMultipartInfoServer + ): Promise> { + const formData = new FormData(); + const resultString = JSON.stringify(cucumberJson); + const infoString = JSON.stringify(cucumberInfo); + formData.append("result", resultString, { + filename: "results.json", + }); + formData.append("info", infoString, { + filename: "info.json", + }); + const authenticationHeader = await this.credentials.getAuthenticationHeader(); + return { + url: `${this.apiBaseURL}/rest/raven/latest/import/execution/cucumber/multipart`, + data: formData, + config: { + headers: { + ...authenticationHeader, + ...formData.getHeaders(), + }, + }, + }; } public handleResponseImportExecutionCucumberMultipart( diff --git a/src/context.ts b/src/context.ts index 546c8f90..cb19f3c3 100644 --- a/src/context.ts +++ b/src/context.ts @@ -40,8 +40,7 @@ import { import { logInfo } from "./logging/logging"; import { JiraRepositoryCloud } from "./repository/jira/jiraRepositoryCloud"; import { JiraRepositoryServer } from "./repository/jira/jiraRepositoryServer"; -import { InternalOptions, Options, XrayStepOptions } from "./types/plugin"; -import { ClientCombination } from "./types/util"; +import { ClientCombination, InternalOptions, Options, XrayStepOptions } from "./types/plugin"; import { asBoolean, asInt, asString, parse } from "./util/parsing"; export function initOptions(env: Cypress.ObjectLike, options: Options): InternalOptions { @@ -198,6 +197,7 @@ export function initClients(options: InternalOptions, env: Cypress.ObjectLike): kind: "cloud", jiraClient: jiraClient, xrayClient: xrayClient, + jiraRepository: new JiraRepositoryCloud(jiraClient, xrayClient, options), }; } else { throw new Error( @@ -225,6 +225,7 @@ export function initClients(options: InternalOptions, env: Cypress.ObjectLike): kind: "server", jiraClient: jiraClient, xrayClient: xrayClient, + jiraRepository: new JiraRepositoryServer(jiraClient, xrayClient, options), }; } else if (ENV_JIRA_USERNAME in env && ENV_JIRA_PASSWORD in env && options.jira.url) { logInfo("Jira username and password found. Setting up Jira server basic auth credentials"); @@ -242,6 +243,7 @@ export function initClients(options: InternalOptions, env: Cypress.ObjectLike): kind: "server", jiraClient: jiraClient, xrayClient: xrayClient, + jiraRepository: new JiraRepositoryServer(jiraClient, xrayClient, options), }; } else { throw new Error( diff --git a/src/conversion/converter.ts b/src/conversion/converter.ts index fbbc24ca..b8b938bd 100644 --- a/src/conversion/converter.ts +++ b/src/conversion/converter.ts @@ -31,5 +31,5 @@ export abstract class Converter< public abstract convert( input: ConversionInputType, parameters?: ConversionParametersType - ): ConversionTargetType; + ): Promise; } diff --git a/src/conversion/importExecution/importExecutionConverter.spec.ts b/src/conversion/importExecution/importExecutionConverter.spec.ts index 24b94f0c..fd12e62a 100644 --- a/src/conversion/importExecution/importExecutionConverter.spec.ts +++ b/src/conversion/importExecution/importExecutionConverter.spec.ts @@ -37,7 +37,7 @@ describe("the import execution converters", () => { testIssueData = { summaries: {}, testTypes: {} }; }); - it("converts test results into xray json", () => { + it("converts test results into xray json", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); @@ -51,16 +51,16 @@ describe("the import execution converters", () => { "CYP-41": "Manual", "CYP-49": "Cucumber", }; - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.tests).to.have.length(3); }); - it("skips tests when encountering unknown statuses", () => { + it("skips tests when encountering unknown statuses", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultUnknownStatus.json", "utf-8") ); const { stubbedWarning } = stubLogging(); - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(stubbedWarning.firstCall).to.have.been.calledWith( dedent(` Skipping result upload for test: TodoMVC hides footer initially @@ -78,7 +78,7 @@ describe("the import execution converters", () => { expect(json.tests).to.be.undefined; }); - it("erases milliseconds from timestamps", () => { + it("erases milliseconds from timestamps", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); @@ -92,12 +92,12 @@ describe("the import execution converters", () => { "CYP-41": "Manual", "CYP-49": "Cucumber", }; - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.info?.startDate).to.eq("2022-11-28T17:41:12Z"); expect(json.info?.finishDate).to.eq("2022-11-28T17:41:19Z"); }); - it("uploads screenshots by default", () => { + it("uploads screenshots by default", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); @@ -111,14 +111,14 @@ describe("the import execution converters", () => { "CYP-41": "Manual", "CYP-49": "Cucumber", }; - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.tests[0].evidence).to.be.undefined; expect(json.tests[1].evidence).to.be.undefined; expect(json.tests[2].evidence).to.be.an("array").with.length(1); expect(json.tests[2].evidence[0].filename).to.eq("turtle.png"); }); - it("skips screenshot upload if disabled", () => { + it("skips screenshot upload if disabled", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); @@ -133,14 +133,14 @@ describe("the import execution converters", () => { "CYP-49": "Cucumber", }; options.xray.uploadScreenshots = false; - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.tests).to.have.length(3); expect(json.tests[0].evidence).to.be.undefined; expect(json.tests[1].evidence).to.be.undefined; expect(json.tests[2].evidence).to.be.undefined; }); - it("normalizes screenshot filenames if enabled", () => { + it("normalizes screenshot filenames if enabled", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultProblematicScreenshot.json", "utf-8") ); @@ -151,13 +151,13 @@ describe("the import execution converters", () => { "CYP-123": "Manual", }; options.plugin.normalizeScreenshotNames = true; - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.tests[0].evidence[0].filename).to.eq( "t_rtle_with_problem_tic_name.png" ); }); - it("does not normalize screenshot filenames by default", () => { + it("does not normalize screenshot filenames by default", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultProblematicScreenshot.json", "utf-8") ); @@ -167,13 +167,13 @@ describe("the import execution converters", () => { testIssueData.testTypes = { "CYP-123": "Generic", }; - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.tests[0].evidence[0].filename).to.eq( "tûrtle with problemätic name.png" ); }); - it("uses custom passed statuses", () => { + it("uses custom passed statuses", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); @@ -188,12 +188,12 @@ describe("the import execution converters", () => { "CYP-49": "Cucumber", }; options.xray.statusPassed = "it worked"; - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.tests[0].status).to.eq("it worked"); expect(json.tests[1].status).to.eq("it worked"); }); - it("uses custom failed statuses", () => { + it("uses custom failed statuses", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); @@ -208,11 +208,11 @@ describe("the import execution converters", () => { "CYP-49": "Cucumber", }; options.xray.statusFailed = "it did not work"; - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.tests[2].status).to.eq("it did not work"); }); - it("uses custom pending statuses", () => { + it("uses custom pending statuses", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultPending.json", "utf-8") ); @@ -229,14 +229,14 @@ describe("the import execution converters", () => { "CYP-987": "No idea", }; options.xray.statusPending = "still pending"; - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.tests[0].status).to.eq("still pending"); expect(json.tests[1].status).to.eq("still pending"); expect(json.tests[2].status).to.eq("still pending"); expect(json.tests[3].status).to.eq("still pending"); }); - it("uses custom skipped statuses", () => { + it("uses custom skipped statuses", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultSkipped.json", "utf-8") ); @@ -249,11 +249,11 @@ describe("the import execution converters", () => { "CYP-456": "Manual", }; options.xray.statusSkipped = "omit"; - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.tests[1].status).to.eq("omit"); }); - it("includes step updates", () => { + it("includes step updates", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); @@ -267,7 +267,7 @@ describe("the import execution converters", () => { "CYP-41": "Manual", "CYP-49": "Cucumber", }; - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.tests).to.have.length(3); expect(json.tests[0].testInfo.steps).to.have.length(1); expect(json.tests[0].testInfo.steps[0].action).to.be.a("string"); @@ -277,7 +277,7 @@ describe("the import execution converters", () => { expect(json.tests[2].testInfo.steps[0].action).to.be.a("string"); }); - it("skips step updates if disabled", () => { + it("skips step updates if disabled", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); @@ -292,14 +292,14 @@ describe("the import execution converters", () => { "CYP-49": "Cucumber", }; options.xray.steps.update = false; - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.tests).to.have.length(3); expect(json.tests[0].testInfo.steps).to.be.undefined; expect(json.tests[1].testInfo.steps).to.be.undefined; expect(json.tests[2].testInfo.steps).to.be.undefined; }); - it("truncates step actions to 8000 characters by default", () => { + it("truncates step actions to 8000 characters by default", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultLongBodies.json", "utf-8") ); @@ -313,13 +313,13 @@ describe("the import execution converters", () => { "CYP-456": "Manual", "CYP-789": "Manual", }; - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.tests[0].testInfo.steps[0].action).to.eq(`${"x".repeat(7997)}...`); expect(json.tests[1].testInfo.steps[0].action).to.eq(`${"x".repeat(8000)}`); expect(json.tests[2].testInfo.steps[0].action).to.eq(`${"x".repeat(2000)}`); }); - it("truncates step actions to custom lengths if enabled", () => { + it("truncates step actions to custom lengths if enabled", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultLongBodies.json", "utf-8") ); @@ -334,13 +334,13 @@ describe("the import execution converters", () => { "CYP-456": "Manual", "CYP-789": "Manual", }; - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.tests[0].testInfo.steps[0].action).to.eq("xx..."); expect(json.tests[1].testInfo.steps[0].action).to.eq("xx..."); expect(json.tests[2].testInfo.steps[0].action).to.eq("xx..."); }); - it("includes issue summaries", () => { + it("includes issue summaries", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); @@ -354,14 +354,14 @@ describe("the import execution converters", () => { "CYP-41": "Manual", "CYP-49": "Cucumber", }; - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.tests).to.have.length(3); expect(json.tests[0].testInfo.summary).to.eq("This is"); expect(json.tests[1].testInfo.summary).to.eq("a distributed"); expect(json.tests[2].testInfo.summary).to.eq("summary"); }); - it("includes test issue keys", () => { + it("includes test issue keys", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); @@ -375,14 +375,14 @@ describe("the import execution converters", () => { "CYP-41": "Manual", "CYP-49": "Cucumber", }; - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.tests).to.have.length(3); expect(json.tests[0].testKey).to.eq("CYP-40"); expect(json.tests[1].testKey).to.eq("CYP-41"); expect(json.tests[2].testKey).to.eq("CYP-49"); }); - it("skips tests with missing test type", () => { + it("skips tests with missing test type", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); @@ -397,7 +397,7 @@ describe("the import execution converters", () => { }; options.xray.steps.update = false; const { stubbedWarning } = stubLogging(); - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.tests).to.be.an("array").with.length(2); expect(stubbedWarning).to.have.been.calledWith( dedent(` @@ -408,7 +408,7 @@ describe("the import execution converters", () => { ); }); - it("skips tests with missing summaries", () => { + it("skips tests with missing summaries", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); @@ -423,7 +423,7 @@ describe("the import execution converters", () => { }; options.xray.steps.update = false; const { stubbedWarning } = stubLogging(); - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.tests).to.be.an("array").with.length(2); expect(stubbedWarning).to.have.been.calledWith( dedent(` @@ -434,7 +434,7 @@ describe("the import execution converters", () => { ); }); - it("adds test execution issue keys", () => { + it("adds test execution issue keys", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); @@ -449,11 +449,11 @@ describe("the import execution converters", () => { "CYP-49": "Cucumber", }; options.jira.testExecutionIssueKey = "CYP-123"; - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.testExecutionKey).to.eq("CYP-123"); }); - it("adds test plan issue keys", () => { + it("adds test plan issue keys", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); @@ -468,11 +468,11 @@ describe("the import execution converters", () => { "CYP-49": "Cucumber", }; options.jira.testPlanIssueKey = "CYP-123"; - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.info.testPlanKey).to.eq("CYP-123"); }); - it("does not add test execution issue keys on its own", () => { + it("does not add test execution issue keys on its own", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); @@ -486,11 +486,11 @@ describe("the import execution converters", () => { "CYP-41": "Manual", "CYP-49": "Cucumber", }; - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.testExecutionKey).to.be.undefined; }); - it("does not add test plan issue keys on its own", () => { + it("does not add test plan issue keys on its own", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); @@ -504,11 +504,11 @@ describe("the import execution converters", () => { "CYP-41": "Manual", "CYP-49": "Cucumber", }; - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.info.testPlanKey).to.be.undefined; }); - it("includes a custom test execution summary if provided", () => { + it("includes a custom test execution summary if provided", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); @@ -523,11 +523,11 @@ describe("the import execution converters", () => { "CYP-49": "Cucumber", }; options.jira.testExecutionIssueSummary = "Jeffrey's Test"; - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.info.summary).to.eq("Jeffrey's Test"); }); - it("uses a timestamp as test execution summary by default", () => { + it("uses a timestamp as test execution summary by default", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); @@ -541,11 +541,11 @@ describe("the import execution converters", () => { "CYP-41": "Manual", "CYP-49": "Cucumber", }; - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.info.summary).to.eq("Execution Results [1669657272234]"); }); - it("includes a custom test execution description if provided", () => { + it("includes a custom test execution description if provided", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); @@ -560,11 +560,11 @@ describe("the import execution converters", () => { "CYP-49": "Cucumber", }; options.jira.testExecutionIssueDescription = "Very Useful Text"; - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.info.description).to.eq("Very Useful Text"); }); - it("uses versions as test execution description by default", () => { + it("uses versions as test execution description by default", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); @@ -578,7 +578,7 @@ describe("the import execution converters", () => { "CYP-41": "Manual", "CYP-49": "Cucumber", }; - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.info.description).to.eq( "Cypress version: 11.1.0 Browser: electron (106.0.5249.51)" ); diff --git a/src/conversion/importExecution/importExecutionConverter.ts b/src/conversion/importExecution/importExecutionConverter.ts index d285ea4e..28772280 100644 --- a/src/conversion/importExecution/importExecutionConverter.ts +++ b/src/conversion/importExecution/importExecutionConverter.ts @@ -34,10 +34,10 @@ export abstract class ImportExecutionConverter< XrayTestExecutionResultsType, TestIssueData > { - public convert( + public async convert( results: CypressCommandLine.CypressRunResult, issueData: TestIssueData - ): XrayTestExecutionResultsType { + ): Promise { const runs: CypressCommandLine.RunResult[] = results.runs.filter( (run: CypressCommandLine.RunResult) => { return !run.spec.absolute.endsWith(this.options.cucumber.featureFileExtension); diff --git a/src/conversion/importExecution/importExecutionConverterCloud.spec.ts b/src/conversion/importExecution/importExecutionConverterCloud.spec.ts index 454b850e..27cbd678 100644 --- a/src/conversion/importExecution/importExecutionConverterCloud.spec.ts +++ b/src/conversion/importExecution/importExecutionConverterCloud.spec.ts @@ -27,7 +27,7 @@ describe("the import execution results converter (cloud)", () => { testIssueData = { summaries: {}, testTypes: {} }; }); - it("uses PASSED as default status name for passed tests", () => { + it("uses PASSED as default status name for passed tests", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); @@ -42,12 +42,12 @@ describe("the import execution results converter (cloud)", () => { "CYP-49": "Cucumber", }; const converter = new ImportExecutionConverterCloud(options); - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.tests[0].status).to.eq("PASSED"); expect(json.tests[1].status).to.eq("PASSED"); }); - it("uses FAILED as default status name for failed tests", () => { + it("uses FAILED as default status name for failed tests", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); @@ -62,11 +62,11 @@ describe("the import execution results converter (cloud)", () => { "CYP-49": "Cucumber", }; const converter = new ImportExecutionConverterCloud(options); - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.tests[2].status).to.eq("FAILED"); }); - it("uses TODO as default status name for pending tests", () => { + it("uses TODO as default status name for pending tests", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultPending.json", "utf-8") ); @@ -83,14 +83,14 @@ describe("the import execution results converter (cloud)", () => { "CYP-987": "No idea", }; const converter = new ImportExecutionConverterCloud(options); - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.tests[0].status).to.eq("TODO"); expect(json.tests[1].status).to.eq("TODO"); expect(json.tests[2].status).to.eq("TODO"); expect(json.tests[3].status).to.eq("TODO"); }); - it("uses FAILED as default status name for skipped tests", () => { + it("uses FAILED as default status name for skipped tests", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultSkipped.json", "utf-8") ); @@ -103,7 +103,7 @@ describe("the import execution results converter (cloud)", () => { "CYP-456": "Cucumber", }; const converter = new ImportExecutionConverterCloud(options); - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.tests[0].status).to.eq("FAILED"); expect(json.tests[1].status).to.eq("FAILED"); }); diff --git a/src/conversion/importExecution/importExecutionConverterServer.spec.ts b/src/conversion/importExecution/importExecutionConverterServer.spec.ts index 2b0674cc..7009d721 100644 --- a/src/conversion/importExecution/importExecutionConverterServer.spec.ts +++ b/src/conversion/importExecution/importExecutionConverterServer.spec.ts @@ -26,7 +26,7 @@ describe("the import execution results converter (server)", () => { testIssueData = { summaries: {}, testTypes: {} }; }); - it("uses PASS as default status name for passed tests", () => { + it("uses PASS as default status name for passed tests", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); @@ -41,12 +41,12 @@ describe("the import execution results converter (server)", () => { "CYP-49": "Cucumber", }; const converter = new ImportExecutionConverterServer(options); - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.tests[0].status).to.eq("PASS"); expect(json.tests[1].status).to.eq("PASS"); }); - it("uses FAIL as default status name for failed tests", () => { + it("uses FAIL as default status name for failed tests", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultExistingTestIssues.json", "utf-8") ); @@ -61,11 +61,11 @@ describe("the import execution results converter (server)", () => { "CYP-49": "Cucumber", }; const converter = new ImportExecutionConverterServer(options); - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.tests[2].status).to.eq("FAIL"); }); - it("uses TODO as default status name for pending tests", () => { + it("uses TODO as default status name for pending tests", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultPending.json", "utf-8") ); @@ -82,14 +82,14 @@ describe("the import execution results converter (server)", () => { "CYP-987": "No idea", }; const converter = new ImportExecutionConverterServer(options); - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.tests[0].status).to.eq("TODO"); expect(json.tests[1].status).to.eq("TODO"); expect(json.tests[2].status).to.eq("TODO"); expect(json.tests[3].status).to.eq("TODO"); }); - it("uses FAIL as default status name for skipped tests", () => { + it("uses FAIL as default status name for skipped tests", async () => { const result: CypressCommandLine.CypressRunResult = JSON.parse( readFileSync("./test/resources/runResultSkipped.json", "utf-8") ); @@ -102,7 +102,7 @@ describe("the import execution results converter (server)", () => { "CYP-456": "Cucumber", }; const converter = new ImportExecutionConverterServer(options); - const json = converter.convert(result, testIssueData); + const json = await converter.convert(result, testIssueData); expect(json.tests[0].status).to.eq("FAIL"); expect(json.tests[1].status).to.eq("FAIL"); }); diff --git a/src/conversion/importExecutionCucumberMultipart/importExecutionCucumberMultipartConverter.spec.ts b/src/conversion/importExecutionCucumberMultipart/importExecutionCucumberMultipartConverter.spec.ts index 954c2bcd..50792dc1 100644 --- a/src/conversion/importExecutionCucumberMultipart/importExecutionCucumberMultipartConverter.spec.ts +++ b/src/conversion/importExecutionCucumberMultipart/importExecutionCucumberMultipartConverter.spec.ts @@ -39,7 +39,7 @@ describe("the import execution cucumber multipart converters", () => { ); converter = converterType === "server" - ? new ImportExecutionCucumberMultipartConverterServer(options) + ? new ImportExecutionCucumberMultipartConverterServer(options, null) : new ImportExecutionCucumberMultipartConverterCloud(options); result = converterType === "server" @@ -57,53 +57,53 @@ describe("the import execution cucumber multipart converters", () => { ); }); - it("should include all tagged features and tests", () => { - const multipart = converter.convert(result, parameters); + it("should include all tagged features and tests", async () => { + const multipart = await converter.convert(result, parameters); expect(multipart.features).to.be.an("array").with.length(2); expect(multipart.features[0].elements).to.be.an("array").with.length(3); expect(multipart.features[1].elements).to.be.an("array").with.length(1); }); - it("should use the configured project key", () => { - const multipart = converter.convert([result[0]], parameters); + it("should use the configured project key", async () => { + const multipart = await converter.convert([result[0]], parameters); expect(multipart.info.fields.project).to.deep.eq({ key: "CYP", }); }); - it("should use the configured test execution summary", () => { + it("should use the configured test execution summary", async () => { options.jira.testExecutionIssueSummary = "A summary"; - const multipart = converter.convert([result[0]], parameters); + const multipart = await converter.convert([result[0]], parameters); expect(multipart.info.fields.summary).to.eq("A summary"); }); - it("should use the configured test execution issue description", () => { + it("should use the configured test execution issue description", async () => { options.jira.testExecutionIssueDescription = "This is a nice description"; - const multipart = converter.convert([result[0]], parameters); + const multipart = await converter.convert([result[0]], parameters); expect(multipart.info.fields.description).to.eq("This is a nice description"); }); - it("should use the configured test execution issue key", () => { + it("should use the configured test execution issue key", async () => { options.jira.testExecutionIssueKey = "CYP-456"; - const multipart = converter.convert([result[0], result[0]], parameters); + const multipart = await converter.convert([result[0], result[0]], parameters); expect(multipart.features[0].tags[0]).to.deep.eq({ name: "@CYP-456" }); expect(multipart.features[1].tags[0]).to.deep.eq({ name: "@CYP-456" }); }); - it("should use the configured test execution issue details", () => { + it("should use the configured test execution issue details", async () => { options.jira.testExecutionIssueDetails = { name: options.jira.testExecutionIssueType, subtask: false, }; - const multipart = converter.convert([result[0]], parameters); + const multipart = await converter.convert([result[0]], parameters); expect(multipart.info.fields.issuetype).to.deep.eq({ name: "Test Execution", subtask: false, }); }); - it("should include screenshots by default", () => { - const multipart = converter.convert([result[0]], parameters); + it("should include screenshots by default", async () => { + const multipart = await converter.convert([result[0]], parameters); expect(multipart.features[0].elements[2].steps[1].embeddings).to.have.length(1); expect(multipart.features[0].elements[2].steps[1].embeddings[0].data).to.be.a( "string" @@ -113,9 +113,9 @@ describe("the import execution cucumber multipart converters", () => { ); }); - it("should skip embeddings if screenshots are disabled", () => { + it("should skip embeddings if screenshots are disabled", async () => { options.xray.uploadScreenshots = false; - const multipart = converter.convert([result[0]], parameters); + const multipart = await converter.convert([result[0]], parameters); expect(multipart.features[0].elements[0].steps[0].embeddings).to.be.empty; expect(multipart.features[0].elements[0].steps[1].embeddings).to.be.empty; expect(multipart.features[0].elements[1].steps[0].embeddings).to.be.empty; diff --git a/src/conversion/importExecutionCucumberMultipart/importExecutionCucumberMultipartConverter.ts b/src/conversion/importExecutionCucumberMultipart/importExecutionCucumberMultipartConverter.ts index 413be377..3d24bd45 100644 --- a/src/conversion/importExecutionCucumberMultipart/importExecutionCucumberMultipartConverter.ts +++ b/src/conversion/importExecutionCucumberMultipart/importExecutionCucumberMultipartConverter.ts @@ -26,10 +26,10 @@ export abstract class ImportExecutionCucumberMultipartConverter< CucumberMultipart, ConversionParameters > { - public convert( + public async convert( input: CucumberMultipartFeature[], parameters: ConversionParameters - ): CucumberMultipart { + ): Promise> { const tests: CucumberMultipartFeature[] = []; input.forEach((result: CucumberMultipartFeature) => { const test: CucumberMultipartFeature = { @@ -66,7 +66,7 @@ export abstract class ImportExecutionCucumberMultipartConverter< tests.push(test); } }); - const info: CucumberMultipartInfoType = this.getMultipartInfo(parameters); + const info: CucumberMultipartInfoType = await this.getMultipartInfo(parameters); return { features: tests, info: info, @@ -81,7 +81,7 @@ export abstract class ImportExecutionCucumberMultipartConverter< */ protected abstract getMultipartInfo( parameters: ConversionParameters - ): CucumberMultipartInfoType; + ): Promise; private getSteps(element: CucumberMultipartElement): CucumberMultipartStep[] { const steps: CucumberMultipartStep[] = []; diff --git a/src/conversion/importExecutionCucumberMultipart/importExecutionCucumberMultipartConverterCloud.spec.ts b/src/conversion/importExecutionCucumberMultipart/importExecutionCucumberMultipartConverterCloud.spec.ts index f1459b29..1de6d8ac 100644 --- a/src/conversion/importExecutionCucumberMultipart/importExecutionCucumberMultipartConverterCloud.spec.ts +++ b/src/conversion/importExecutionCucumberMultipart/importExecutionCucumberMultipartConverterCloud.spec.ts @@ -34,7 +34,7 @@ describe("the import execution cucumber multipart cloud converter", () => { converter = new ImportExecutionCucumberMultipartConverterCloud(options); }); - it("should add test plan issue keys", () => { + it("should add test plan issue keys", async () => { const result: CucumberMultipartFeature[] = JSON.parse( readFileSync( "./test/resources/fixtures/xray/requests/importExecutionCucumberMultipartCloud.json", @@ -42,7 +42,7 @@ describe("the import execution cucumber multipart cloud converter", () => { ) ); options.jira.testPlanIssueKey = "CYP-123"; - const multipart = converter.convert([result[0]], parameters); + const multipart = await converter.convert([result[0]], parameters); expect(multipart.info.xrayFields.testPlanKey).to.eq("CYP-123"); }); }); diff --git a/src/conversion/importExecutionCucumberMultipart/importExecutionCucumberMultipartConverterCloud.ts b/src/conversion/importExecutionCucumberMultipart/importExecutionCucumberMultipartConverterCloud.ts index e2b809a7..460d8e71 100644 --- a/src/conversion/importExecutionCucumberMultipart/importExecutionCucumberMultipartConverterCloud.ts +++ b/src/conversion/importExecutionCucumberMultipart/importExecutionCucumberMultipartConverterCloud.ts @@ -5,7 +5,9 @@ import { } from "./importExecutionCucumberMultipartConverter"; export class ImportExecutionCucumberMultipartConverterCloud extends ImportExecutionCucumberMultipartConverter { - protected getMultipartInfo(parameters: ConversionParameters): CucumberMultipartInfoCloud { + protected async getMultipartInfo( + parameters: ConversionParameters + ): Promise { const summary = this.options.jira.testExecutionIssueSummary || `Execution Results [${new Date(parameters.startedTestsAt).getTime()}]`; diff --git a/src/conversion/importExecutionCucumberMultipart/importExecutionCucumberMultipartConverterServer.spec.ts b/src/conversion/importExecutionCucumberMultipart/importExecutionCucumberMultipartConverterServer.spec.ts index cc8417ad..273fa0fa 100644 --- a/src/conversion/importExecutionCucumberMultipart/importExecutionCucumberMultipartConverterServer.spec.ts +++ b/src/conversion/importExecutionCucumberMultipart/importExecutionCucumberMultipartConverterServer.spec.ts @@ -2,7 +2,9 @@ import { expect } from "chai"; import { readFileSync } from "fs"; +import { stub } from "sinon"; import { initOptions } from "../../context"; +import { JiraRepositoryServer } from "../../repository/jira/jiraRepositoryServer"; import { InternalOptions } from "../../types/plugin"; import { CucumberMultipartFeature } from "../../types/xray/requests/importExecutionCucumberMultipart"; import { ConversionParameters } from "./importExecutionCucumberMultipartConverter"; @@ -11,6 +13,7 @@ import { ImportExecutionCucumberMultipartConverterServer } from "./importExecuti describe("the import execution cucumber multipart server converter", () => { let options: InternalOptions; let converter: ImportExecutionCucumberMultipartConverterServer; + let jiraRepository: JiraRepositoryServer; const result: CucumberMultipartFeature[] = JSON.parse( readFileSync( "./test/resources/fixtures/xray/requests/importExecutionCucumberMultipartServer.json", @@ -36,16 +39,14 @@ describe("the import execution cucumber multipart server converter", () => { }, } ); - converter = new ImportExecutionCucumberMultipartConverterServer(options); + jiraRepository = new JiraRepositoryServer(null, null, options); + converter = new ImportExecutionCucumberMultipartConverterServer(options, jiraRepository); }); - it("should be able to add test plan issue keys", () => { + it("should be able to add test plan issue keys", async () => { + stub(jiraRepository, "getFieldId").withArgs("Test Plan").resolves("customfield_12126"); options.jira.testPlanIssueKey = "CYP-123"; - options.jira.testPlanIssueDetails = { - id: "customfield_12126", - subtask: false, - }; - const multipart = converter.convert([result[0]], parameters); + const multipart = await converter.convert([result[0]], parameters); expect(multipart.info.fields["customfield_12126"]).to.deep.eq(["CYP-123"]); }); }); diff --git a/src/conversion/importExecutionCucumberMultipart/importExecutionCucumberMultipartConverterServer.ts b/src/conversion/importExecutionCucumberMultipart/importExecutionCucumberMultipartConverterServer.ts index 81166560..3e675036 100644 --- a/src/conversion/importExecutionCucumberMultipart/importExecutionCucumberMultipartConverterServer.ts +++ b/src/conversion/importExecutionCucumberMultipart/importExecutionCucumberMultipartConverterServer.ts @@ -1,3 +1,5 @@ +import { JiraRepositoryServer } from "../../repository/jira/jiraRepositoryServer"; +import { InternalOptions } from "../../types/plugin"; import { CucumberMultipartInfoServer } from "../../types/xray/requests/importExecutionCucumberMultipartInfo"; import { ConversionParameters, @@ -5,7 +7,16 @@ import { } from "./importExecutionCucumberMultipartConverter"; export class ImportExecutionCucumberMultipartConverterServer extends ImportExecutionCucumberMultipartConverter { - protected getMultipartInfo(parameters: ConversionParameters): CucumberMultipartInfoServer { + private readonly jiraRepository: JiraRepositoryServer; + + constructor(options: InternalOptions, jiraRepository: JiraRepositoryServer) { + super(options); + this.jiraRepository = jiraRepository; + } + + protected async getMultipartInfo( + parameters: ConversionParameters + ): Promise { const summary = this.options.jira.testExecutionIssueSummary || `Execution Results [${new Date(parameters.startedTestsAt).getTime()}]`; @@ -29,9 +40,10 @@ export class ImportExecutionCucumberMultipartConverterServer extends ImportExecu }, }; if (this.options.jira.testPlanIssueKey) { - info.fields[this.options.jira.testPlanIssueDetails.id] = [ - this.options.jira.testPlanIssueKey, - ]; + const testPlanFieldId = await this.jiraRepository.getFieldId( + this.options.jira.testPlanIssueType + ); + info.fields[testPlanFieldId] = [this.options.jira.testPlanIssueKey]; } return info; } diff --git a/src/hooks.spec.ts b/src/hooks.spec.ts index 5a6e2f65..a919f58d 100644 --- a/src/hooks.spec.ts +++ b/src/hooks.spec.ts @@ -1,27 +1,24 @@ -import { AxiosError, AxiosHeaders, HttpStatusCode } from "axios"; import chai, { expect } from "chai"; import chaiAsPromised from "chai-as-promised"; import dedent from "dedent"; import { readFileSync } from "fs"; import path from "path"; -import { DummyJiraClient, DummyXrayClient, stubLogging, stubRequests } from "../test/util"; -import { BasicAuthCredentials } from "./authentication/credentials"; -import { JiraClientCloud } from "./client/jira/jiraClientCloud"; +import { stub } from "sinon"; +import { stubLogging } from "../test/util"; +import { JiraClientServer } from "./client/jira/jiraClientServer"; +import { XrayClientServer } from "./client/xray/xrayClientServer"; import { initOptions } from "./context"; import { afterRunHook, beforeRunHook, synchronizeFile } from "./hooks"; -import { InternalOptions } from "./types/plugin"; +import { ClientCombination, InternalOptions } from "./types/plugin"; // Enable promise assertions. chai.use(chaiAsPromised); -describe("the before run hook", () => { - let beforeRunDetails: Cypress.BeforeRunDetails; - let config: Cypress.PluginConfigOptions; +describe("the hooks", () => { let options: InternalOptions; + let clients: ClientCombination; beforeEach(() => { - beforeRunDetails = JSON.parse(readFileSync("./test/resources/beforeRun.json", "utf-8")); - config = JSON.parse(readFileSync("./test/resources/cypress.config.json", "utf-8")); options = initOptions( {}, { @@ -34,670 +31,401 @@ describe("the before run hook", () => { }, } ); - config.env["jsonEnabled"] = true; - config.env["jsonOutput"] = "logs"; + clients = { + kind: "server", + jiraClient: new JiraClientServer("https://example.org", null), + xrayClient: new XrayClientServer("https://example.org", null, null), + jiraRepository: null, + }; }); - it("should throw if the plugin was not configured", async () => { - const { stubbedError } = stubLogging(); - await beforeRunHook(beforeRunDetails, config); - expect(stubbedError).to.have.been.calledOnceWith( - dedent(` - Plugin misconfigured: configureXrayPlugin() was not called. Skipping before:run hook + describe("beforeRun", () => { + let beforeRunDetails: Cypress.BeforeRunDetails; + let config: Cypress.PluginConfigOptions; - Make sure your project is set up correctly: https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/introduction/ - `) - ); - }); + beforeEach(() => { + beforeRunDetails = JSON.parse(readFileSync("./test/resources/beforeRun.json", "utf-8")); + config = JSON.parse(readFileSync("./test/resources/cypress.config.json", "utf-8")); + config.env["jsonEnabled"] = true; + config.env["jsonOutput"] = "logs"; + }); - it("should not do anything if disabled", async () => { - const { stubbedInfo } = stubLogging(); - options.plugin.enabled = false; - await beforeRunHook(beforeRunDetails, config, options); - expect(stubbedInfo).to.have.been.calledOnceWith( - "Plugin disabled. Skipping before:run hook" - ); - }); + it("should throw if the plugin was not configured", async () => { + const { stubbedError } = stubLogging(); + await beforeRunHook(beforeRunDetails, config); + expect(stubbedError).to.have.been.calledOnceWith( + dedent(` + Plugin misconfigured: configureXrayPlugin() was not called. Skipping before:run hook - it("should throw if the xray client was not configured", async () => { - await expect( - beforeRunHook(beforeRunDetails, config, options) - ).to.eventually.be.rejectedWith( - dedent(` - Plugin misconfigured: Xray client was not configured + Make sure your project is set up correctly: https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/introduction/ + `) + ); + }); - Make sure your project is set up correctly: https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/introduction/ - `) - ); - }); + it("should not do anything if disabled", async () => { + const { stubbedInfo } = stubLogging(); + options.plugin.enabled = false; + await beforeRunHook(beforeRunDetails, config, options); + expect(stubbedInfo).to.have.been.calledOnceWith( + "Plugin disabled. Skipping before:run hook" + ); + }); - it("should throw if the jira client was not configured", async () => { - await expect( - beforeRunHook(beforeRunDetails, config, options, new DummyXrayClient()) - ).to.eventually.be.rejectedWith( - dedent(` - Plugin misconfigured: Jira client was not configured + it("should throw if the xray client was not configured", async () => { + clients.xrayClient = undefined; + await expect( + beforeRunHook(beforeRunDetails, config, options, clients) + ).to.eventually.be.rejectedWith( + dedent(` + Plugin misconfigured: Xray client was not configured + + Make sure your project is set up correctly: https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/introduction/ + `) + ); + }); - Make sure your project is set up correctly: https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/introduction/ - `) - ); - }); + it("should throw if the jira client was not configured", async () => { + clients.jiraClient = undefined; + await expect( + beforeRunHook(beforeRunDetails, config, options, clients) + ).to.eventually.be.rejectedWith( + dedent(` + Plugin misconfigured: Jira client was not configured + + Make sure your project is set up correctly: https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/introduction/ + `) + ); + }); - it("should ignore the run details if the results upload is disabled", async () => { - beforeRunDetails = JSON.parse( - readFileSync("./test/resources/beforeRunMixed.json", "utf-8") - ); - options.xray.uploadResults = false; - await beforeRunHook( - beforeRunDetails, - config, - options, - new DummyXrayClient(), - new DummyJiraClient() - ); - expect(options.cucumber.preprocessor).to.be.undefined; - }); + it("should ignore the run details if the results upload is disabled", async () => { + beforeRunDetails = JSON.parse( + readFileSync("./test/resources/beforeRunMixed.json", "utf-8") + ); + options.xray.uploadResults = false; + await beforeRunHook(beforeRunDetails, config, options, clients); + expect(options.cucumber.preprocessor).to.be.undefined; + }); - it("should throw if the cucumber preprocessor json report is not enabled", async () => { - beforeRunDetails = JSON.parse( - readFileSync("./test/resources/beforeRunMixed.json", "utf-8") - ); - config.env["jsonEnabled"] = false; - await expect( - beforeRunHook( - beforeRunDetails, - config, - options, - new DummyXrayClient(), - new DummyJiraClient() - ) - ).to.eventually.be.rejectedWith( - dedent(` - Plugin misconfigured: Cucumber preprocessor JSON report disabled - - Make sure to enable the JSON report as described in https://github.com/badeball/cypress-cucumber-preprocessor/blob/master/docs/json-report.md - `) - ); - }); + it("should throw if the cucumber preprocessor json report is not enabled", async () => { + beforeRunDetails = JSON.parse( + readFileSync("./test/resources/beforeRunMixed.json", "utf-8") + ); + config.env["jsonEnabled"] = false; + await expect( + beforeRunHook(beforeRunDetails, config, options, clients) + ).to.eventually.be.rejectedWith( + dedent(` + Plugin misconfigured: Cucumber preprocessor JSON report disabled + + Make sure to enable the JSON report as described in https://github.com/badeball/cypress-cucumber-preprocessor/blob/master/docs/json-report.md + `) + ); + }); - it("should throw if the cucumber preprocessor json report path was not set", async () => { - beforeRunDetails = JSON.parse( - readFileSync("./test/resources/beforeRunMixed.json", "utf-8") - ); - config.env["jsonOutput"] = ""; - await expect( - beforeRunHook( - beforeRunDetails, - config, - options, - new DummyXrayClient(), - new DummyJiraClient() - ) - ).to.eventually.be.rejectedWith( - dedent(` - Plugin misconfigured: Cucumber preprocessor JSON report path was not set - - Make sure to configure the JSON report path as described in https://github.com/badeball/cypress-cucumber-preprocessor/blob/master/docs/json-report.md - `) - ); - }); + it("should throw if the cucumber preprocessor json report path was not set", async () => { + beforeRunDetails = JSON.parse( + readFileSync("./test/resources/beforeRunMixed.json", "utf-8") + ); + config.env["jsonOutput"] = ""; + await expect( + beforeRunHook(beforeRunDetails, config, options, clients) + ).to.eventually.be.rejectedWith( + dedent(` + Plugin misconfigured: Cucumber preprocessor JSON report path was not set + + Make sure to configure the JSON report path as described in https://github.com/badeball/cypress-cucumber-preprocessor/blob/master/docs/json-report.md + `) + ); + }); - it("should fetch xray issue type information to prepare for cucumber results upload", async () => { - const { stubbedInfo } = stubLogging(); - const { stubbedGet } = stubRequests(); - beforeRunDetails = JSON.parse( - readFileSync("./test/resources/beforeRunMixed.json", "utf-8") - ); - options.jira.testPlanIssueKey = "CYP-456"; - stubbedGet.onFirstCall().resolves({ - status: HttpStatusCode.Ok, - data: [ + it("should fetch xray issue type information to prepare for cucumber results upload", async () => { + const { stubbedInfo } = stubLogging(); + beforeRunDetails = JSON.parse( + readFileSync("./test/resources/beforeRunMixed.json", "utf-8") + ); + options.jira.testPlanIssueKey = "CYP-456"; + stub(clients.jiraClient, "getIssueTypes").resolves([ { name: "Test Execution", - id: 12345, - }, - { - name: "Test Plan", - id: 67890, + id: "12345", + subtask: false, }, - ], - headers: null, - statusText: HttpStatusCode[HttpStatusCode.Ok], - config: null, - }); - await beforeRunHook( - beforeRunDetails, - config, - options, - new DummyXrayClient(), - new JiraClientCloud("https://example.org", new BasicAuthCredentials("user", "token")) - ); - expect(stubbedInfo).to.have.been.calledWith( - "Fetching necessary Jira issue type information in preparation for Cucumber result uploads..." - ); - expect(options.jira.testExecutionIssueDetails).to.deep.eq({ - name: "Test Execution", - id: 12345, - }); - expect(options.jira.testPlanIssueDetails).to.deep.eq({ - name: "Test Plan", - id: 67890, + ]); + await beforeRunHook(beforeRunDetails, config, options, clients); + expect(stubbedInfo).to.have.been.calledWith( + "Fetching necessary Jira issue type information in preparation for Cucumber result uploads..." + ); + expect(options.jira.testExecutionIssueDetails).to.deep.eq({ + name: "Test Execution", + id: "12345", + subtask: false, + }); }); - }); - it("should not fetch xray issue type information for native results upload", async () => { - const { stubbedInfo } = stubLogging(); - beforeRunDetails = JSON.parse(readFileSync("./test/resources/beforeRun.json", "utf-8")); - await beforeRunHook( - beforeRunDetails, - config, - options, - new DummyXrayClient(), - new DummyJiraClient() - ); - expect(stubbedInfo).to.not.have.been.called; - }); + it("should not fetch xray issue type information for native results upload", async () => { + const { stubbedInfo } = stubLogging(); + beforeRunDetails = JSON.parse(readFileSync("./test/resources/beforeRun.json", "utf-8")); + await beforeRunHook(beforeRunDetails, config, options, clients); + expect(stubbedInfo).to.not.have.been.called; + }); - it("should throw if xray test execution issue type information can not be fetched", async () => { - stubLogging(); - const { stubbedGet } = stubRequests(); - beforeRunDetails = JSON.parse( - readFileSync("./test/resources/beforeRunMixed.json", "utf-8") - ); - options.jira.testExecutionIssueType = "Execution Issue"; - stubbedGet.onFirstCall().resolves({ - status: HttpStatusCode.Ok, - data: [ + it("should throw if xray test execution issue type information can not be fetched", async () => { + stubLogging(); + beforeRunDetails = JSON.parse( + readFileSync("./test/resources/beforeRunMixed.json", "utf-8") + ); + options.jira.testExecutionIssueType = "Execution Issue"; + stub(clients.jiraClient, "getIssueTypes").resolves([ { name: "Bug", - id: 67890, + id: "67890", + subtask: false, }, - ], - headers: null, - statusText: HttpStatusCode[HttpStatusCode.Ok], - config: null, + ]); + await expect( + beforeRunHook(beforeRunDetails, config, options, clients) + ).to.eventually.be.rejectedWith( + dedent(` + Failed to retrieve issue type information for issue type: Execution Issue + + Make sure you have Xray installed. + + For more information, visit: + - https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/jira/#testExecutionIssueType + - https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/jira/#testPlanIssueType + `) + ); }); - await expect( - beforeRunHook( - beforeRunDetails, - config, - options, - new DummyXrayClient(), - new JiraClientCloud( - "https://example.org", - new BasicAuthCredentials("user", "token") - ) - ) - ).to.eventually.be.rejectedWith( - dedent(` - Failed to retrieve issue type information for issue type: Execution Issue - - Make sure you have Xray installed. - - For more information, visit: - - https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/jira/#testExecutionIssueType - - https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/jira/#testPlanIssueType - `) - ); - }); - it("should throw if multiple xray test execution issue types are fetched", async () => { - stubLogging(); - const { stubbedGet } = stubRequests(); - beforeRunDetails = JSON.parse( - readFileSync("./test/resources/beforeRunMixed.json", "utf-8") - ); - options.jira.testExecutionIssueType = "Execution Issue"; - stubbedGet.onFirstCall().resolves({ - status: HttpStatusCode.Ok, - data: [ + it("should throw if multiple xray test execution issue types are fetched", async () => { + stubLogging(); + beforeRunDetails = JSON.parse( + readFileSync("./test/resources/beforeRunMixed.json", "utf-8") + ); + options.jira.testExecutionIssueType = "Execution Issue"; + stub(clients.jiraClient, "getIssueTypes").resolves([ { name: "Execution Issue", - id: 12345, + id: "12345", + subtask: false, }, { name: "Execution Issue", - id: 67890, + id: "67890", + subtask: false, }, - ], - headers: null, - statusText: HttpStatusCode[HttpStatusCode.Ok], - config: null, + ]); + await expect( + beforeRunHook(beforeRunDetails, config, options, clients) + ).to.eventually.be.rejectedWith( + dedent(` + Found multiple issue types named: Execution Issue + + Make sure to only make a single one available in project CYP. + + For more information, visit: + - https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/jira/#testExecutionIssueType + - https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/jira/#testPlanIssueType + `) + ); }); - await expect( - beforeRunHook( - beforeRunDetails, - config, - options, - new DummyXrayClient(), - new JiraClientCloud( - "https://example.org", - new BasicAuthCredentials("user", "token") - ) - ) - ).to.eventually.be.rejectedWith( - dedent(` - Found multiple issue types named: Execution Issue - - Make sure to only make a single one available in project CYP. - - For more information, visit: - - https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/jira/#testExecutionIssueType - - https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/jira/#testPlanIssueType - `) - ); - }); - it("should throw if multiple xray test plan issue types are fetched", async () => { - stubLogging(); - const { stubbedGet } = stubRequests(); - beforeRunDetails = JSON.parse( - readFileSync("./test/resources/beforeRunMixed.json", "utf-8") - ); - options.jira.testPlanIssueKey = "CYP-456"; - stubbedGet.onFirstCall().resolves({ - status: HttpStatusCode.Ok, - data: [ - { - name: "Test Execution", - id: 123, - }, - { - name: "Test Plan", - id: 456, - }, - { - name: "Test Plan", - id: 789, - }, - ], - headers: null, - statusText: HttpStatusCode[HttpStatusCode.Ok], - config: null, + it("should throw if jira issue type information cannot be fetched", async () => { + stubLogging(); + beforeRunDetails = JSON.parse( + readFileSync("./test/resources/beforeRunMixed.json", "utf-8") + ); + stub(clients.jiraClient, "getIssueTypes").resolves(undefined); + await expect( + beforeRunHook(beforeRunDetails, config, options, clients) + ).to.eventually.be.rejectedWith( + dedent(` + Jira issue type information could not be fetched. + + Please make sure project CYP exists at https://example.org + + For more information, visit: + - https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/jira/#projectkey + - https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/jira/#url + `) + ); }); - await expect( - beforeRunHook( - beforeRunDetails, - config, - options, - new DummyXrayClient(), - new JiraClientCloud( - "https://example.org", - new BasicAuthCredentials("user", "token") - ) - ) - ).to.eventually.be.rejectedWith( - dedent(` - Found multiple issue types named: Test Plan - - Make sure to only make a single one available in project CYP. - - For more information, visit: - - https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/jira/#testExecutionIssueType - - https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/jira/#testPlanIssueType - `) - ); }); - it("should throw if xray test plan issue type information can not be fetched", async () => { - stubLogging(); - const { stubbedGet } = stubRequests(); - beforeRunDetails = JSON.parse( - readFileSync("./test/resources/beforeRunMixed.json", "utf-8") + describe("afterRun", () => { + let results: CypressCommandLine.CypressRunResult = JSON.parse( + readFileSync("./test/resources/runResult.json", "utf-8") ); - options.jira.testPlanIssueKey = "CYP-456"; - options.jira.testPlanIssueType = "Plan Issue"; - stubbedGet.onFirstCall().resolves({ - status: HttpStatusCode.Ok, - data: [ - { - name: "Test Execution", - id: 67890, - }, - ], - headers: null, - statusText: HttpStatusCode[HttpStatusCode.Ok], - config: null, - }); - await expect( - beforeRunHook( - beforeRunDetails, - config, - options, - new DummyXrayClient(), - new JiraClientCloud( - "https://example.org", - new BasicAuthCredentials("user", "token") - ) - ) - ).to.eventually.be.rejectedWith( - dedent(` - Failed to retrieve issue type information for issue type: Plan Issue - - Make sure you have Xray installed. - - For more information, visit: - - https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/jira/#testExecutionIssueType - - https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/jira/#testPlanIssueType - `) - ); - }); - it("should not fetch plan issue type information if not necessary", async () => { - stubLogging(); - const { stubbedGet } = stubRequests(); - beforeRunDetails = JSON.parse( - readFileSync("./test/resources/beforeRunMixed.json", "utf-8") - ); - stubbedGet.onFirstCall().resolves({ - status: HttpStatusCode.Ok, - data: [ - { - name: "Test Execution", - id: 67890, - }, - ], - headers: null, - statusText: HttpStatusCode[HttpStatusCode.Ok], - config: null, + beforeEach(() => { + results = JSON.parse(readFileSync("./test/resources/runResult.json", "utf-8")); }); - await beforeRunHook( - beforeRunDetails, - config, - options, - new DummyXrayClient(), - new JiraClientCloud("https://example.org", new BasicAuthCredentials("user", "token")) - ); - expect(options.jira.testPlanIssueDetails).to.be.undefined; - }); - it("should fetch plan issue type information only if necessary", async () => { - stubLogging(); - const { stubbedGet } = stubRequests(); - beforeRunDetails = JSON.parse( - readFileSync("./test/resources/beforeRunMixed.json", "utf-8") - ); - options.jira.testExecutionIssueDetails = { - name: "Execution Type", - subtask: false, - }; - options.jira.testPlanIssueKey = "CYP-456"; - stubbedGet.onFirstCall().resolves({ - status: HttpStatusCode.Ok, - data: [ - { - name: "Execution Type", - id: 12345, - }, - { - name: "Test Plan", - id: 67890, - }, - ], - headers: null, - statusText: HttpStatusCode[HttpStatusCode.Ok], - config: null, + it("should display errors if the plugin was not configured", async () => { + const { stubbedError } = stubLogging(); + await afterRunHook(results); + expect(stubbedError).to.have.been.calledOnce; + expect(stubbedError).to.have.been.calledWith( + dedent(` + Skipping after:run hook: Plugin misconfigured: configureXrayPlugin() was not called + + Make sure your project is set up correctly: https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/introduction/ + `) + ); }); - await beforeRunHook( - beforeRunDetails, - config, - options, - new DummyXrayClient(), - new JiraClientCloud("https://example.org", new BasicAuthCredentials("user", "token")) - ); - expect(options.jira.testExecutionIssueDetails).to.be.deep.eq({ - name: "Execution Type", - subtask: false, - }); - expect(options.jira.testPlanIssueDetails).to.be.deep.eq({ - name: "Test Plan", - id: 67890, - }); - }); - it("should throw if jira issue type information cannot be fetched", async () => { - stubLogging(); - const { stubbedGet } = stubRequests(); - beforeRunDetails = JSON.parse( - readFileSync("./test/resources/beforeRunMixed.json", "utf-8") - ); - stubbedGet.onFirstCall().rejects( - new AxiosError("Request failed with status code 404", "404", undefined, null, { - status: 404, - statusText: "NotFound", - config: { headers: new AxiosHeaders() }, - headers: {}, - data: { - errorMessages: ["Project CYP does not exist"], - }, - }) - ); - await expect( - beforeRunHook( - beforeRunDetails, - config, - options, - new DummyXrayClient(), - new JiraClientCloud( - "https://example.org", - new BasicAuthCredentials("user", "token") - ) - ) - ).to.eventually.be.rejectedWith( - dedent(` - Jira issue type information could not be fetched. - - Please make sure project CYP exists at https://example.org - - For more information, visit: - - https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/jira/#projectkey - - https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/jira/#url - `) - ); - }); -}); - -describe("the after run hook", () => { - let results: CypressCommandLine.CypressRunResult = JSON.parse( - readFileSync("./test/resources/runResult.json", "utf-8") - ); + it("should throw an error for missing xray clients", async () => { + clients.xrayClient = undefined; + await expect(afterRunHook(results, options)).to.eventually.be.rejectedWith( + dedent(` + Plugin misconfigured: Xray client not configured - let options: InternalOptions; - - beforeEach(() => { - results = JSON.parse(readFileSync("./test/resources/runResult.json", "utf-8")); - options = initOptions( - {}, - { - jira: { - projectKey: "CYP", - url: "https://example.org", - }, - } - ); - }); - - it("should display errors if the plugin was not configured", async () => { - const { stubbedError } = stubLogging(); - await afterRunHook(results); - expect(stubbedError).to.have.been.calledOnce; - expect(stubbedError).to.have.been.calledWith( - dedent(` - Skipping after:run hook: Plugin misconfigured: configureXrayPlugin() was not called - - Make sure your project is set up correctly: https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/introduction/ - `) - ); - }); + Make sure your project is set up correctly: https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/introduction/ + `) + ); + }); - it("should throw an error for missing xray clients", async () => { - await expect(afterRunHook(results, options)).to.eventually.be.rejectedWith( - dedent(` - Plugin misconfigured: Xray client not configured + it("should not display an error for missing xray clients if disabled", async () => { + const { stubbedInfo } = stubLogging(); + options.plugin.enabled = false; + await afterRunHook(results, options); + expect(stubbedInfo).to.have.been.calledOnce; + expect(stubbedInfo).to.have.been.calledWith("Skipping after:run hook: Plugin disabled"); + }); - Make sure your project is set up correctly: https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/introduction/ - `) - ); - }); + it("should throw an error for missing jira clients", async () => { + clients.jiraClient = undefined; + await expect(afterRunHook(results, options, clients)).to.eventually.be.rejectedWith( + dedent(` + Plugin misconfigured: Jira client not configured - it("should not display an error for missing xray clients if disabled", async () => { - const { stubbedInfo } = stubLogging(); - options.plugin.enabled = false; - await afterRunHook(results, options); - expect(stubbedInfo).to.have.been.calledOnce; - expect(stubbedInfo).to.have.been.calledWith("Skipping after:run hook: Plugin disabled"); - }); + Make sure your project is set up correctly: https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/introduction/ + `) + ); + }); - it("should throw an error for missing jira clients", async () => { - await expect( - afterRunHook(results, options, new DummyXrayClient()) - ).to.eventually.be.rejectedWith( - dedent(` - Plugin misconfigured: Jira client not configured + it("should not display an error for missing jira clients if disabled", async () => { + const { stubbedInfo } = stubLogging(); + options.plugin.enabled = false; + await afterRunHook(results, options, clients); + expect(stubbedInfo).to.have.been.calledOnce; + expect(stubbedInfo).to.have.been.calledWith("Skipping after:run hook: Plugin disabled"); + }); - Make sure your project is set up correctly: https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/introduction/ - `) - ); - }); + it("should display an error for failed runs", async () => { + const { stubbedError } = stubLogging(); + const failedResults: CypressCommandLine.CypressFailedRunResult = { + status: "failed", + failures: 47, + message: "Pretty messed up", + }; + await afterRunHook(failedResults, options); + expect(stubbedError).to.have.been.calledOnce; + expect(stubbedError).to.have.been.calledWith( + dedent(` + Skipping after:run hook: Failed to run 47 tests + + Pretty messed up + `) + ); + }); - it("should not display an error for missing jira clients if disabled", async () => { - const { stubbedInfo } = stubLogging(); - options.plugin.enabled = false; - await afterRunHook(results, options, new DummyXrayClient()); - expect(stubbedInfo).to.have.been.calledOnce; - expect(stubbedInfo).to.have.been.calledWith("Skipping after:run hook: Plugin disabled"); - }); + it("should not display an error for failed runs if disabled", async () => { + const { stubbedInfo } = stubLogging(); + const failedResults: CypressCommandLine.CypressFailedRunResult = { + status: "failed", + failures: 47, + message: "Pretty messed up", + }; + options.plugin.enabled = false; + await afterRunHook(failedResults, options); + expect(stubbedInfo).to.have.been.calledOnce; + expect(stubbedInfo).to.have.been.calledWith("Skipping after:run hook: Plugin disabled"); + }); - it("should display an error for failed runs", async () => { - const { stubbedError } = stubLogging(); - const failedResults: CypressCommandLine.CypressFailedRunResult = { - status: "failed", - failures: 47, - message: "Pretty messed up", - }; - await afterRunHook(failedResults, options); - expect(stubbedError).to.have.been.calledOnce; - expect(stubbedError).to.have.been.calledWith( - dedent(` - Skipping after:run hook: Failed to run 47 tests - - Pretty messed up - `) - ); + it("should skip the results upload if disabled", async () => { + const { stubbedInfo } = stubLogging(); + options.xray.uploadResults = false; + await afterRunHook(results, options, clients); + expect(stubbedInfo).to.have.been.calledOnce; + expect(stubbedInfo).to.have.been.calledWith( + "Skipping results upload: Plugin is configured to not upload test results" + ); + }); }); - it("should not display an error for failed runs if disabled", async () => { - const { stubbedInfo } = stubLogging(); - const failedResults: CypressCommandLine.CypressFailedRunResult = { - status: "failed", - failures: 47, - message: "Pretty messed up", + describe("the synchronize file hook", () => { + const file: Cypress.FileObject = { + filePath: "./test/resources/features/taggedCloud.feature", + outputPath: null, + shouldWatch: false, + addListener: null, + on: null, + once: null, + removeListener: null, + off: null, + removeAllListeners: null, + setMaxListeners: null, + getMaxListeners: null, + listeners: null, + rawListeners: null, + emit: null, + listenerCount: null, + prependListener: null, + prependOnceListener: null, + eventNames: null, }; - options.plugin.enabled = false; - await afterRunHook(failedResults, options); - expect(stubbedInfo).to.have.been.calledOnce; - expect(stubbedInfo).to.have.been.calledWith("Skipping after:run hook: Plugin disabled"); - }); - - it("should skip the results upload if disabled", async () => { - const { stubbedInfo } = stubLogging(); - options.xray.uploadResults = false; - await afterRunHook(results, options, new DummyXrayClient(), new DummyJiraClient()); - expect(stubbedInfo).to.have.been.calledOnce; - expect(stubbedInfo).to.have.been.calledWith( - "Skipping results upload: Plugin is configured to not upload test results" - ); - }); -}); - -describe("the synchronize file hook", () => { - const file: Cypress.FileObject = { - filePath: "./test/resources/features/taggedCloud.feature", - outputPath: null, - shouldWatch: false, - addListener: null, - on: null, - once: null, - removeListener: null, - off: null, - removeAllListeners: null, - setMaxListeners: null, - getMaxListeners: null, - listeners: null, - rawListeners: null, - emit: null, - listenerCount: null, - prependListener: null, - prependOnceListener: null, - eventNames: null, - }; - - let options: InternalOptions; - beforeEach(() => { - options = initOptions( - {}, - { - jira: { - projectKey: "CYP", - url: "https://example.org", - }, - cucumber: { - featureFileExtension: ".feature", - }, - } - ); - }); - - it("should display errors if the plugin was not configured", async () => { - const { stubbedError } = stubLogging(); - await synchronizeFile(file, ".", null, null, null, null); - expect(stubbedError).to.have.been.calledOnce; - expect(stubbedError).to.have.been.calledWith( - dedent(` - Plugin misconfigured (no configuration was provided). Skipping feature file synchronization triggered by: ./test/resources/features/taggedCloud.feature - - Make sure your project is set up correctly: https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/introduction/ - `) - ); - }); + it("should display errors if the plugin was not configured", async () => { + const { stubbedError } = stubLogging(); + await synchronizeFile(file, ".", null, null); + expect(stubbedError).to.have.been.calledOnce; + expect(stubbedError).to.have.been.calledWith( + dedent(` + Plugin misconfigured (no configuration was provided). Skipping feature file synchronization triggered by: ./test/resources/features/taggedCloud.feature + + Make sure your project is set up correctly: https://qytera-gmbh.github.io/projects/cypress-xray-plugin/section/configuration/introduction/ + `) + ); + }); - it("should not do anything if disabled", async () => { - file.filePath = "./test/resources/features/taggedCloud.feature"; - const { stubbedInfo } = stubLogging(); - options.plugin = { enabled: false }; - await synchronizeFile(file, ".", options, null, null, null); - expect(stubbedInfo).to.have.been.calledOnce; - expect(stubbedInfo).to.have.been.calledWith( - "Plugin disabled. Skipping feature file synchronization triggered by: ./test/resources/features/taggedCloud.feature" - ); - }); + it("should not do anything if disabled", async () => { + file.filePath = "./test/resources/features/taggedCloud.feature"; + const { stubbedInfo } = stubLogging(); + options.plugin = { enabled: false }; + await synchronizeFile(file, ".", options, null); + expect(stubbedInfo).to.have.been.calledOnce; + expect(stubbedInfo).to.have.been.calledWith( + "Plugin disabled. Skipping feature file synchronization triggered by: ./test/resources/features/taggedCloud.feature" + ); + }); - it("should display errors for invalid feature files", async () => { - file.filePath = "./test/resources/features/invalid.feature"; - const { stubbedInfo, stubbedError } = stubLogging(); - options.cucumber.uploadFeatures = true; - await synchronizeFile(file, ".", options, null, null, null); - expect(stubbedError).to.have.been.calledOnce; - expect(stubbedError).to.have.been.calledWith( - "Feature file invalid, skipping synchronization: Error: Parser errors:\n" + - "(9:3): expected: #EOF, #TableRow, #DocStringSeparator, #StepLine, #TagLine, #ScenarioLine, #RuleLine, #Comment, #Empty, got 'Invalid: Element'" - ); - expect(stubbedInfo).to.have.been.calledOnce; - expect(stubbedInfo).to.have.been.calledWith( - `Preprocessing feature file ${path.join( - "test", - "resources", - "features", - "invalid.feature" - )}...` - ); - }); + it("should display errors for invalid feature files", async () => { + file.filePath = "./test/resources/features/invalid.feature"; + const { stubbedInfo, stubbedError } = stubLogging(); + options.cucumber.uploadFeatures = true; + await synchronizeFile(file, ".", options, clients); + expect(stubbedError).to.have.been.calledOnce; + expect(stubbedError).to.have.been.calledWith( + "Feature file invalid, skipping synchronization: Error: Parser errors:\n" + + "(9:3): expected: #EOF, #TableRow, #DocStringSeparator, #StepLine, #TagLine, #ScenarioLine, #RuleLine, #Comment, #Empty, got 'Invalid: Element'" + ); + expect(stubbedInfo).to.have.been.calledOnce; + expect(stubbedInfo).to.have.been.calledWith( + `Preprocessing feature file ${path.join( + "test", + "resources", + "features", + "invalid.feature" + )}...` + ); + }); - it("should not try to parse mismatched feature files", async () => { - file.filePath = "./test/resources/greetings.txt"; - const { stubbedError } = stubLogging(); - await synchronizeFile(file, ".", options, null, null, null); - expect(stubbedError).to.not.have.been.called; + it("should not try to parse mismatched feature files", async () => { + file.filePath = "./test/resources/greetings.txt"; + const { stubbedError } = stubLogging(); + await synchronizeFile(file, ".", options, null); + expect(stubbedError).to.not.have.been.called; + }); }); }); diff --git a/src/hooks.ts b/src/hooks.ts index 03c40293..ac828cd6 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -25,7 +25,7 @@ import { IssueTypeDetailsServer, } from "./types/jira/responses/issueTypeDetails"; import { IssueUpdateCloud, IssueUpdateServer } from "./types/jira/responses/issueUpdate"; -import { InternalOptions } from "./types/plugin"; +import { ClientCombination, InternalOptions } from "./types/plugin"; import { StringMap } from "./types/util"; import { XrayTestExecutionResultsCloud, @@ -41,8 +41,7 @@ export async function beforeRunHook( runDetails: Cypress.BeforeRunDetails, config?: Cypress.PluginConfigOptions, options?: InternalOptions, - xrayClient?: XrayClientServer | XrayClientCloud, - jiraClient?: JiraClientServer | JiraClientCloud + clients?: ClientCombination ) { if (!options) { // Don't throw here in case someone simply doesn't want the plugin to run but forgot to @@ -60,7 +59,7 @@ export async function beforeRunHook( logInfo("Plugin disabled. Skipping before:run hook"); return; } - if (!xrayClient) { + if (!clients.xrayClient) { throw new Error( dedent(` Plugin misconfigured: Xray client was not configured @@ -69,7 +68,7 @@ export async function beforeRunHook( `) ); } - if (!jiraClient) { + if (!clients.jiraClient) { throw new Error( dedent(` Plugin misconfigured: Jira client was not configured @@ -107,14 +106,11 @@ export async function beforeRunHook( `) ); } - if ( - !options.jira.testExecutionIssueDetails || - (options.jira.testPlanIssueKey && !options.jira.testPlanIssueDetails) - ) { + if (!options.jira.testExecutionIssueDetails) { logInfo( "Fetching necessary Jira issue type information in preparation for Cucumber result uploads..." ); - const issueDetails = await jiraClient.getIssueTypes(); + const issueDetails = await clients.jiraClient.getIssueTypes(); if (!issueDetails) { throw new Error( dedent(` @@ -135,14 +131,6 @@ export async function beforeRunHook( options.jira.projectKey ); } - // Test plan information might not be needed. - if (options.jira.testPlanIssueKey && !options.jira.testPlanIssueDetails) { - options.jira.testPlanIssueDetails = retrieveIssueTypeInformation( - options.jira.testPlanIssueType, - issueDetails, - options.jira.projectKey - ); - } } } } @@ -183,9 +171,7 @@ function retrieveIssueTypeInformation< export async function afterRunHook( results: CypressCommandLine.CypressRunResult | CypressCommandLine.CypressFailedRunResult, options?: InternalOptions, - xrayClient?: XrayClientServer | XrayClientCloud, - jiraClient?: JiraClientServer | JiraClientCloud, - jiraRepository?: JiraRepositoryServer | JiraRepositoryCloud + clients?: ClientCombination ) { if (!options) { // Don't throw here in case someone simply doesn't want the plugin to run but forgot to @@ -218,7 +204,7 @@ export async function afterRunHook( logInfo("Skipping results upload: Plugin is configured to not upload test results"); return; } - if (!xrayClient) { + if (!clients?.xrayClient) { throw new Error( dedent(` Plugin misconfigured: Xray client not configured @@ -227,7 +213,7 @@ export async function afterRunHook( `) ); } - if (!jiraClient) { + if (!clients?.jiraClient) { throw new Error( dedent(` Plugin misconfigured: Jira client not configured @@ -238,7 +224,12 @@ export async function afterRunHook( } let issueKey: string = null; if (containsNativeTest(runResult, options)) { - issueKey = await uploadCypressResults(runResult, options, xrayClient, jiraRepository); + issueKey = await uploadCypressResults( + runResult, + options, + clients.xrayClient, + clients.jiraRepository + ); if ( options.jira.testExecutionIssueKey && issueKey && @@ -257,7 +248,7 @@ export async function afterRunHook( } } if (containsCucumberTest(runResult, options)) { - const cucumberIssueKey = await uploadCucumberResults(runResult, options, xrayClient); + const cucumberIssueKey = await uploadCucumberResults(runResult, options, clients); if ( options.jira.testExecutionIssueKey && cucumberIssueKey && @@ -291,7 +282,7 @@ export async function afterRunHook( return; } if (options.jira.attachVideos) { - await attachVideos(runResult, issueKey, jiraClient); + await attachVideos(runResult, issueKey, clients.jiraClient); } } @@ -306,12 +297,12 @@ async function uploadCypressResults( const issueTestTypes = await jiraRepository.getTestTypes(...issueKeys); let cypressExecution: XrayTestExecutionResultsServer | XrayTestExecutionResultsCloud; if (xrayClient instanceof XrayClientServer) { - cypressExecution = new ImportExecutionConverterServer(options).convert(runResult, { + cypressExecution = await new ImportExecutionConverterServer(options).convert(runResult, { summaries: issueSummaries, testTypes: issueTestTypes, }); } else { - cypressExecution = new ImportExecutionConverterCloud(options).convert(runResult, { + cypressExecution = await new ImportExecutionConverterCloud(options).convert(runResult, { summaries: issueSummaries, testTypes: issueTestTypes, }); @@ -321,25 +312,24 @@ async function uploadCypressResults( async function uploadCucumberResults( runResult: CypressCommandLine.CypressRunResult, - options?: InternalOptions, - xrayClient?: XrayClientServer | XrayClientCloud + options: InternalOptions, + clients: ClientCombination ) { const results: CucumberMultipartFeature[] = JSON.parse( fs.readFileSync(options.cucumber.preprocessor.json.output, "utf-8") ); let cucumberMultipart: CucumberMultipartServer | CucumberMultipartCloud; - if (xrayClient instanceof XrayClientServer) { - cucumberMultipart = new ImportExecutionCucumberMultipartConverterServer(options).convert( - results, - runResult - ); + if (clients.kind === "server") { + cucumberMultipart = await new ImportExecutionCucumberMultipartConverterServer( + options, + clients.jiraRepository + ).convert(results, runResult); } else { - cucumberMultipart = new ImportExecutionCucumberMultipartConverterCloud(options).convert( - results, - runResult - ); + cucumberMultipart = await new ImportExecutionCucumberMultipartConverterCloud( + options + ).convert(results, runResult); } - return await xrayClient.importExecutionCucumberMultipart( + return await clients.xrayClient.importExecutionCucumberMultipart( cucumberMultipart.features, cucumberMultipart.info ); @@ -364,9 +354,7 @@ export async function synchronizeFile( file: Cypress.FileObject, projectRoot: string, options: InternalOptions, - xrayClient: XrayClientServer | XrayClientCloud, - jiraClient: JiraClientServer | JiraClientCloud, - jiraRepository: JiraRepositoryServer | JiraRepositoryCloud + clients: ClientCombination ): Promise { if (!options) { logError( @@ -396,7 +384,7 @@ export async function synchronizeFile( const issueData = getCucumberIssueData( file.filePath, options, - xrayClient instanceof XrayClientCloud + clients.kind === "cloud" ); // Xray currently does not allow keeping the test issues' summaries when importing // feature files to existing issues. Therefore, we manually need to backup and @@ -410,13 +398,18 @@ export async function synchronizeFile( ${testIssueKeys.join("\n")} `) ); - const testSummaries = await jiraRepository.getSummaries(...testIssueKeys); - const wasImportSuccessful = await xrayClient.importFeature( + const testSummaries = await clients.jiraRepository.getSummaries(...testIssueKeys); + const wasImportSuccessful = await clients.xrayClient.importFeature( file.filePath, options.jira.projectKey ); if (wasImportSuccessful) { - await resetSummaries(issueData, testSummaries, jiraClient, jiraRepository); + await resetSummaries( + issueData, + testSummaries, + clients.jiraClient, + clients.jiraRepository + ); } } } catch (error: unknown) { diff --git a/src/https/requests.ts b/src/https/requests.ts index cc530be7..06f7ce2d 100644 --- a/src/https/requests.ts +++ b/src/https/requests.ts @@ -5,6 +5,12 @@ import { logDebug, writeFile } from "../logging/logging"; import { InternalOptions } from "../types/plugin"; import { normalizedFilename } from "../util/files"; +export type RequestConfigPost = { + url: string; + data?: D; + config?: RawAxiosRequestConfig; +}; + export class Requests { private static AGENT: Agent = undefined; private static AXIOS: Axios = undefined; @@ -40,8 +46,7 @@ export class Requests { const filename = normalizedFilename( `${timestamp}_${method}_${url}_request.json` ); - logDebug(`Writing request to ${filename}.`); - writeFile( + const resolvedFilename = writeFile( { url: url, headers: request.headers, @@ -50,6 +55,7 @@ export class Requests { }, filename ); + logDebug(`Request: ${resolvedFilename}`); return request; }, (error) => { @@ -67,8 +73,8 @@ export class Requests { filename = normalizedFilename(`${timestamp}_request.json`); data = error; } - logDebug(`Writing request to ${filename}.`); - writeFile(data, filename); + const resolvedFilename = writeFile(data, filename); + logDebug(`Request: ${resolvedFilename}`); return Promise.reject(error); } ); @@ -80,8 +86,7 @@ export class Requests { const filename = normalizedFilename( `${timestamp}_${method}_${url}_response.json` ); - logDebug(`Writing response to ${filename}.`); - writeFile( + const resolvedFilename = writeFile( { data: response.data, headers: response.headers, @@ -90,6 +95,7 @@ export class Requests { }, filename ); + logDebug(`Response: ${resolvedFilename}`); return response; }, (error) => { @@ -107,8 +113,8 @@ export class Requests { filename = normalizedFilename(`${timestamp}_response.json`); data = error; } - logDebug(`Writing response to ${filename}.`); - writeFile(data, filename); + const resolvedFilename = writeFile(data, filename); + logDebug(`Response: ${resolvedFilename}`); return Promise.reject(error); } ); diff --git a/src/logging/logging.ts b/src/logging/logging.ts index 2ae4810c..0cd99543 100644 --- a/src/logging/logging.ts +++ b/src/logging/logging.ts @@ -83,11 +83,12 @@ function log( * @param data the data to write * @param filename the filename to use for the file */ -export function writeFile(data: T, filename: string): void { +export function writeFile(data: T, filename: string): string { const logDirectoryPath = path.resolve(loggingOptions.logDirectory); fs.mkdirSync(logDirectoryPath, { recursive: true }); const filepath = path.resolve(logDirectoryPath, filename); fs.writeFileSync(filepath, JSON.stringify(data)); + return filepath; } /** diff --git a/src/plugin.ts b/src/plugin.ts index f0e376a1..694767a9 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,4 +1,4 @@ -import { initClients, initJiraRepository, initOptions, verifyOptions } from "./context"; +import { initClients, initOptions, verifyOptions } from "./context"; import { afterRunHook, beforeRunHook, synchronizeFile } from "./hooks"; import { Requests } from "./https/requests"; import { initLogging, logInfo } from "./logging/logging"; @@ -16,10 +16,7 @@ export async function configureXrayPlugin(config: Cypress.PluginConfigOptions, o return; } verifyOptions(context.internal); - const clients = initClients(context.internal, config.env); - context.xrayClient = clients.xrayClient; - context.jiraClient = clients.jiraClient; - context.jiraRepository = initJiraRepository(clients, options); + context.clients = initClients(context.internal, config.env); Requests.init(context.internal); initLogging({ debug: context.internal.plugin.debug, @@ -29,26 +26,14 @@ export async function configureXrayPlugin(config: Cypress.PluginConfigOptions, o export async function addXrayResultUpload(on: Cypress.PluginEvents) { on("before:run", async (runDetails: Cypress.BeforeRunDetails) => { - await beforeRunHook( - runDetails, - context.cypress, - context.internal, - context.xrayClient, - context.jiraClient - ); + await beforeRunHook(runDetails, context.cypress, context.internal, context.clients); }); on( "after:run", async ( results: CypressCommandLine.CypressRunResult | CypressCommandLine.CypressFailedRunResult ) => { - await afterRunHook( - results, - context.internal, - context.xrayClient, - context.jiraClient, - context.jiraRepository - ); + await afterRunHook(results, context.internal, context.clients); } ); } @@ -58,8 +43,6 @@ export async function syncFeatureFile(file: Cypress.FileObject): Promise file, context.cypress.projectRoot, context.internal, - context.xrayClient, - context.jiraClient, - context.jiraRepository + context.clients ); } diff --git a/src/preprocessing/preprocessing.spec.ts b/src/preprocessing/preprocessing.spec.ts index 7f537fb5..92b3b527 100644 --- a/src/preprocessing/preprocessing.spec.ts +++ b/src/preprocessing/preprocessing.spec.ts @@ -197,7 +197,7 @@ describe("cucumber preprocessing", () => { }); describe("server", () => { - it("should throw for missing scenario tags", () => { + it("throws for missing scenario tags", () => { expect(() => getCucumberIssueData( "./test/resources/features/taggedServerMissingScenario.feature", @@ -220,7 +220,7 @@ describe("cucumber preprocessing", () => { ); }); - it("should throw for multiple scenario tags", async () => { + it("throws for multiple scenario tags", async () => { expect(() => getCucumberIssueData( "./test/resources/features/taggedServerMultipleScenario.feature", @@ -257,7 +257,7 @@ describe("cucumber preprocessing", () => { You can target existing precondition issues by adding a corresponding comment: Background: A background - #@Precondition:CYP-123 + #@CYP-123 # steps ... For more information, visit: @@ -280,11 +280,11 @@ describe("cucumber preprocessing", () => { The plugin cannot decide for you which one to use: Background: A background - #@Precondition:CYP-244 - ^^^^^^^^^^^^^^^^^^^^^^ + #@CYP-244 + ^^^^^^^^^ # a random comment - #@Precondition:CYP-262 - ^^^^^^^^^^^^^^^^^^^^^^ + #@CYP-262 + ^^^^^^^^^ # steps ... For more information, visit: @@ -301,6 +301,7 @@ describe("cucumber preprocessing", () => { const tag = getCucumberPreconditionIssueTags( document.feature.children[0].background, "CYP", + false, document.comments ); expect(tag).to.deep.eq(["CYP-244", "CYP-262"]); @@ -310,15 +311,14 @@ describe("cucumber preprocessing", () => { const feature = parseFeatureFile( "./test/resources/features/taggedServerMultipleScenario.feature" ).feature; - expect(getCucumberScenarioIssueTags(feature.children[1].scenario, "CYP")).to.deep.eq([ - "CYP-123", - "CYP-456", - ]); + expect( + getCucumberScenarioIssueTags(feature.children[1].scenario, "CYP", false) + ).to.deep.eq(["CYP-123", "CYP-456"]); }); }); describe("cloud", () => { - it("should throw for missing scenario tags", async () => { + it("throws for missing scenario tags", async () => { expect(() => getCucumberIssueData( "./test/resources/features/taggedCloudMissingScenario.feature", @@ -341,7 +341,7 @@ describe("cucumber preprocessing", () => { ); }); - it("should throw for multiple scenario tags", async () => { + it("throws for multiple scenario tags", async () => { expect(() => getCucumberIssueData( "./test/resources/features/taggedCloudMultipleScenario.feature", @@ -365,7 +365,7 @@ describe("cucumber preprocessing", () => { ); }); - it("should throw for missing background tags", async () => { + it("throws for missing background tags", async () => { expect(() => getCucumberIssueData( "./test/resources/features/taggedCloudMissingBackground.feature", @@ -388,7 +388,7 @@ describe("cucumber preprocessing", () => { ); }); - it("should throw for multiple background tags", async () => { + it("throws for multiple background tags", async () => { expect(() => getCucumberIssueData( "./test/resources/features/taggedCloudMultipleBackground.feature", @@ -422,6 +422,7 @@ describe("cucumber preprocessing", () => { const tag = getCucumberPreconditionIssueTags( document.feature.children[0].background, "CYP", + true, document.comments ); expect(tag).to.deep.eq(["CYP-244", "CYP-262"]); @@ -431,10 +432,9 @@ describe("cucumber preprocessing", () => { const feature = parseFeatureFile( "./test/resources/features/taggedCloudMultipleScenario.feature" ).feature; - expect(getCucumberScenarioIssueTags(feature.children[1].scenario, "CYP")).to.deep.eq([ - "CYP-123", - "CYP-456", - ]); + expect( + getCucumberScenarioIssueTags(feature.children[1].scenario, "CYP", true) + ).to.deep.eq(["CYP-123", "CYP-456"]); }); }); diff --git a/src/preprocessing/preprocessing.ts b/src/preprocessing/preprocessing.ts index 89be748c..2c021b14 100644 --- a/src/preprocessing/preprocessing.ts +++ b/src/preprocessing/preprocessing.ts @@ -146,7 +146,11 @@ export function getCucumberIssueData( const document = parseFeatureFile(filePath); for (const child of document.feature.children) { if (child.scenario) { - const issueKeys = getCucumberScenarioIssueTags(child.scenario, options.jira.projectKey); + const issueKeys = getCucumberScenarioIssueTags( + child.scenario, + options.jira.projectKey, + isCloudClient + ); if (issueKeys.length === 0) { throw new Error( dedent(` @@ -187,6 +191,7 @@ export function getCucumberIssueData( const preconditionKeys = getCucumberPreconditionIssueTags( child.background, options.jira.projectKey, + isCloudClient, document.comments ); if (preconditionKeys.length === 0) { @@ -198,7 +203,7 @@ export function getCucumberIssueData( You can target existing precondition issues by adding a corresponding comment: ${getBackgroundLine(child.background)} - ${getBackgroundTag(options.jira.projectKey)} + ${getBackgroundTag(options.jira.projectKey, isCloudClient)} # steps ... For more information, visit: @@ -264,8 +269,8 @@ function getBackgroundLine(background: Background): string { return `${background.keyword}: ${background.name}`; } -function getBackgroundTag(projectKey: string): string { - return `#@Precondition:${projectKey}-123`; +function getBackgroundTag(projectKey: string, isCloudClient: boolean): string { + return isCloudClient ? `#@Precondition:${projectKey}-123` : `#@${projectKey}-123`; } function reconstructMultipleTagsBackground( @@ -310,10 +315,14 @@ export function parseFeatureFile( return parser.parse(fs.readFileSync(file, { encoding: encoding })); } -export function getCucumberScenarioIssueTags(scenario: Scenario, projectKey: string): string[] { +export function getCucumberScenarioIssueTags( + scenario: Scenario, + projectKey: string, + isCloudClient: boolean +): string[] { const issueKeys: string[] = []; for (const tag of scenario.tags) { - const matches = tag.name.match(scenarioRegex(projectKey)); + const matches = tag.name.match(scenarioRegex(projectKey, isCloudClient)); if (!matches) { continue; } else if (matches.length === 2) { @@ -323,15 +332,19 @@ export function getCucumberScenarioIssueTags(scenario: Scenario, projectKey: str return issueKeys; } -function scenarioRegex(projectKey: string) { - // Xray cloud: @TestName:CYP-123 - // Xray server: @CYP-123 - return new RegExp(`@(?:TestName:)?(${projectKey}-\\d+)`); +function scenarioRegex(projectKey: string, isCloudClient: boolean) { + if (isCloudClient) { + // @TestName:CYP-123 + return new RegExp(`@TestName:(${projectKey}-\\d+)`); + } + // @CYP-123 + return new RegExp(`@(${projectKey}-\\d+)`); } export function getCucumberPreconditionIssueTags( background: Background, projectKey: string, + isCloudClient: boolean, comments: readonly Comment[] ): string[] { const preconditionKeys: string[] = []; @@ -340,7 +353,7 @@ export function getCucumberPreconditionIssueTags( const firstStepLine = background.steps[0].location.line; for (const comment of comments) { if (comment.location.line > backgroundLine && comment.location.line < firstStepLine) { - const matches = comment.text.match(backgroundRegex(projectKey)); + const matches = comment.text.match(backgroundRegex(projectKey, isCloudClient)); if (!matches) { continue; } else if (matches.length === 2) { @@ -352,7 +365,11 @@ export function getCucumberPreconditionIssueTags( return preconditionKeys; } -function backgroundRegex(projectKey: string) { - // @Precondition:CYP-111 - return new RegExp(`@Precondition:(${projectKey}-\\d+)`); +function backgroundRegex(projectKey: string, isCloudClient: boolean) { + if (isCloudClient) { + // @Precondition:CYP-111 + return new RegExp(`@Precondition:(${projectKey}-\\d+)`); + } + // @CYP-111 + return new RegExp(`@(${projectKey}-\\d+)`); } diff --git a/src/types/plugin.ts b/src/types/plugin.ts index e9732432..9e969565 100644 --- a/src/types/plugin.ts +++ b/src/types/plugin.ts @@ -231,20 +231,31 @@ export type InternalOptions = Options & { * The details of the test execution issue type. */ testExecutionIssueDetails?: OneOf<[IssueTypeDetailsServer, IssueTypeDetailsCloud]>; - /** - * The details of the test plan issue type. - */ - testPlanIssueDetails?: OneOf<[IssueTypeDetailsServer, IssueTypeDetailsCloud]>; }; cucumber?: { preprocessor?: Awaited>; }; }; +/** + * Type describing the possible client combinations. + */ +export type ClientCombination = + | { + kind: "server"; + jiraClient: JiraClientServer; + xrayClient: XrayClientServer; + jiraRepository: JiraRepositoryServer; + } + | { + kind: "cloud"; + jiraClient: JiraClientCloud; + xrayClient: XrayClientCloud; + jiraRepository: JiraRepositoryCloud; + }; + export interface PluginContext { cypress: Cypress.PluginConfigOptions; internal: InternalOptions; - xrayClient?: XrayClientServer | XrayClientCloud; - jiraClient?: JiraClientServer | JiraClientCloud; - jiraRepository?: JiraRepositoryServer | JiraRepositoryCloud; + clients?: ClientCombination; } diff --git a/src/types/util.ts b/src/types/util.ts index e577d660..d59af8e4 100644 --- a/src/types/util.ts +++ b/src/types/util.ts @@ -1,8 +1,3 @@ -import { JiraClientCloud } from "../client/jira/jiraClientCloud"; -import { JiraClientServer } from "../client/jira/jiraClientServer"; -import { XrayClientCloud } from "../client/xray/xrayClientCloud"; -import { XrayClientServer } from "../client/xray/xrayClientServer"; - // https://stackoverflow.com/a/53229567 type UnionKeys = T extends T ? keyof T : never; type Expand = T extends T ? { [K in keyof T]: T[K] } : never; @@ -38,18 +33,3 @@ export function getEnumKeyByEnumValue = { [key: string]: T; }; - -/** - * Type describing the possible client combinations. - */ -export type ClientCombination = - | { - kind: "server"; - jiraClient: JiraClientServer; - xrayClient: XrayClientServer; - } - | { - kind: "cloud"; - jiraClient: JiraClientCloud; - xrayClient: XrayClientCloud; - }; diff --git a/test/resources/features/taggedServerMissingScenario.feature b/test/resources/features/taggedServerMissingScenario.feature index cd2b32f7..d5aff66e 100644 --- a/test/resources/features/taggedServerMissingScenario.feature +++ b/test/resources/features/taggedServerMissingScenario.feature @@ -1,7 +1,7 @@ Feature: A tagged feature Background: A background - #@Precondition:CYP-123 + #@CYP-123 Given abc123 Then xyz987 diff --git a/test/resources/features/taggedServerMultipleBackground.feature b/test/resources/features/taggedServerMultipleBackground.feature index 7fe12b42..6b85e211 100644 --- a/test/resources/features/taggedServerMultipleBackground.feature +++ b/test/resources/features/taggedServerMultipleBackground.feature @@ -1,9 +1,9 @@ Feature: A tagged feature Background: A background - #@Precondition:CYP-244 + #@CYP-244 # a random comment - #@Precondition:CYP-262 + #@CYP-262 Given abc123 Then xyz987 diff --git a/test/resources/features/taggedServerMultipleScenario.feature b/test/resources/features/taggedServerMultipleScenario.feature index bac5e6dd..fa091ab6 100644 --- a/test/resources/features/taggedServerMultipleScenario.feature +++ b/test/resources/features/taggedServerMultipleScenario.feature @@ -1,7 +1,7 @@ Feature: A tagged feature Background: A background - #@Precondition:CYP-244 + #@CYP-244 Given abc123 Then xyz987 diff --git a/test/util.ts b/test/util.ts index da0437ca..116982fb 100644 --- a/test/util.ts +++ b/test/util.ts @@ -1,5 +1,6 @@ import { HttpStatusCode } from "axios"; import chai from "chai"; +import FormData from "form-data"; import fs from "fs"; import path from "path"; import Sinon, { stub } from "sinon"; @@ -7,7 +8,7 @@ import sinonChai from "sinon-chai"; import { JWTCredentials } from "../src/authentication/credentials"; import { JiraClient } from "../src/client/jira/jiraClient"; import { XrayClient } from "../src/client/xray/xrayClient"; -import { Requests } from "../src/https/requests"; +import { RequestConfigPost, Requests } from "../src/https/requests"; import * as logging from "../src/logging/logging"; import { initLogging } from "../src/logging/logging"; @@ -27,6 +28,7 @@ export const stubRequests = () => { return { stubbedGet: stub(Requests, "get"), stubbedPost: stub(Requests, "post"), + stubbedPut: stub(Requests, "put"), }; }; @@ -86,7 +88,7 @@ export class DummyXrayClient extends XrayClient { public getTestTypes(): Promise<{ [key: string]: string }> { throw new Error("Method not implemented."); } - public getUrlImportExecutionCucumberMultipart(): string { + public prepareRequestImportExecutionCucumberMultipart(): Promise> { throw new Error("Method not implemented."); } public handleResponseImportExecutionCucumberMultipart(): string { From 894086edba0982ef43327e33f1affc45eb26cd06 Mon Sep 17 00:00:00 2001 From: Sebastian Vollbrecht Date: Sat, 29 Jul 2023 13:47:23 +0200 Subject: [PATCH 18/19] Improve log messages --- src/hooks.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/hooks.ts b/src/hooks.ts index ac828cd6..cc9f62a0 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -439,8 +439,8 @@ async function resetSummaries( dedent(` Resetting issue summary of issue: ${issueKey} - Old summary (pre sync): ${oldSummary} - New summary (post sync): ${newSummary} + Summary pre sync: ${oldSummary} + Summary post sync: ${newSummary} `) ); if (!(await jiraClient.editIssue(issueKey, issueUpdate))) { @@ -448,8 +448,8 @@ async function resetSummaries( dedent(` Failed to reset issue summary of issue to its old summary: ${issueKey} - Old summary (pre sync): ${oldSummary} - New summary (post sync): ${newSummary} + Summary pre sync: ${oldSummary} + Summary post sync: ${newSummary} Make sure to reset it manually if needed `) From daa93d0a0d0003d76f346b12427ebd7147524fa5 Mon Sep 17 00:00:00 2001 From: Sebastian Vollbrecht Date: Sat, 29 Jul 2023 14:04:22 +0200 Subject: [PATCH 19/19] Also update precondition summaries --- src/hooks.ts | 18 +++++++++++------- src/preprocessing/preprocessing.ts | 12 +++++++++--- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/hooks.ts b/src/hooks.ts index cc9f62a0..38696d33 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -386,12 +386,15 @@ export async function synchronizeFile( options, clients.kind === "cloud" ); - // Xray currently does not allow keeping the test issues' summaries when importing - // feature files to existing issues. Therefore, we manually need to backup and - // reset the summary once the import is done. + // Xray currently (almost) always overwrites issue summaries when importing feature + // files to existing issues. Therefore, we manually need to backup and reset the + // summary once the import is done. // See: https://docs.getxray.app/display/XRAY/Importing+Cucumber+Tests+-+REST // See: https://docs.getxray.app/display/XRAYCLOUD/Importing+Cucumber+Tests+-+REST+v2 - const testIssueKeys = issueData.tests.map((data) => data.key); + const testIssueKeys = [ + ...issueData.tests.map((data) => data.key), + ...issueData.preconditions.map((data) => data.key), + ]; logDebug( dedent(` Creating issue summary backups for issues: @@ -425,10 +428,11 @@ async function resetSummaries( jiraClient: JiraClientServer | JiraClientCloud, jiraRepository: JiraRepositoryServer | JiraRepositoryCloud ) { - for (let i = 0; i < issueData.tests.length; i++) { - const issueKey = issueData.tests[i].key; + const allIssues = [...issueData.tests, ...issueData.preconditions]; + for (let i = 0; i < allIssues.length; i++) { + const issueKey = allIssues[i].key; const oldSummary = testSummaries[issueKey]; - const newSummary = issueData.tests[i].summary; + const newSummary = allIssues[i].summary; if (oldSummary !== newSummary) { const issueUpdate: IssueUpdateServer | IssueUpdateCloud = { fields: {}, diff --git a/src/preprocessing/preprocessing.ts b/src/preprocessing/preprocessing.ts index 2c021b14..7a6903f3 100644 --- a/src/preprocessing/preprocessing.ts +++ b/src/preprocessing/preprocessing.ts @@ -131,7 +131,10 @@ export interface FeatureFileIssueData { key: string; summary: string; }[]; - preconditions: string[]; + preconditions: { + key: string; + summary: string; + }[]; } export function getCucumberIssueData( @@ -185,7 +188,7 @@ export function getCucumberIssueData( } featureFileIssueKeys.tests.push({ key: issueKeys[0], - summary: child.scenario.name, + summary: child.scenario.name ? child.scenario.name : "", }); } else if (child.background) { const preconditionKeys = getCucumberPreconditionIssueTags( @@ -228,7 +231,10 @@ export function getCucumberIssueData( ]; throw new Error(lines.join("\n")); } - featureFileIssueKeys.preconditions.push(preconditionKeys[0]); + featureFileIssueKeys.preconditions.push({ + key: preconditionKeys[0], + summary: child.background.name ? child.background.name : "", + }); } } return featureFileIssueKeys;