From 2f3c17d534f3f0cd99bba959ef12c6af6547ff22 Mon Sep 17 00:00:00 2001 From: Leon Zolati Date: Wed, 14 Jun 2023 15:13:35 +1000 Subject: [PATCH 01/11] fix(no recent worklog): account for subtask's worklog when calculating 'no recent worklog' Add any worklogs recorded on subtasks of a given task when calculating that tasks grade for 'no recent worklog' fix #258 --- cli/.eslintrc.js | 149 ++++++++++++----------- cli/src/scripts/search.ts | 9 +- lib/.eslintrc.js | 5 + lib/src/services/issue_checks.ts | 17 +++ lib/src/services/jira.test.ts | 7 +- lib/src/services/jira.ts | 41 +++++-- lib/src/services/jira_api.ts | 48 +++++++- lib/src/services/test_data/issue_data.ts | 3 +- 8 files changed, 190 insertions(+), 89 deletions(-) diff --git a/cli/.eslintrc.js b/cli/.eslintrc.js index aeaef6a..9ae186c 100644 --- a/cli/.eslintrc.js +++ b/cli/.eslintrc.js @@ -1,72 +1,77 @@ -module.exports = { - parser: "@typescript-eslint/parser", - parserOptions: { - project: "tsconfig.json", - tsconfigRootDir: __dirname, - ecmaVersion: 2018, - sourceType: "module", - }, - extends: ["typed-fp", "agile-digital"], - env: { - "jest/globals": true, - es6: true, - }, - plugins: [ - "jest", - "sonarjs", - "functional", - "@typescript-eslint", - "prettier", - "total-functions", - ], - rules: { - // https://github.com/aotaduy/eslint-plugin-spellcheck - "spellcheck/spell-checker": [ - "warn", - { - skipWords: [ - "Argv", - "Authorised", - "assignee", - "changelog", - "changelogs", - "clc", - "codec", - "Codec", - "globals", - "io", - "issuetype", - "jira", - "Jira", - "jiralint", - "jiralintrc", - "jql", - "Kanban", - "Nullable", - "oauth", - "proxied", - "Readonly", - "readonly", - "servlet", - "sonarjs", - "subtask", - "subtasks", - "timetracking", - "unicode", - "utf8", - "Urls", - "versioned", - "worklog", - "Worklog", - "worklogs", - "yargs", - ], - }, - ], - }, - settings: { - jest: { - version: 28, - }, - }, -}; +module.exports = { + parser: "@typescript-eslint/parser", + parserOptions: { + project: "tsconfig.json", + tsconfigRootDir: __dirname, + ecmaVersion: 2018, + sourceType: "module", + }, + extends: ["typed-fp", "agile-digital"], + env: { + "jest/globals": true, + es6: true, + }, + plugins: [ + "jest", + "sonarjs", + "functional", + "@typescript-eslint", + "prettier", + "total-functions", + ], + rules: { + // https://github.com/aotaduy/eslint-plugin-spellcheck + "spellcheck/spell-checker": [ + "warn", + { + skipWords: [ + "Argv", + "Authorised", + "assignee", + "changelog", + "changelogs", + "clc", + "codec", + "Codec", + "globals", + "io", + "issuetype", + "jira", + "Jira", + "jiralint", + "jiralintrc", + "jql", + "Kanban", + "Nullable", + "oauth", + "proxied", + "Readonly", + "readonly", + "servlet", + "sonarjs", + "subtask", + "subtasks", + "timetracking", + "unicode", + "utf8", + "Urls", + "versioned", + "worklog", + "Worklog", + "worklogs", + "yargs", + "monday", + "tuesday", + "sunday", + "aggregatetimeoriginalestimate", + "aggregatetimespent", + ], + }, + ], + }, + settings: { + jest: { + version: 28, + }, + }, +}; diff --git a/cli/src/scripts/search.ts b/cli/src/scripts/search.ts index eca2083..79b425a 100644 --- a/cli/src/scripts/search.ts +++ b/cli/src/scripts/search.ts @@ -20,6 +20,7 @@ import * as clc from "cli-color"; // eslint-disable-next-line functional/no-expression-statements require("cli-color"); +// eslint-disable-next-line functional/type-declaration-immutability type CheckedIssue = EnhancedIssue & { readonly action: IssueAction; readonly reasons: readonly string[]; @@ -27,7 +28,9 @@ type CheckedIssue = EnhancedIssue & { }; const checkedIssues = ( + // eslint-disable-next-line functional/prefer-immutable-types issues: readonly EnhancedIssue[] + // eslint-disable-next-line functional/prefer-immutable-types ): readonly CheckedIssue[] => { const now = readonlyDate(readonlyNow()); @@ -51,9 +54,9 @@ const checkedIssues = ( }); }; -// eslint-disable-next-line functional/no-return-void +// eslint-disable-next-line functional/no-return-void, functional/prefer-immutable-types const renderJson = (issues: readonly EnhancedIssue[]): void => { - // eslint-disable-next-line functional/no-return-void + // eslint-disable-next-line functional/no-return-void, functional/prefer-immutable-types checkedIssues(issues).forEach((issue) => // eslint-disable-next-line no-console console.log(JSON.stringify(issue, null, 2)) @@ -61,6 +64,7 @@ const renderJson = (issues: readonly EnhancedIssue[]): void => { }; const renderTable = ( + // eslint-disable-next-line functional/prefer-immutable-types issues: readonly EnhancedIssue[], qualityFieldName: string // eslint-disable-next-line functional/no-return-void @@ -205,6 +209,7 @@ const search = async ( // eslint-disable-next-line functional/no-expression-statements countdown.start(); + // eslint-disable-next-line functional/prefer-immutable-types const issues = await jira.searchIssues( jql, boardNamesToIgnore, diff --git a/lib/.eslintrc.js b/lib/.eslintrc.js index 1253887..cd2ce93 100644 --- a/lib/.eslintrc.js +++ b/lib/.eslintrc.js @@ -66,6 +66,11 @@ module.exports = { "Worklog", "worklogs", "yargs", + "monday", + "tuesday", + "sunday", + "aggregatetimeoriginalestimate", + "aggregatetimespent", ], }, ], diff --git a/lib/src/services/issue_checks.ts b/lib/src/services/issue_checks.ts index 19ab3c2..eefc650 100644 --- a/lib/src/services/issue_checks.ts +++ b/lib/src/services/issue_checks.ts @@ -24,6 +24,7 @@ export type CheckResult = { readonly reasons: ReadonlyNonEmptyArray; }; +// eslint-disable-next-line functional/prefer-immutable-types export type Check = (issue: EnhancedIssue) => CheckResult; export type Action = "none" | "inspect"; @@ -84,6 +85,7 @@ const notInProgressReason = "not in progress"; */ export const validateInProgressHasWorklog = (at: ReadonlyDate) => + // eslint-disable-next-line functional/prefer-immutable-types (issue: EnhancedIssue): CheckResult => { const check = checker("In progress tickets have been worked"); @@ -111,6 +113,7 @@ export const validateInProgressHasWorklog = * @returns result of checking the issue. */ export const validateDependenciesHaveDueDate = ( + // eslint-disable-next-line functional/prefer-immutable-types issue: EnhancedIssue ): CheckResult => { const check = checker("Dependencies have a due date"); @@ -132,6 +135,7 @@ export const validateDependenciesHaveDueDate = ( */ export const validateNotClosedDependenciesNotPassedDueDate = (at: ReadonlyDate) => + // eslint-disable-next-line functional/prefer-immutable-types (issue: EnhancedIssue): CheckResult => { const check = checker("Dependencies not passed due date"); @@ -160,6 +164,7 @@ export const validateNotClosedDependenciesNotPassedDueDate = }; export const validateInProgressHasEstimate = ( + // eslint-disable-next-line functional/prefer-immutable-types issue: EnhancedIssue ): CheckResult => { const check = checker("In Progress issues have estimates"); @@ -182,6 +187,7 @@ export const validateInProgressHasEstimate = ( }; // TODO check whether the description is a template and fail if it is. +// eslint-disable-next-line functional/prefer-immutable-types export const validateDescription = (issue: EnhancedIssue): CheckResult => { const check = checker("Tickets have a description"); @@ -193,6 +199,7 @@ export const validateDescription = (issue: EnhancedIssue): CheckResult => { const validateNotStalledFor = (at: ReadonlyDate) => ( + // eslint-disable-next-line functional/prefer-immutable-types issue: EnhancedIssue, duration: number, durationDescription: string @@ -222,16 +229,19 @@ const validateNotStalledFor = const validateNotStalledForMoreThanOneDay = (at: ReadonlyDate) => + // eslint-disable-next-line functional/prefer-immutable-types (issue: EnhancedIssue): CheckResult => validateNotStalledFor(at)(issue, 0, "one day"); const validateNotStalledForMoreThanOneWeek = (at: ReadonlyDate) => + // eslint-disable-next-line functional/prefer-immutable-types (issue: EnhancedIssue): CheckResult => validateNotStalledFor(at)(issue, 5, "one week"); const vaildateNotWaitingForReviewForMoreThanHalfADay = (at: ReadonlyDate) => + // eslint-disable-next-line functional/prefer-immutable-types (issue: EnhancedIssue): CheckResult => { const check = checker("issues not waiting for review for too long"); @@ -275,6 +285,7 @@ const vaildateNotWaitingForReviewForMoreThanHalfADay = }; const validateInProgressNotCloseToEstimate = ( + // eslint-disable-next-line functional/prefer-immutable-types issue: EnhancedIssue ): CheckResult => { const check = checker( @@ -299,6 +310,7 @@ const validateInProgressNotCloseToEstimate = ( */ export const validateTooLongInBacklog = (at: ReadonlyDate) => + // eslint-disable-next-line functional/prefer-immutable-types (issue: EnhancedIssue): CheckResult => { const check = checker("issues don't stay in the backlog for too long"); const ageInMonths = differenceInCalendarMonths( @@ -323,6 +335,7 @@ export const validateTooLongInBacklog = // TODO check sub-tasks for comments? export const validateComment = (at: ReadonlyDate) => + // eslint-disable-next-line functional/prefer-immutable-types (issue: EnhancedIssue): CheckResult => { const check = checker("issues that have been worked have comments"); @@ -366,7 +379,9 @@ export const validateComment = }; const check = ( + // eslint-disable-next-line functional/prefer-immutable-types issue: EnhancedIssue, + // eslint-disable-next-line functional/prefer-immutable-types checks: readonly ((t: EnhancedIssue) => CheckResult)[] ): IssueAction => { const noAction: IssueAction = { @@ -400,6 +415,7 @@ const check = ( * @returns true if the issue deserves some grace, otherwise false. */ export const issueDeservesGrace = ( + // eslint-disable-next-line functional/prefer-immutable-types issue: EnhancedIssue, now: ReadonlyDate ): boolean => { @@ -420,6 +436,7 @@ export const issueDeservesGrace = ( * @returns whether action is required, and the checks that were run to form that recommendation. */ export const issueActionRequired = ( + // eslint-disable-next-line functional/prefer-immutable-types issue: EnhancedIssue, now: ReadonlyDate, customChecks: readonly Check[] diff --git a/lib/src/services/jira.test.ts b/lib/src/services/jira.test.ts index 4eb87a6..c6fec26 100644 --- a/lib/src/services/jira.test.ts +++ b/lib/src/services/jira.test.ts @@ -88,6 +88,7 @@ describe("decoding well-formed tickets", () => { // Given a well-formed bit of data. // When it is decoded. + // eslint-disable-next-line functional/prefer-immutable-types const actual = CloudIssue.decode(data); // Then no errors should be reported. @@ -181,7 +182,7 @@ describe("finding the most recent work date", () => { // Given an issue. // When the time it was last worked is found. - const lastWorked = issueLastWorked(issue); + const lastWorked = issueLastWorked(issue, []); // Then it should match the expected value. expect(lastWorked).toEqual(expected); @@ -310,8 +311,10 @@ describe("enhancing issues", () => { }; // When it is enhanced + // eslint-disable-next-line functional/prefer-immutable-types const enhanced = enhancedIssue( issue, + [], "viewlink", fieldName, "not_reason", @@ -341,8 +344,10 @@ describe("enhancing issues", () => { }; // When it is enhanced + // eslint-disable-next-line functional/prefer-immutable-types const enhanced = enhancedIssue( issue, + [], "viewlink", "not_quality", fieldName, diff --git a/lib/src/services/jira.ts b/lib/src/services/jira.ts index 5d06da8..c956524 100644 --- a/lib/src/services/jira.ts +++ b/lib/src/services/jira.ts @@ -1,4 +1,4 @@ -/* eslint functional/prefer-immutable-types: ["error", { "enforcement": "ReadonlyDeep" }] */ +/* eslint-disable functional/prefer-immutable-types */ /* eslint-disable spellcheck/spell-checker */ import * as T from "io-ts"; import * as ITT from "io-ts-types"; @@ -152,6 +152,12 @@ export const BaseIssue = T.readonly( ) ), duedate: nullOrMissingToUndefined(readonlyDateFromDate), + subtasks: T.readonlyArray( + T.type({ + id: T.string, + key: T.string, + }) + ), }) ), T.readonly( @@ -294,7 +300,6 @@ export const Board = T.readonly( }) ); -// eslint-disable-next-line functional/type-declaration-immutability export type Board = Readonly>; export const BoardSummary = T.readonly( @@ -442,12 +447,26 @@ export const mostRecentIssueComment = ( * @returns the most recent worklog, or undefined if no work has been logged. */ export const mostRecentIssueWorklog = ( - issue: Issue + issue: Issue, + issues: readonly Issue[] ): IssueWorklog | undefined => { - const worklogs = - issue.fields.worklog === undefined ? [] : issue.fields.worklog.worklogs; + const subtaskWorklogs = issue.fields.subtasks.reduce( + (acc: IssueWorklog[], subtask) => { + const subtaskIssue = issues.find( + (subtaskIssue) => subtaskIssue.key === subtask.key + ); + const work = subtaskIssue?.fields.worklog?.worklogs; + + return work === undefined ? acc : acc.concat(work); + }, + [] + ); - return [...worklogs].sort((a, b) => + const allWorklogs: IssueWorklog[] = ( + issue.fields.worklog?.worklogs ?? [] + ).concat(subtaskWorklogs); + + return [...allWorklogs].sort((a, b) => compareDesc(a.started.valueOf(), b.started.valueOf()) )[0]; }; @@ -458,12 +477,15 @@ export const mostRecentIssueWorklog = ( * @param issue the issue. * @returns the time that the issue was last worked, or undefined if it has never been worked. */ -export const issueLastWorked = (issue: Issue): ReadonlyDate | undefined => { +export const issueLastWorked = ( + issue: Issue, + issues: readonly Issue[] +): ReadonlyDate | undefined => { const mostRecentTransition = mostRecentIssueTransition(issue); const mostRecentComment = mostRecentIssueComment(issue); - const mostRecentWorklog = mostRecentIssueWorklog(issue); + const mostRecentWorklog = mostRecentIssueWorklog(issue, issues); return [ mostRecentTransition?.created, @@ -476,6 +498,7 @@ export const issueLastWorked = (issue: Issue): ReadonlyDate | undefined => { export const enhancedIssue = ( issue: Issue, + issues: readonly Issue[], viewLink: string, qualityFieldName: string, qualityReasonFieldName: string, @@ -488,7 +511,7 @@ export const enhancedIssue = ( const released = issue.fields.fixVersions.some((version) => version.released); - const lastWorked = issueLastWorked(issue); + const lastWorked = issueLastWorked(issue, issues); // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const quality = issue.fields[qualityFieldName] as string | undefined; diff --git a/lib/src/services/jira_api.ts b/lib/src/services/jira_api.ts index 9178497..5a3d381 100644 --- a/lib/src/services/jira_api.ts +++ b/lib/src/services/jira_api.ts @@ -1,4 +1,3 @@ -/* eslint functional/prefer-immutable-types: ["error", { "enforcement": "ReadonlyDeep" }] */ /* eslint-disable spellcheck/spell-checker */ import { Either } from "fp-ts/lib/Either"; import * as E from "fp-ts/lib/Either"; @@ -59,6 +58,7 @@ export type JiraClient = { qualityReasonField: string, customFieldNames: readonly string[], descriptionFields: ReadonlyRecord + // eslint-disable-next-line functional/prefer-immutable-types ) => Promise>; readonly currentUser: () => Promise>; }; @@ -263,6 +263,7 @@ const jiraClient = ( * @returns the mapped validation. */ const mapValidationError = ( + // eslint-disable-next-line functional/prefer-immutable-types validation: T.Validation ): Either => E.isLeft(validation) @@ -280,6 +281,7 @@ const jiraClient = ( const decode = ( name: string, input: I, + // eslint-disable-next-line functional/prefer-immutable-types decoder: (i: I) => T.Validation ): Either => pipe( @@ -301,6 +303,7 @@ const jiraClient = ( * @returns Issue */ const onPremJiraToGeneric = ( + // eslint-disable-next-line functional/prefer-immutable-types onPremIssue: OnPremIssue ): TE.TaskEither => { // eslint-disable-next-line functional/prefer-immutable-types @@ -332,6 +335,7 @@ const jiraClient = ( * @returns Issue */ const cloudJiraToGeneric = ( + // eslint-disable-next-line functional/prefer-immutable-types cloudIssue: CloudIssue ): TE.TaskEither => { // eslint-disable-next-line functional/prefer-immutable-types @@ -440,10 +444,12 @@ const jiraClient = ( }; const boardsByProject = ( + // eslint-disable-next-line functional/prefer-immutable-types issues: readonly Issue[], boardNamesToIgnore: readonly string[] ): TE.TaskEither> => { const projectKeys: readonly string[] = issues + // eslint-disable-next-line functional/prefer-immutable-types .map((issue) => issue.fields.project.key) .filter( (value, index, self: readonly string[]) => self.indexOf(value) === index @@ -530,6 +536,7 @@ const jiraClient = ( return pipe(fetch, TE.chain(parsed)); }; + // eslint-disable-next-line functional/prefer-immutable-types const issueLink = (issue: Issue): string => `${jiraProtocol}://${jiraHost}/browse/${encodeURIComponent(issue.key)}`; @@ -565,6 +572,7 @@ const jiraClient = ( }, }), (error: unknown) => { + // eslint-disable-next-line functional/prefer-immutable-types const jiraError = JiraError.decode(error); return isLeft(jiraError) ? `Unexpected error from Jira when updating quality of [${key}] to [${quality}] - [${JSON.stringify( @@ -628,6 +636,7 @@ const jiraClient = ( qualityReasonField: string, customFieldNames: readonly string[], descriptionFields: ReadonlyRecord + // eslint-disable-next-line sonarjs/cognitive-complexity, functional/prefer-immutable-types ): Promise> => { const fetchIssues = TE.tryCatch( // eslint-disable-next-line functional/functional-parameters, functional/prefer-immutable-types @@ -673,6 +682,7 @@ const jiraClient = ( ? pipe( parseCloudJira(response), //response to cloudIssueType TE.chain( + // eslint-disable-next-line functional/prefer-immutable-types TE.traverseSeqArray((cloudIssue) => cloudJiraToGeneric(cloudIssue) ) @@ -681,6 +691,7 @@ const jiraClient = ( : pipe( parseOnPremJira(response), TE.chain( + // eslint-disable-next-line functional/prefer-immutable-types TE.traverseSeqArray((onPremIssue) => onPremJiraToGeneric(onPremIssue) ) @@ -713,10 +724,12 @@ const jiraClient = ( ); const enhancedIssues = ( + // eslint-disable-next-line functional/prefer-immutable-types issues: readonly Issue[] ): TE.TaskEither => { // eslint-disable-next-line functional/prefer-immutable-types return TE.map((boards: ReadonlyRecord) => { + // eslint-disable-next-line functional/prefer-immutable-types return issues.map((issue) => { const issueBoards = boards[issue.fields.project.key] ?? []; const boardByStatus = issueBoards.find((board) => @@ -728,6 +741,7 @@ const jiraClient = ( ); return enhancedIssue( issue, + issues, issueLink(issue), qualityField, qualityReasonField, @@ -739,6 +753,7 @@ const jiraClient = ( }; const issueWithComment = ( + // eslint-disable-next-line functional/prefer-immutable-types issue: EnhancedIssue ): TE.TaskEither => { const mostRecentCommentLoaded = @@ -747,10 +762,12 @@ const jiraClient = ( const recentComment = ( worklogs: readonly IssueComment[] | undefined + // eslint-disable-next-line functional/prefer-immutable-types ): IssueComment | undefined => worklogs === undefined ? undefined - : [...worklogs].sort((w1, w2) => + : // eslint-disable-next-line functional/prefer-immutable-types + [...worklogs].sort((w1, w2) => compareDesc(w1.created.valueOf(), w2.created.valueOf()) )[0]; @@ -767,6 +784,7 @@ const jiraClient = ( }; const issueWithWorklog = ( + // eslint-disable-next-line functional/prefer-immutable-types issue: EnhancedIssue ): TE.TaskEither => { const mostRecentWorklogLoaded = @@ -775,17 +793,37 @@ const jiraClient = ( const recentWorklog = ( worklogs: readonly IssueWorklog[] | undefined + // eslint-disable-next-line functional/prefer-immutable-types ): IssueWorklog | undefined => worklogs === undefined ? undefined - : [...worklogs].sort((w1, w2) => + : // eslint-disable-next-line functional/prefer-immutable-types + [...worklogs].sort((w1, w2) => compareDesc(w1.started.valueOf(), w2.started.valueOf()) )[0]; + // eslint-disable-next-line functional/prefer-immutable-types + const relevantKeys: string[] = mostRecentWorklogLoaded + ? [] + : issue.fields.subtasks + // eslint-disable-next-line functional/prefer-immutable-types + .reduce((acc: string[], cur) => acc.concat([cur.key]), []) + .concat([issue.key]); + + // eslint-disable-next-line functional/prefer-immutable-types + const worklogs: TE.TaskEither[] = + relevantKeys.map((key) => fetchMostRecentWorklogs(key)); + + const worklog: TE.TaskEither = pipe( + worklogs, + TE.sequenceArray, + TE.map((x): readonly IssueWorklog[] => x.flat()) + ); + return pipe( mostRecentWorklogLoaded ? TE.right(issue.fields.worklog.worklogs) - : fetchMostRecentWorklogs(issue.key), + : worklog, // eslint-disable-next-line functional/prefer-immutable-types TE.map((worklogs) => ({ ...issue, @@ -798,7 +836,9 @@ const jiraClient = ( fetchIssues, TE.chain(convertIssueType), TE.chain(enhancedIssues), + // eslint-disable-next-line functional/prefer-immutable-types TE.chain(TE.traverseSeqArray((issue) => issueWithComment(issue))), + // eslint-disable-next-line functional/prefer-immutable-types TE.chain(TE.traverseSeqArray((issue) => issueWithWorklog(issue))) )(); }, diff --git a/lib/src/services/test_data/issue_data.ts b/lib/src/services/test_data/issue_data.ts index 664a1a1..622a849 100644 --- a/lib/src/services/test_data/issue_data.ts +++ b/lib/src/services/test_data/issue_data.ts @@ -1,4 +1,4 @@ -/* eslint functional/prefer-immutable-types: ["error", { "enforcement": "ReadonlyDeep" }] */ +/* eslint-disable functional/prefer-immutable-types */ /* eslint-disable spellcheck/spell-checker */ import type { EnhancedIssue, Issue, IssueWorklog } from "../jira"; import { readonlyDate } from "readonly-types"; @@ -42,6 +42,7 @@ export const issue: Issue = { total: 0, startAt: 0, }, + subtasks: [], duedate: undefined, worklog: { worklogs: [], From c95f582215b47c1505fa9e649428ca6abfea6bff Mon Sep 17 00:00:00 2001 From: Leon Zolati Date: Wed, 14 Jun 2023 16:32:52 +1000 Subject: [PATCH 02/11] docs: the README should mention npm run commit and windows specific changes required to run --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 75df188..7229ab7 100644 --- a/README.md +++ b/README.md @@ -174,3 +174,14 @@ You can create a Jira API token for your account [here](https://id.atlassian.com "qualityReasonFieldName": "customfield_12346" } ``` + +## Pull Requests +### Commits +In order to conform to the JiraLint PR standard, commits should be done with +``` sh +npm run commit +``` +### Windows +Windows users will need to make an alteration to the package.json in the lib folder in order to run this script and/or the npm run test --workspaces script. +Change the test script to "test": "SET TZ=Australia/Canberra jest", + From fa78928c7a1831a2b8e1bd860edb7ec32179faa1 Mon Sep 17 00:00:00 2001 From: Leon Zolati Date: Thu, 15 Jun 2023 16:17:51 +1000 Subject: [PATCH 03/11] test(jira_api): add a test to jira_api to ensure that worklogs from subtasks are considered When worklogs are fetched using the API, the subtasks worklogs should also be added. --- lib/src/services/jira_api.test.ts | 74 ++++++++++ lib/src/services/jira_api.ts | 55 ++------ lib/src/services/test_data/jira_api_data.ts | 144 ++++++++++++++++++++ 3 files changed, 231 insertions(+), 42 deletions(-) create mode 100644 lib/src/services/jira_api.test.ts create mode 100644 lib/src/services/test_data/jira_api_data.ts diff --git a/lib/src/services/jira_api.test.ts b/lib/src/services/jira_api.test.ts new file mode 100644 index 0000000..35f4df6 --- /dev/null +++ b/lib/src/services/jira_api.test.ts @@ -0,0 +1,74 @@ +/* eslint-disable functional/prefer-immutable-types */ +import * as E from "fp-ts/lib/Either"; +import JiraApi from "jira-client"; +import { JiraClient, jiraClient } from "./jira_api"; +import { boardReturn, searchJiraReturn } from "./test_data/jira_api_data"; +import { readonlyDate } from "readonly-types"; +import { pipe } from "fp-ts/lib/function"; + +jest.mock("jira-client", () => { + return jest.fn().mockImplementation(() => { + return { + searchJira: jest + .fn() + .mockImplementation(() => + Promise.resolve({ issues: searchJiraReturn }) + ), + getAllBoards: jest + .fn() + .mockImplementation(() => + Promise.resolve({ values: [{ id: 0, name: "0" }] }) + ), + getConfiguration: jest + .fn() + .mockImplementation(() => Promise.resolve(boardReturn)), + genericGet: jest.fn().mockImplementation((input: string) => { + if (input.includes("comment")) return Promise.resolve({ comments: [] }); + else + return input.includes("parent") + ? Promise.resolve({ worklogs: [] }) + : Promise.resolve({ + worklogs: [ + { + author: { name: "Jim" }, + started: readonlyDate("2023-05-31T13:46:58.132+1000"), + timeSpentSeconds: 1, + comment: "I did a thing", + }, + ], + }); + }), + }; + }); +}); + +describe("Searching issues", () => { + const mockApi: Readonly = new JiraApi({ + host: "host.atlassian.net", + }); + + const client: JiraClient = jiraClient("https", "host.atlassian.net", mockApi); + + it("should account for worklogs of subtasks in the parent", async () => { + const response = await client.searchIssues("", [], "", "", [], {}); + + expect(E.isRight(response)).toBeTruthy(); + + pipe( + response, + E.map((issues) => { + issues.forEach((issue) => { + if (issue.key.includes("parent")) + // As the worklog is not defined in the mocked JQL search call + // nor are they defined in the mocked worklog API call for the parent + // the only way this can be defined is if the worklogs + // of the subtask is taken into account when you call search issues. + // eslint-disable-next-line jest/no-conditional-expect + expect(issue.mostRecentWorklog).toBeDefined(); + }); + }), + //This should always throw an error in Jest + E.mapLeft((message) => expect(message).toThrow()) + ); + }); +}); diff --git a/lib/src/services/jira_api.ts b/lib/src/services/jira_api.ts index 5a3d381..958b590 100644 --- a/lib/src/services/jira_api.ts +++ b/lib/src/services/jira_api.ts @@ -1,4 +1,5 @@ /* eslint-disable spellcheck/spell-checker */ +/* eslint-disable functional/prefer-immutable-types*/ import { Either } from "fp-ts/lib/Either"; import * as E from "fp-ts/lib/Either"; import * as TE from "fp-ts/lib/TaskEither"; @@ -40,6 +41,7 @@ type FieldNotEditable = { readonly fields: readonly string[]; }; +// eslint-disable-next-line functional/type-declaration-immutability export type JiraClient = { readonly jiraApi: Readonly; readonly updateIssueQuality: ( @@ -58,7 +60,6 @@ export type JiraClient = { qualityReasonField: string, customFieldNames: readonly string[], descriptionFields: ReadonlyRecord - // eslint-disable-next-line functional/prefer-immutable-types ) => Promise>; readonly currentUser: () => Promise>; }; @@ -86,7 +87,6 @@ export const getOAuthAccessToken = async ( jiraConsumerSecret: string, secretCallback: (requestUrl: string) => Promise ): Promise => { - // eslint-disable-next-line functional/prefer-immutable-types const oauth = new OAuth( `${jiraProtocol}://${jiraHost}/plugins/servlet/oauth/request-token`, `${jiraProtocol}://${jiraHost}/plugins/servlet/oauth/access-token`, @@ -251,7 +251,7 @@ export const jiraClientWithUserCredentials = ( * @param jiraApi * @returns The Jira API abstraction. */ -const jiraClient = ( +export const jiraClient = ( jiraProtocol: "http" | "https", jiraHost: string, jiraApi: Readonly @@ -263,7 +263,6 @@ const jiraClient = ( * @returns the mapped validation. */ const mapValidationError = ( - // eslint-disable-next-line functional/prefer-immutable-types validation: T.Validation ): Either => E.isLeft(validation) @@ -281,7 +280,6 @@ const jiraClient = ( const decode = ( name: string, input: I, - // eslint-disable-next-line functional/prefer-immutable-types decoder: (i: I) => T.Validation ): Either => pipe( @@ -303,10 +301,8 @@ const jiraClient = ( * @returns Issue */ const onPremJiraToGeneric = ( - // eslint-disable-next-line functional/prefer-immutable-types onPremIssue: OnPremIssue ): TE.TaskEither => { - // eslint-disable-next-line functional/prefer-immutable-types const assignee = onPremIssue.fields.assignee === undefined ? undefined @@ -335,10 +331,8 @@ const jiraClient = ( * @returns Issue */ const cloudJiraToGeneric = ( - // eslint-disable-next-line functional/prefer-immutable-types cloudIssue: CloudIssue ): TE.TaskEither => { - // eslint-disable-next-line functional/prefer-immutable-types const assignee = cloudIssue.fields.assignee === undefined ? undefined @@ -367,7 +361,7 @@ const jiraClient = ( (id: number): TE.TaskEither => { const fetch = (id: number): TE.TaskEither => TE.tryCatch( - // eslint-disable-next-line functional/functional-parameters, functional/prefer-immutable-types + // eslint-disable-next-line functional/functional-parameters () => jiraApi.getConfiguration(id.toString()), (reason: unknown) => `Failed to fetch board [${id}] for [${JSON.stringify(reason)}].` @@ -388,7 +382,7 @@ const jiraClient = ( projectKey: string ): TE.TaskEither> => { const fetch = TE.tryCatch( - // eslint-disable-next-line functional/functional-parameters, functional/prefer-immutable-types + // eslint-disable-next-line functional/functional-parameters () => jiraApi.getAllBoards( undefined, @@ -444,12 +438,10 @@ const jiraClient = ( }; const boardsByProject = ( - // eslint-disable-next-line functional/prefer-immutable-types issues: readonly Issue[], boardNamesToIgnore: readonly string[] ): TE.TaskEither> => { const projectKeys: readonly string[] = issues - // eslint-disable-next-line functional/prefer-immutable-types .map((issue) => issue.fields.project.key) .filter( (value, index, self: readonly string[]) => self.indexOf(value) === index @@ -477,7 +469,7 @@ const jiraClient = ( issueKey: string ): TE.TaskEither => { const fetch = TE.tryCatch( - // eslint-disable-next-line functional/functional-parameters, functional/prefer-immutable-types + // eslint-disable-next-line functional/functional-parameters () => jiraApi.genericGet(`issue/${encodeURIComponent(issueKey)}/worklog`), (error) => `Failed to fetch worklogs for [${issueKey}] - [${JSON.stringify( @@ -506,7 +498,7 @@ const jiraClient = ( issueKey: string ): TE.TaskEither => { const fetch = TE.tryCatch( - // eslint-disable-next-line functional/functional-parameters, functional/prefer-immutable-types + // eslint-disable-next-line functional/functional-parameters () => jiraApi.genericGet( `issue/${encodeURIComponent( @@ -536,7 +528,6 @@ const jiraClient = ( return pipe(fetch, TE.chain(parsed)); }; - // eslint-disable-next-line functional/prefer-immutable-types const issueLink = (issue: Issue): string => `${jiraProtocol}://${jiraHost}/browse/${encodeURIComponent(issue.key)}`; @@ -563,7 +554,7 @@ const jiraClient = ( Either> > => { const updateIssue = TE.tryCatch( - // eslint-disable-next-line functional/functional-parameters, functional/prefer-immutable-types + // eslint-disable-next-line functional/functional-parameters () => jiraApi.updateIssue(key, { fields: { @@ -572,7 +563,6 @@ const jiraClient = ( }, }), (error: unknown) => { - // eslint-disable-next-line functional/prefer-immutable-types const jiraError = JiraError.decode(error); return isLeft(jiraError) ? `Unexpected error from Jira when updating quality of [${key}] to [${quality}] - [${JSON.stringify( @@ -584,7 +574,6 @@ const jiraClient = ( } ); - // eslint-disable-next-line functional/prefer-immutable-types const mapError = TE.mapLeft((error: string | JiraError) => { const fieldNotSettableError = ( jiraError: JiraError, @@ -636,10 +625,10 @@ const jiraClient = ( qualityReasonField: string, customFieldNames: readonly string[], descriptionFields: ReadonlyRecord - // eslint-disable-next-line sonarjs/cognitive-complexity, functional/prefer-immutable-types + // eslint-disable-next-line sonarjs/cognitive-complexity ): Promise> => { const fetchIssues = TE.tryCatch( - // eslint-disable-next-line functional/functional-parameters, functional/prefer-immutable-types + // eslint-disable-next-line functional/functional-parameters () => jiraApi.searchJira(jql, { fields: [ @@ -682,7 +671,6 @@ const jiraClient = ( ? pipe( parseCloudJira(response), //response to cloudIssueType TE.chain( - // eslint-disable-next-line functional/prefer-immutable-types TE.traverseSeqArray((cloudIssue) => cloudJiraToGeneric(cloudIssue) ) @@ -691,7 +679,6 @@ const jiraClient = ( : pipe( parseOnPremJira(response), TE.chain( - // eslint-disable-next-line functional/prefer-immutable-types TE.traverseSeqArray((onPremIssue) => onPremJiraToGeneric(onPremIssue) ) @@ -724,12 +711,9 @@ const jiraClient = ( ); const enhancedIssues = ( - // eslint-disable-next-line functional/prefer-immutable-types issues: readonly Issue[] ): TE.TaskEither => { - // eslint-disable-next-line functional/prefer-immutable-types return TE.map((boards: ReadonlyRecord) => { - // eslint-disable-next-line functional/prefer-immutable-types return issues.map((issue) => { const issueBoards = boards[issue.fields.project.key] ?? []; const boardByStatus = issueBoards.find((board) => @@ -753,7 +737,6 @@ const jiraClient = ( }; const issueWithComment = ( - // eslint-disable-next-line functional/prefer-immutable-types issue: EnhancedIssue ): TE.TaskEither => { const mostRecentCommentLoaded = @@ -762,12 +745,10 @@ const jiraClient = ( const recentComment = ( worklogs: readonly IssueComment[] | undefined - // eslint-disable-next-line functional/prefer-immutable-types ): IssueComment | undefined => worklogs === undefined ? undefined - : // eslint-disable-next-line functional/prefer-immutable-types - [...worklogs].sort((w1, w2) => + : [...worklogs].sort((w1, w2) => compareDesc(w1.created.valueOf(), w2.created.valueOf()) )[0]; @@ -775,7 +756,6 @@ const jiraClient = ( mostRecentCommentLoaded ? TE.right(issue.fields.comment.comments) : fetchMostRecentComments(issue.key), - // eslint-disable-next-line functional/prefer-immutable-types TE.map((comments) => ({ ...issue, mostRecentComment: recentComment(comments), @@ -784,7 +764,6 @@ const jiraClient = ( }; const issueWithWorklog = ( - // eslint-disable-next-line functional/prefer-immutable-types issue: EnhancedIssue ): TE.TaskEither => { const mostRecentWorklogLoaded = @@ -793,24 +772,19 @@ const jiraClient = ( const recentWorklog = ( worklogs: readonly IssueWorklog[] | undefined - // eslint-disable-next-line functional/prefer-immutable-types ): IssueWorklog | undefined => worklogs === undefined ? undefined - : // eslint-disable-next-line functional/prefer-immutable-types - [...worklogs].sort((w1, w2) => + : [...worklogs].sort((w1, w2) => compareDesc(w1.started.valueOf(), w2.started.valueOf()) )[0]; - // eslint-disable-next-line functional/prefer-immutable-types const relevantKeys: string[] = mostRecentWorklogLoaded ? [] : issue.fields.subtasks - // eslint-disable-next-line functional/prefer-immutable-types .reduce((acc: string[], cur) => acc.concat([cur.key]), []) .concat([issue.key]); - // eslint-disable-next-line functional/prefer-immutable-types const worklogs: TE.TaskEither[] = relevantKeys.map((key) => fetchMostRecentWorklogs(key)); @@ -824,7 +798,6 @@ const jiraClient = ( mostRecentWorklogLoaded ? TE.right(issue.fields.worklog.worklogs) : worklog, - // eslint-disable-next-line functional/prefer-immutable-types TE.map((worklogs) => ({ ...issue, mostRecentWorklog: recentWorklog(worklogs), @@ -836,9 +809,7 @@ const jiraClient = ( fetchIssues, TE.chain(convertIssueType), TE.chain(enhancedIssues), - // eslint-disable-next-line functional/prefer-immutable-types TE.chain(TE.traverseSeqArray((issue) => issueWithComment(issue))), - // eslint-disable-next-line functional/prefer-immutable-types TE.chain(TE.traverseSeqArray((issue) => issueWithWorklog(issue))) )(); }, @@ -852,7 +823,7 @@ const jiraClient = ( // eslint-disable-next-line functional/functional-parameters currentUser: async (): Promise> => { const fetchUser = TE.tryCatch( - // eslint-disable-next-line functional/functional-parameters, functional/prefer-immutable-types + // eslint-disable-next-line functional/functional-parameters () => jiraApi.getCurrentUser(), (error: unknown) => `Error fetching current user - [${JSON.stringify(error)}]` diff --git a/lib/src/services/test_data/jira_api_data.ts b/lib/src/services/test_data/jira_api_data.ts new file mode 100644 index 0000000..da2393e --- /dev/null +++ b/lib/src/services/test_data/jira_api_data.ts @@ -0,0 +1,144 @@ +import { readonlyDate } from "readonly-types"; +import { Board, CloudIssue } from "../jira"; + +export const boardReturn: Board = { + id: 0, + name: "0", + type: "a really cool one", + columnConfig: { + columns: [ + { + name: "backlog", + statuses: [ + { + id: "0", + }, + ], + }, + ], + constraintType: "very constrained", + }, +}; + +// eslint-disable-next-line functional/prefer-immutable-types +export const searchJiraReturn: CloudIssue[] = [ + { + key: "parent key", + self: "parent url atlassian", + fields: { + summary: "parent issue summary", + description: "description", + created: readonlyDate("2023-05-31T13:46:58.132+1000"), + project: { + key: "projectKey", + }, + assignee: { + displayName: "a really cool dude", + }, + timetracking: { + originalEstimateSeconds: 0, + timeSpentSeconds: 0, + remainingEstimateSeconds: 0, + originalEstimate: "0d", + timeSpent: "0d", + }, + fixVersions: [], + // eslint-disable-next-line spellcheck/spell-checker + aggregateprogress: { + progress: 120, + total: 120, + percent: 100, + }, + issuetype: { + name: "issue type", + subtask: false, + }, + status: { + id: "0", + name: "backlog", + statusCategory: { + id: 0, + name: "Backlog", + colorName: "yellow", + }, + }, + comment: undefined, + worklog: undefined, + duedate: undefined, + subtasks: [ + { + id: "subtask id", + key: "subtask key", + }, + ], + // eslint-disable-next-line spellcheck/spell-checker + aggregatetimeestimate: 0, + aggregatetimeoriginalestimate: 0, + aggregatetimespent: 0, + parent: { + id: "", + key: "", + }, + }, + changelog: { + histories: [], + }, + }, + { + key: "subtask key", + self: "subtask url atlassian", + fields: { + summary: "subtask issue summary", + description: "description", + created: readonlyDate("2023-05-31T13:46:58.132+1000"), + project: { + key: "projectKey", + }, + assignee: { + displayName: "a really cool dude", + }, + timetracking: { + originalEstimateSeconds: 0, + timeSpentSeconds: 0, + remainingEstimateSeconds: 0, + originalEstimate: "0d", + timeSpent: "0d", + }, + fixVersions: [], + // eslint-disable-next-line spellcheck/spell-checker + aggregateprogress: { + progress: 120, + total: 120, + percent: 100, + }, + issuetype: { + name: "issue type", + subtask: true, + }, + status: { + id: "0", + name: "backlog", + statusCategory: { + id: 0, + name: "Backlog", + colorName: "yellow", + }, + }, + comment: undefined, + worklog: undefined, + duedate: undefined, + subtasks: [], + // eslint-disable-next-line spellcheck/spell-checker + aggregatetimeestimate: 0, + aggregatetimeoriginalestimate: 0, + aggregatetimespent: 0, + parent: { + id: "", + key: "", + }, + }, + changelog: { + histories: [], + }, + }, +]; From 8afd12baf2206cfbfd221584c7aa8b78de0a3ca1 Mon Sep 17 00:00:00 2001 From: Leon Zolati Date: Thu, 15 Jun 2023 17:12:52 +1000 Subject: [PATCH 04/11] test(jira.test): add a unit test to test for the subtasks worklog being added in jira.ts --- lib/src/services/jira.test.ts | 12 ++++ lib/src/services/jira_api.test.ts | 8 +-- lib/src/services/test_data/issue_data.ts | 74 +++++++++++++++++++++++- 3 files changed, 89 insertions(+), 5 deletions(-) diff --git a/lib/src/services/jira.test.ts b/lib/src/services/jira.test.ts index c6fec26..13a1fe6 100644 --- a/lib/src/services/jira.test.ts +++ b/lib/src/services/jira.test.ts @@ -6,6 +6,7 @@ import { enhancedIssue, CloudIssue, issueLastWorked, + mostRecentIssueWorklog, } from "./jira"; import reporter from "io-ts-reporters"; import * as E from "fp-ts/lib/Either"; @@ -361,3 +362,14 @@ describe("enhancing issues", () => { ); }); }); + +describe("Getting Most Recent Issue Worklog", () => { + it("should take that issues subtasks into account", () => { + expect( + mostRecentIssueWorklog(IssueData.enhancedIssue, [ + IssueData.enhancedIssue, + IssueData.enhancedSubtask, + ]) + ).toBeDefined(); + }); +}); diff --git a/lib/src/services/jira_api.test.ts b/lib/src/services/jira_api.test.ts index 35f4df6..3608fe6 100644 --- a/lib/src/services/jira_api.test.ts +++ b/lib/src/services/jira_api.test.ts @@ -52,8 +52,6 @@ describe("Searching issues", () => { it("should account for worklogs of subtasks in the parent", async () => { const response = await client.searchIssues("", [], "", "", [], {}); - expect(E.isRight(response)).toBeTruthy(); - pipe( response, E.map((issues) => { @@ -67,8 +65,10 @@ describe("Searching issues", () => { expect(issue.mostRecentWorklog).toBeDefined(); }); }), - //This should always throw an error in Jest - E.mapLeft((message) => expect(message).toThrow()) + E.mapLeft((error) => { + console.error(error); + expect(false).toBeTruthy(); + }) ); }); }); diff --git a/lib/src/services/test_data/issue_data.ts b/lib/src/services/test_data/issue_data.ts index 622a849..d9fd44e 100644 --- a/lib/src/services/test_data/issue_data.ts +++ b/lib/src/services/test_data/issue_data.ts @@ -42,7 +42,12 @@ export const issue: Issue = { total: 0, startAt: 0, }, - subtasks: [], + subtasks: [ + { + id: "ABC-124 id", + key: "ABC-124", + }, + ], duedate: undefined, worklog: { worklogs: [], @@ -78,3 +83,70 @@ export const worklog: IssueWorklog = { started: readonlyDate("2021-08-16T14:00:00.000Z"), timeSpentSeconds: 10800, }; + +export const Subtask: Issue = { + key: "ABC-124", + self: "self", + fields: { + summary: "summary", + description: "description", + created: readonlyDate("2020-01-01"), + project: { + key: "project", + }, + timetracking: {}, + fixVersions: [], + aggregateprogress: { + progress: undefined, + total: undefined, + percent: undefined, + }, + issuetype: { + name: "issue name", + subtask: true, + }, + assignee: { + name: "assignee", + }, + status: { + id: "id", + name: "status", + statusCategory: { + id: undefined, + name: undefined, + colorName: undefined, + }, + }, + comment: { + comments: [], + maxResults: 0, + total: 0, + startAt: 0, + }, + subtasks: [], + duedate: undefined, + worklog: { + worklogs: [worklog], + maxResults: 1, + total: 1, + startAt: 0, + }, + }, + changelog: { + histories: [], + }, +}; + +export const enhancedSubtask: EnhancedIssue = { + ...Subtask, + inProgress: true, + stalled: false, + waitingForReview: false, + closed: false, + released: false, + viewLink: "viewlink", + lastWorked: undefined, + quality: "A+++", + qualityReason: "for a great reason", + description: "description", +}; From 0292b940f3839185c66a7ef76a68637d42823381 Mon Sep 17 00:00:00 2001 From: Leon Zolati Date: Fri, 16 Jun 2023 10:10:10 +1000 Subject: [PATCH 05/11] style: disable functional/prefer-immutable-types rule --- cli/.eslintrc.js | 1 + cli/src/index.ts | 5 --- cli/src/scripts/auth.ts | 4 +- cli/src/scripts/rate.ts | 4 +- cli/src/scripts/search.ts | 24 ++++------- lib/.eslintrc.js | 1 + lib/src/codecs/null_or_missing.ts | 1 - lib/src/codecs/readonly_date.test.ts | 4 -- lib/src/codecs/readonly_date.ts | 3 -- lib/src/services/issue_checks.test.ts | 9 ---- lib/src/services/issue_checks.ts | 46 ++++++--------------- lib/src/services/issue_quality.ts | 2 - lib/src/services/jira.test.ts | 29 ++++++------- lib/src/services/jira.ts | 4 -- lib/src/services/jira_api.test.ts | 1 - lib/src/services/jira_api.ts | 2 - lib/src/services/jira_date_fns.ts | 8 +--- lib/src/services/test_data/issue_data.ts | 1 - lib/src/services/test_data/jira_api_data.ts | 1 - lib/src/services/test_data/jira_data.ts | 1 - 20 files changed, 40 insertions(+), 111 deletions(-) diff --git a/cli/.eslintrc.js b/cli/.eslintrc.js index 9ae186c..80b44bc 100644 --- a/cli/.eslintrc.js +++ b/cli/.eslintrc.js @@ -20,6 +20,7 @@ module.exports = { "total-functions", ], rules: { + "functional/prefer-immutable-types" : "off", // https://github.com/aotaduy/eslint-plugin-spellcheck "spellcheck/spell-checker": [ "warn", diff --git a/cli/src/index.ts b/cli/src/index.ts index 1c1a99e..b4493b3 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -28,7 +28,6 @@ export type RootCommand = typeof rootCommand; * Add global arguments here using the .option function. * E.g. const rootCommand = yargs.option('example', {type: 'string'}); */ -// eslint-disable-next-line functional/prefer-immutable-types const rootCommand = yargs; const CliConfig = T.readonly( @@ -298,7 +297,6 @@ const verifyClient = (builder: JiraClientBuilder): JiraClient => */ const config: CliConfig | undefined = findConfig(process.cwd()); -// eslint-disable-next-line functional/prefer-immutable-types export const withCommonOptions = (command: C) => command .option(jiraProtocolOptionKey, { @@ -329,13 +327,11 @@ export const withCommonOptions = (command: C) => }) .group([jiraHostOptionKey, jiraProtocolOptionKey], "Common Required:"); -// eslint-disable-next-line functional/prefer-immutable-types export const withAuthenticationOptions = (command: C) => withCommonOptions(command) .group([jiraConsumerKeyOptionKey, jiraConsumerSecretOptionKey], "Auth:") .demandOption([jiraHostOptionKey, jiraConsumerSecretOptionKey]); -// eslint-disable-next-line functional/prefer-immutable-types export const withAuthOptions = (command: C) => withCommonOptions(command) .option(jiraAccessTokenOptionKey, { @@ -407,7 +403,6 @@ export const withAuthOptions = (command: C) => ) .demandOption("jira"); -// eslint-disable-next-line functional/prefer-immutable-types export const withQualityFieldsOption = (command: C) => withAuthOptions(command) .option("qualityFieldName", { diff --git a/cli/src/scripts/auth.ts b/cli/src/scripts/auth.ts index 611b4f5..32aee3b 100644 --- a/cli/src/scripts/auth.ts +++ b/cli/src/scripts/auth.ts @@ -48,15 +48,13 @@ const auth = async ( : console.info(JSON.stringify(user.right, null, 2)); }; -// eslint-disable-next-line functional/prefer-immutable-types export default ({ command }: RootCommand): Argv => command( "auth", // eslint-disable-next-line spellcheck/spell-checker "authorises the linter to call Jira APIs and outputs the access token and secret", - // eslint-disable-next-line functional/prefer-immutable-types (yargs) => withAuthenticationOptions(yargs), - // eslint-disable-next-line functional/no-return-void, functional/prefer-immutable-types + // eslint-disable-next-line functional/no-return-void (args) => { const protocol = args["jira.protocol"]; // eslint-disable-next-line functional/no-expression-statements diff --git a/cli/src/scripts/rate.ts b/cli/src/scripts/rate.ts index 22258af..fa689e4 100644 --- a/cli/src/scripts/rate.ts +++ b/cli/src/scripts/rate.ts @@ -22,12 +22,10 @@ const rate = async ( console.log(`Updated [${JSON.stringify(update, null, 2)}]`); }; -// eslint-disable-next-line functional/prefer-immutable-types export default ({ command }: RootCommand): Argv => command( "rate", "records the quality of a jira issue", - // eslint-disable-next-line functional/prefer-immutable-types (yargs) => withQualityFieldsOption(yargs) .option("key", { @@ -45,7 +43,7 @@ export default ({ command }: RootCommand): Argv => describe: "reason for assessment", }) .demandOption(["key", "quality", "reason"]), - // eslint-disable-next-line functional/prefer-immutable-types, functional/no-return-void + // eslint-disable-next-line functional/no-return-void (args) => { // eslint-disable-next-line functional/no-expression-statements void rate( diff --git a/cli/src/scripts/search.ts b/cli/src/scripts/search.ts index 79b425a..d1534a4 100644 --- a/cli/src/scripts/search.ts +++ b/cli/src/scripts/search.ts @@ -28,20 +28,16 @@ type CheckedIssue = EnhancedIssue & { }; const checkedIssues = ( - // eslint-disable-next-line functional/prefer-immutable-types issues: readonly EnhancedIssue[] - // eslint-disable-next-line functional/prefer-immutable-types ): readonly CheckedIssue[] => { const now = readonlyDate(readonlyNow()); - // eslint-disable-next-line functional/prefer-immutable-types return issues.map((issue) => { const customChecks: readonly Check[] = [] as const; // TODO ability to dynamically load custom checks const issueAction = issueActionRequired(issue, now, customChecks); const issueQuality = quality(issueAction); - // eslint-disable-next-line functional/prefer-immutable-types const reasons: readonly string[] = issueAction.checks.flatMap((check) => check.outcome === "warn" || check.outcome === "fail" ? check.reasons : [] ); @@ -54,9 +50,9 @@ const checkedIssues = ( }); }; -// eslint-disable-next-line functional/no-return-void, functional/prefer-immutable-types +// eslint-disable-next-line functional/no-return-void const renderJson = (issues: readonly EnhancedIssue[]): void => { - // eslint-disable-next-line functional/no-return-void, functional/prefer-immutable-types + // eslint-disable-next-line functional/no-return-void checkedIssues(issues).forEach((issue) => // eslint-disable-next-line no-console console.log(JSON.stringify(issue, null, 2)) @@ -64,7 +60,6 @@ const renderJson = (issues: readonly EnhancedIssue[]): void => { }; const renderTable = ( - // eslint-disable-next-line functional/prefer-immutable-types issues: readonly EnhancedIssue[], qualityFieldName: string // eslint-disable-next-line functional/no-return-void @@ -90,7 +85,7 @@ const renderTable = ( // Simple visual representation of the degree of alarm a viewer should feel. // More whimsical emoji (e.g. 👀) raise some issues with rendering of wide // unicode characters. - // eslint-disable-next-line functional/prefer-immutable-types + const alarm = ["⠀", "⠁", "⠉", "⠋", "⠛", "⣿"] as const; const tableHeaderWidths: readonly number[] = tableHeaders.map( @@ -109,7 +104,6 @@ const renderTable = ( const data: readonly (readonly (readonly [ string, readonly clc.Format[] - // eslint-disable-next-line functional/prefer-immutable-types ])[])[] = checkedIssues(issues).map((issue) => { const originalEstimateSeconds = issue.fields.timetracking.originalEstimateSeconds ?? 0; @@ -161,9 +155,7 @@ const renderTable = ( ]; }); - // eslint-disable-next-line functional/prefer-immutable-types const calculatedWidths = data.reduce((previous, current) => { - // eslint-disable-next-line functional/prefer-immutable-types return current.map(([value], index) => Math.max(stringLength(value) + 1, previous[index] ?? 0) ); @@ -174,7 +166,7 @@ const renderTable = ( // eslint-disable-next-line functional/no-return-void ): void => { const initialValue: Readonly = new CLUI.Line(outputBuffer); - // eslint-disable-next-line functional/prefer-immutable-types + const columns: Readonly = row.reduce((line, [text], index) => { const columnWidth = calculatedWidths[index] ?? 0; return line.column(text, columnWidth); @@ -184,7 +176,7 @@ const renderTable = ( columns.fill().store(); }; - // eslint-disable-next-line functional/no-expression-statements, functional/prefer-immutable-types + // eslint-disable-next-line functional/no-expression-statements renderRow(tableHeaders.map((header) => [header, [clc.cyan]])); // eslint-disable-next-line functional/no-expression-statements @@ -209,7 +201,6 @@ const search = async ( // eslint-disable-next-line functional/no-expression-statements countdown.start(); - // eslint-disable-next-line functional/prefer-immutable-types const issues = await jira.searchIssues( jql, boardNamesToIgnore, @@ -233,12 +224,11 @@ type OutputMode = "json" | "table"; const DEFAULT_OUTPUT_MODE: OutputMode = "table"; -// eslint-disable-next-line functional/prefer-immutable-types export default ({ command }: RootCommand): Argv => command( "search", "searches for jira issues using JQL and then lints", - // eslint-disable-next-line functional/prefer-immutable-types + (yargs) => withQualityFieldsOption(yargs) .option("jql", { @@ -267,7 +257,7 @@ export default ({ command }: RootCommand): Argv => default: [], }) .demandOption(["jql"]), - // eslint-disable-next-line functional/no-return-void, functional/prefer-immutable-types + // eslint-disable-next-line functional/no-return-void (args) => { // eslint-disable-next-line functional/no-expression-statements void search( diff --git a/lib/.eslintrc.js b/lib/.eslintrc.js index cd2ce93..ef8c34c 100644 --- a/lib/.eslintrc.js +++ b/lib/.eslintrc.js @@ -20,6 +20,7 @@ module.exports = { "total-functions", ], rules: { + "functional/prefer-immutable-types" : "off", // https://github.com/aotaduy/eslint-plugin-spellcheck "spellcheck/spell-checker": [ "warn", diff --git a/lib/src/codecs/null_or_missing.ts b/lib/src/codecs/null_or_missing.ts index 0d32b74..de05ff5 100644 --- a/lib/src/codecs/null_or_missing.ts +++ b/lib/src/codecs/null_or_missing.ts @@ -6,6 +6,5 @@ import * as ITT from "io-ts-types"; * @param t the type of the property - if it is supplied and not null. * @returns a codec that will treat missing property or null value as undefined. */ -// eslint-disable-next-line functional/prefer-immutable-types export const nullOrMissingToUndefined = (t: T.Type) => T.readonly(ITT.fromNullable(T.union([t, T.undefined]), undefined)); diff --git a/lib/src/codecs/readonly_date.test.ts b/lib/src/codecs/readonly_date.test.ts index 495d14f..460853b 100644 --- a/lib/src/codecs/readonly_date.test.ts +++ b/lib/src/codecs/readonly_date.test.ts @@ -6,10 +6,8 @@ import fc from "fast-check"; describe("decoding a Date", () => { it("should decode a date to a readonly date", () => { fc.assert( - // eslint-disable-next-line functional/prefer-immutable-types fc.property(fc.date(), (d) => { // Given a date, - // When it is decoded to a readonly date. const actual = readonlyDateFromDate.decode(d); @@ -31,10 +29,8 @@ describe("decoding a Date", () => { describe("decoding a string", () => { it("should decode an ISO formatted string to a readonly date", () => { fc.assert( - // eslint-disable-next-line functional/prefer-immutable-types fc.property(fc.date(), (d) => { // Given a date, - // That has been encoded as a ISO string. const isoDate = ITT.DateFromISOString.encode(d); diff --git a/lib/src/codecs/readonly_date.ts b/lib/src/codecs/readonly_date.ts index 9788721..d4f0b60 100644 --- a/lib/src/codecs/readonly_date.ts +++ b/lib/src/codecs/readonly_date.ts @@ -5,17 +5,14 @@ import { readonlyDate, ReadonlyDate } from "readonly-types"; /** * Codec to convert between a `Date` and a `ReadonlyDate`. */ -// eslint-disable-next-line functional/prefer-immutable-types export const readonlyDateFromDate = new T.Type( "readonly date", (u): u is ReadonlyDate => u instanceof Date, - // eslint-disable-next-line functional/prefer-immutable-types (u, context) => typeof u === "string" ? ITT.DateFromISOString.decode(u) : u instanceof Date ? T.success(readonlyDate(u)) : T.failure(u, context, "Not a Date or string"), - // eslint-disable-next-line functional/prefer-immutable-types (a) => a.toISOString() ); diff --git a/lib/src/services/issue_checks.test.ts b/lib/src/services/issue_checks.test.ts index 5dad6e1..18a1e0d 100644 --- a/lib/src/services/issue_checks.test.ts +++ b/lib/src/services/issue_checks.test.ts @@ -45,7 +45,6 @@ describe("checking that in progress tickets have worklogs", () => { ] as const)( "checks as expected %#", (inProgress, lastWorklogCreated, checkTime, expected) => { - // eslint-disable-next-line functional/prefer-immutable-types const mostRecentWorklog = lastWorklogCreated === undefined ? undefined @@ -54,7 +53,6 @@ describe("checking that in progress tickets have worklogs", () => { started: lastWorklogCreated, }; - // eslint-disable-next-line functional/prefer-immutable-types const input = { ...IssueData.enhancedIssue, inProgress, @@ -75,7 +73,6 @@ describe("checking that in progress tickets have estimates", () => { [false, 0, { outcome: "not applied", reasons: ["not in progress"] }], [false, 10, { outcome: "not applied", reasons: ["not in progress"] }], ] as const)("checks as expected", (inProgress, estimate, expected) => { - // eslint-disable-next-line functional/prefer-immutable-types const input = { ...IssueData.enhancedIssue, inProgress, @@ -97,7 +94,6 @@ describe("checking that tickets have a description", () => { [undefined, { outcome: "fail", reasons: ["description is empty"] }], ["description", { outcome: "ok", reasons: ["description isn't empty"] }], ] as const)("checks as expected", (description, expected) => { - // eslint-disable-next-line functional/prefer-immutable-types const input = { ...IssueData.enhancedIssue, description, @@ -180,7 +176,6 @@ describe("checking comments", () => { now, expected ) => { - // eslint-disable-next-line functional/prefer-immutable-types const mostRecentComment = commentText !== undefined ? { @@ -194,7 +189,6 @@ describe("checking comments", () => { } : undefined; - // eslint-disable-next-line functional/prefer-immutable-types const input = { ...IssueData.enhancedIssue, fields: { @@ -240,7 +234,6 @@ describe("checking for tickets languishing in the backlog", () => { }, ], ] as const)("checks as expected", (created, column, now, expected) => { - // eslint-disable-next-line functional/prefer-immutable-types const input = { ...IssueData.enhancedIssue, fields: { @@ -279,7 +272,6 @@ describe("checking that dependencies have a due date", () => { { outcome: "ok", reasons: ["has a due date"] }, ], ] as const)("checks as expected", (issueTypeName, duedate, expected) => { - // eslint-disable-next-line functional/prefer-immutable-types const input = { ...IssueData.enhancedIssue, fields: { @@ -352,7 +344,6 @@ describe("checking that dependencies have not blown past the due date", () => { ] as const)( "checks as expected", (issueTypeName, duedate, now, closed, expected) => { - // eslint-disable-next-line functional/prefer-immutable-types const input = { ...IssueData.enhancedIssue, fields: { diff --git a/lib/src/services/issue_checks.ts b/lib/src/services/issue_checks.ts index eefc650..0e36fc0 100644 --- a/lib/src/services/issue_checks.ts +++ b/lib/src/services/issue_checks.ts @@ -1,4 +1,3 @@ -/* eslint functional/prefer-immutable-types: ["error", { "enforcement": "ReadonlyDeep" }] */ /* eslint-disable spellcheck/spell-checker */ /* eslint-disable functional/functional-parameters */ import { EnhancedIssue as EnhancedIssue } from "./jira"; @@ -17,14 +16,12 @@ import { ReadonlyNonEmptyArray } from "fp-ts/lib/ReadonlyNonEmptyArray"; import { differenceInBusinessHours } from "./jira_date_fns"; // TODO unclear why ReadonlyNonEmptyArray is not judged to be Immutable -// eslint-disable-next-line functional/type-declaration-immutability export type CheckResult = { readonly description: string; readonly outcome: "cant apply" | "not applied" | "ok" | "warn" | "fail"; readonly reasons: ReadonlyNonEmptyArray; }; -// eslint-disable-next-line functional/prefer-immutable-types export type Check = (issue: EnhancedIssue) => CheckResult; export type Action = "none" | "inspect"; @@ -42,25 +39,24 @@ export type Checker = { }; export const checker = (check: string): Checker => ({ - // eslint-disable-next-line functional/prefer-immutable-types fail: (reason: string) => ({ outcome: "fail", description: check, reasons: [reason], }), - // eslint-disable-next-line functional/prefer-immutable-types + ok: (reason: string) => ({ outcome: "ok", description: check, reasons: [reason], }), - // eslint-disable-next-line functional/prefer-immutable-types + na: (reason: string) => ({ outcome: "not applied", description: check, reasons: [reason], }), - // eslint-disable-next-line functional/prefer-immutable-types + cantApply: (reason: string) => ({ outcome: "cant apply", description: check, @@ -85,7 +81,6 @@ const notInProgressReason = "not in progress"; */ export const validateInProgressHasWorklog = (at: ReadonlyDate) => - // eslint-disable-next-line functional/prefer-immutable-types (issue: EnhancedIssue): CheckResult => { const check = checker("In progress tickets have been worked"); @@ -113,7 +108,6 @@ export const validateInProgressHasWorklog = * @returns result of checking the issue. */ export const validateDependenciesHaveDueDate = ( - // eslint-disable-next-line functional/prefer-immutable-types issue: EnhancedIssue ): CheckResult => { const check = checker("Dependencies have a due date"); @@ -135,7 +129,6 @@ export const validateDependenciesHaveDueDate = ( */ export const validateNotClosedDependenciesNotPassedDueDate = (at: ReadonlyDate) => - // eslint-disable-next-line functional/prefer-immutable-types (issue: EnhancedIssue): CheckResult => { const check = checker("Dependencies not passed due date"); @@ -164,7 +157,6 @@ export const validateNotClosedDependenciesNotPassedDueDate = }; export const validateInProgressHasEstimate = ( - // eslint-disable-next-line functional/prefer-immutable-types issue: EnhancedIssue ): CheckResult => { const check = checker("In Progress issues have estimates"); @@ -187,7 +179,7 @@ export const validateInProgressHasEstimate = ( }; // TODO check whether the description is a template and fail if it is. -// eslint-disable-next-line functional/prefer-immutable-types + export const validateDescription = (issue: EnhancedIssue): CheckResult => { const check = checker("Tickets have a description"); @@ -199,7 +191,6 @@ export const validateDescription = (issue: EnhancedIssue): CheckResult => { const validateNotStalledFor = (at: ReadonlyDate) => ( - // eslint-disable-next-line functional/prefer-immutable-types issue: EnhancedIssue, duration: number, durationDescription: string @@ -216,7 +207,7 @@ const validateNotStalledFor = ) .with( [true, not(undefined)], - // eslint-disable-next-line functional/prefer-immutable-types + ([, transition]) => differenceInBusinessDays(at.valueOf(), transition.valueOf()) > duration, @@ -229,19 +220,16 @@ const validateNotStalledFor = const validateNotStalledForMoreThanOneDay = (at: ReadonlyDate) => - // eslint-disable-next-line functional/prefer-immutable-types (issue: EnhancedIssue): CheckResult => validateNotStalledFor(at)(issue, 0, "one day"); const validateNotStalledForMoreThanOneWeek = (at: ReadonlyDate) => - // eslint-disable-next-line functional/prefer-immutable-types (issue: EnhancedIssue): CheckResult => validateNotStalledFor(at)(issue, 5, "one week"); const vaildateNotWaitingForReviewForMoreThanHalfADay = (at: ReadonlyDate) => - // eslint-disable-next-line functional/prefer-immutable-types (issue: EnhancedIssue): CheckResult => { const check = checker("issues not waiting for review for too long"); @@ -257,9 +245,9 @@ const vaildateNotWaitingForReviewForMoreThanHalfADay = ) .with( [true, not(undefined)], - // eslint-disable-next-line functional/prefer-immutable-types + ([, transition]) => differenceInBusinessHours(at, transition) > 4, - // eslint-disable-next-line functional/prefer-immutable-types + ([, transition]) => check.fail( `waiting for review for more than half a day (${differenceInBusinessHours( @@ -270,9 +258,9 @@ const vaildateNotWaitingForReviewForMoreThanHalfADay = ) .with( [true, not(undefined)], - // eslint-disable-next-line functional/prefer-immutable-types + ([, transition]) => differenceInBusinessHours(at, transition) <= 4, - // eslint-disable-next-line functional/prefer-immutable-types + ([, transition]) => check.ok( `waiting for review for less than half a day (${differenceInBusinessHours( @@ -285,7 +273,6 @@ const vaildateNotWaitingForReviewForMoreThanHalfADay = }; const validateInProgressNotCloseToEstimate = ( - // eslint-disable-next-line functional/prefer-immutable-types issue: EnhancedIssue ): CheckResult => { const check = checker( @@ -310,7 +297,6 @@ const validateInProgressNotCloseToEstimate = ( */ export const validateTooLongInBacklog = (at: ReadonlyDate) => - // eslint-disable-next-line functional/prefer-immutable-types (issue: EnhancedIssue): CheckResult => { const check = checker("issues don't stay in the backlog for too long"); const ageInMonths = differenceInCalendarMonths( @@ -325,7 +311,7 @@ export const validateTooLongInBacklog = .with([not("backlog"), __], () => check.na("not on the backlog")) .with( ["backlog", __], - // eslint-disable-next-line functional/prefer-immutable-types + ([, age]) => age > 3, () => check.fail(`in backlog for too long [${ageInMonths} months]`) ) @@ -335,7 +321,6 @@ export const validateTooLongInBacklog = // TODO check sub-tasks for comments? export const validateComment = (at: ReadonlyDate) => - // eslint-disable-next-line functional/prefer-immutable-types (issue: EnhancedIssue): CheckResult => { const check = checker("issues that have been worked have comments"); @@ -363,11 +348,11 @@ export const validateComment = ) .with( [not(undefined), __, false, __], - // eslint-disable-next-line functional/prefer-immutable-types + ([recentCommentTime, inProgress, , loggedTime]) => isBefore(recentCommentTime, lastBusinessDay(at).valueOf()) && (inProgress || loggedTime > 0), - // eslint-disable-next-line functional/prefer-immutable-types + ([recentCommentTime]) => { const commentAge = formatDistance(recentCommentTime, at.valueOf()); return check.fail( @@ -379,9 +364,7 @@ export const validateComment = }; const check = ( - // eslint-disable-next-line functional/prefer-immutable-types issue: EnhancedIssue, - // eslint-disable-next-line functional/prefer-immutable-types checks: readonly ((t: EnhancedIssue) => CheckResult)[] ): IssueAction => { const noAction: IssueAction = { @@ -389,17 +372,14 @@ const check = ( checks: [], }; - // eslint-disable-next-line functional/prefer-immutable-types return checks.reduceRight((issueAction, check) => { const result = check(issue); - const actionRequired: Action = match([ result, ]) .with([{ outcome: "warn" }], () => "inspect") .with([{ outcome: "fail" }], () => "inspect") .otherwise(() => issueAction.actionRequired); - return { actionRequired, checks: issueAction.checks.concat(result), @@ -415,7 +395,6 @@ const check = ( * @returns true if the issue deserves some grace, otherwise false. */ export const issueDeservesGrace = ( - // eslint-disable-next-line functional/prefer-immutable-types issue: EnhancedIssue, now: ReadonlyDate ): boolean => { @@ -436,7 +415,6 @@ export const issueDeservesGrace = ( * @returns whether action is required, and the checks that were run to form that recommendation. */ export const issueActionRequired = ( - // eslint-disable-next-line functional/prefer-immutable-types issue: EnhancedIssue, now: ReadonlyDate, customChecks: readonly Check[] diff --git a/lib/src/services/issue_quality.ts b/lib/src/services/issue_quality.ts index bf5a02d..ed6e218 100644 --- a/lib/src/services/issue_quality.ts +++ b/lib/src/services/issue_quality.ts @@ -1,5 +1,3 @@ -/* eslint functional/prefer-immutable-types: ["error", { "enforcement": "ReadonlyDeep" }] */ - import { IssueAction } from "./issue_checks"; /** diff --git a/lib/src/services/jira.test.ts b/lib/src/services/jira.test.ts index 13a1fe6..cef0b05 100644 --- a/lib/src/services/jira.test.ts +++ b/lib/src/services/jira.test.ts @@ -89,7 +89,7 @@ describe("decoding well-formed tickets", () => { // Given a well-formed bit of data. // When it is decoded. - // eslint-disable-next-line functional/prefer-immutable-types + const actual = CloudIssue.decode(data); // Then no errors should be reported. @@ -111,7 +111,6 @@ describe("decoding well-formed tickets", () => { }); describe("finding the most recent work date", () => { - // eslint-disable-next-line functional/prefer-immutable-types const issueWithTransition = { ...IssueData.issue, changelog: { @@ -119,7 +118,6 @@ describe("finding the most recent work date", () => { }, } as const; - // eslint-disable-next-line functional/prefer-immutable-types const issueWithComment = { ...IssueData.issue, fields: { @@ -133,7 +131,6 @@ describe("finding the most recent work date", () => { }, } as const; - // eslint-disable-next-line functional/prefer-immutable-types const issueWithWorklog = { ...IssueData.issue, fields: { @@ -147,7 +144,6 @@ describe("finding the most recent work date", () => { }, } as const; - // eslint-disable-next-line functional/prefer-immutable-types const issueWithEverything = { ...IssueData.issue, fields: { @@ -178,10 +174,9 @@ describe("finding the most recent work date", () => { ["with all three", issueWithEverything, mixedChangeFrom2022.created], ] as const)( "should returned for expected result for an issue %s", - // eslint-disable-next-line functional/prefer-immutable-types + (_desc, issue, expected) => { // Given an issue. - // When the time it was last worked is found. const lastWorked = issueLastWorked(issue, []); @@ -199,7 +194,7 @@ describe("finding the most recent comment", () => { [[commentFrom2000, commentFrom2021, commentFrom2000], commentFrom2021], ] as const)("should be found as expected", (comments, expected) => { // Given an issue with the provided comments - // eslint-disable-next-line functional/prefer-immutable-types + const issue = { ...IssueData.issue, fields: { @@ -238,7 +233,7 @@ describe("finding transitions", () => { ], ] as const)("should filter %s as expected", (_desc, histories, expected) => { // Given some change histories. - // eslint-disable-next-line functional/prefer-immutable-types + const issue = { ...IssueData.issue, changelog: { @@ -278,7 +273,7 @@ describe("finding the most recent transition", () => { ], ] as const)("should be found as expected", (histories, expected) => { // Given an issue with the provided changelogs - // eslint-disable-next-line functional/prefer-immutable-types + const issue = { ...IssueData.issue, changelog: { @@ -302,7 +297,7 @@ describe("enhancing issues", () => { fc.string({ minLength: 1 }).filter((s) => s.trim().length > 0), (fieldName, value) => { // Given an issue that has the provided value - // eslint-disable-next-line functional/prefer-immutable-types + const issue = { ...IssueData.issue, fields: { @@ -312,7 +307,7 @@ describe("enhancing issues", () => { }; // When it is enhanced - // eslint-disable-next-line functional/prefer-immutable-types + const enhanced = enhancedIssue( issue, [], @@ -335,7 +330,7 @@ describe("enhancing issues", () => { fc.string({ minLength: 1 }).filter((s) => s.trim().length > 0), (fieldName, value) => { // Given an issue that has the provided value - // eslint-disable-next-line functional/prefer-immutable-types + const issue = { ...IssueData.issue, fields: { @@ -345,7 +340,7 @@ describe("enhancing issues", () => { }; // When it is enhanced - // eslint-disable-next-line functional/prefer-immutable-types + const enhanced = enhancedIssue( issue, [], @@ -365,6 +360,12 @@ describe("enhancing issues", () => { describe("Getting Most Recent Issue Worklog", () => { it("should take that issues subtasks into account", () => { + // There are no worklogs on the enhanced issue and thus should be undefined + expect(mostRecentIssueWorklog(IssueData.enhancedIssue, [])).toBeUndefined(); + // There are worklogs on the subtask and thus should be defined + expect(mostRecentIssueWorklog(IssueData.enhancedSubtask, [])).toBeDefined(); + // Then, when calculating the worklog for a task with no worklogs but with a + // subtask that has worklogs, it should be defined. expect( mostRecentIssueWorklog(IssueData.enhancedIssue, [ IssueData.enhancedIssue, diff --git a/lib/src/services/jira.ts b/lib/src/services/jira.ts index c956524..19c0947 100644 --- a/lib/src/services/jira.ts +++ b/lib/src/services/jira.ts @@ -1,4 +1,3 @@ -/* eslint-disable functional/prefer-immutable-types */ /* eslint-disable spellcheck/spell-checker */ import * as T from "io-ts"; import * as ITT from "io-ts-types"; @@ -262,13 +261,10 @@ export type CloudIssue = Readonly>; // eslint-disable-next-line functional/type-declaration-immutability export type Issue = Readonly>; -// eslint-disable-next-line functional/type-declaration-immutability export type IssueComment = Readonly>; -// eslint-disable-next-line functional/type-declaration-immutability export type IssueChangeLog = Readonly>; -// eslint-disable-next-line functional/type-declaration-immutability export type IssueWorklog = Readonly>; export const BoardColumn = T.readonly( diff --git a/lib/src/services/jira_api.test.ts b/lib/src/services/jira_api.test.ts index 3608fe6..60e7932 100644 --- a/lib/src/services/jira_api.test.ts +++ b/lib/src/services/jira_api.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable functional/prefer-immutable-types */ import * as E from "fp-ts/lib/Either"; import JiraApi from "jira-client"; import { JiraClient, jiraClient } from "./jira_api"; diff --git a/lib/src/services/jira_api.ts b/lib/src/services/jira_api.ts index 958b590..7b69a87 100644 --- a/lib/src/services/jira_api.ts +++ b/lib/src/services/jira_api.ts @@ -1,5 +1,4 @@ /* eslint-disable spellcheck/spell-checker */ -/* eslint-disable functional/prefer-immutable-types*/ import { Either } from "fp-ts/lib/Either"; import * as E from "fp-ts/lib/Either"; import * as TE from "fp-ts/lib/TaskEither"; @@ -41,7 +40,6 @@ type FieldNotEditable = { readonly fields: readonly string[]; }; -// eslint-disable-next-line functional/type-declaration-immutability export type JiraClient = { readonly jiraApi: Readonly; readonly updateIssueQuality: ( diff --git a/lib/src/services/jira_date_fns.ts b/lib/src/services/jira_date_fns.ts index 4030900..07c9881 100644 --- a/lib/src/services/jira_date_fns.ts +++ b/lib/src/services/jira_date_fns.ts @@ -1,4 +1,3 @@ -/* eslint-next-line functional/prefer-immutable-types: ["error", { "enforcement": "ReadonlyDeep" }] */ import { differenceInBusinessDays, differenceInMinutes, @@ -48,7 +47,6 @@ export const jiraFormattedDuration = (duration: Readonly): string => { * @returns duration in a Jira-like format. */ export const jiraFormattedSeconds = (seconds: number): string => { - // eslint-disable-next-line functional/prefer-immutable-types const units = (n: number, unit: number): readonly [number, number] => { const numberOfUnits = Math.floor(n / unit); const remainder = n - numberOfUnits * unit; @@ -87,9 +85,8 @@ export const jiraFormattedSeconds = (seconds: number): string => { * @returns duration in a Jira-like format. */ export const jiraFormattedDistance = ( - // eslint-disable-next-line functional/prefer-immutable-types from: ReadonlyDate, - // eslint-disable-next-line functional/prefer-immutable-types + to: ReadonlyDate ): string => { const duration: Readonly = intervalToDuration({ @@ -107,9 +104,8 @@ export const jiraFormattedDistance = ( * @returns the number of business hours in the interval. */ export const differenceInBusinessHours = ( - // eslint-disable-next-line functional/prefer-immutable-types to: ReadonlyDate, - // eslint-disable-next-line functional/prefer-immutable-types + from: ReadonlyDate ) => { const businessDays = differenceInBusinessDays(to.getTime(), from.getTime()); diff --git a/lib/src/services/test_data/issue_data.ts b/lib/src/services/test_data/issue_data.ts index d9fd44e..dba33a0 100644 --- a/lib/src/services/test_data/issue_data.ts +++ b/lib/src/services/test_data/issue_data.ts @@ -1,4 +1,3 @@ -/* eslint-disable functional/prefer-immutable-types */ /* eslint-disable spellcheck/spell-checker */ import type { EnhancedIssue, Issue, IssueWorklog } from "../jira"; import { readonlyDate } from "readonly-types"; diff --git a/lib/src/services/test_data/jira_api_data.ts b/lib/src/services/test_data/jira_api_data.ts index da2393e..c6351d6 100644 --- a/lib/src/services/test_data/jira_api_data.ts +++ b/lib/src/services/test_data/jira_api_data.ts @@ -20,7 +20,6 @@ export const boardReturn: Board = { }, }; -// eslint-disable-next-line functional/prefer-immutable-types export const searchJiraReturn: CloudIssue[] = [ { key: "parent key", diff --git a/lib/src/services/test_data/jira_data.ts b/lib/src/services/test_data/jira_data.ts index 6a5a5a7..dd1c059 100644 --- a/lib/src/services/test_data/jira_data.ts +++ b/lib/src/services/test_data/jira_data.ts @@ -1,4 +1,3 @@ -/* eslint functional/prefer-immutable-types: ["error", { "enforcement": "ReadonlyDeep" }] */ /* eslint-disable spellcheck/spell-checker */ /* eslint-disable sonarjs/no-duplicate-string */ export const nullDescription = { From d2ec840e15a8e53e4dbbf44a772b7bae4ea001fd Mon Sep 17 00:00:00 2001 From: Leon Zolati Date: Fri, 16 Jun 2023 10:21:02 +1000 Subject: [PATCH 06/11] style: rename issues variable and update the typedocs --- lib/src/services/jira.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/src/services/jira.ts b/lib/src/services/jira.ts index 19c0947..258072f 100644 --- a/lib/src/services/jira.ts +++ b/lib/src/services/jira.ts @@ -440,15 +440,16 @@ export const mostRecentIssueComment = ( /** * Finds the most recent worklog on the issue (based on the started date). * @param issue the issue whose most recent worklog should be found. + * @param issueSubtasks a list of issues that must contain the subtasks of issue. * @returns the most recent worklog, or undefined if no work has been logged. */ export const mostRecentIssueWorklog = ( issue: Issue, - issues: readonly Issue[] + issueSubtasks: readonly Issue[] ): IssueWorklog | undefined => { const subtaskWorklogs = issue.fields.subtasks.reduce( (acc: IssueWorklog[], subtask) => { - const subtaskIssue = issues.find( + const subtaskIssue = issueSubtasks.find( (subtaskIssue) => subtaskIssue.key === subtask.key ); const work = subtaskIssue?.fields.worklog?.worklogs; @@ -471,17 +472,18 @@ export const mostRecentIssueWorklog = ( * Determines the time at which the issue was last worked, as evidenced by * comments, transitions or worklogs. * @param issue the issue. + * @param issueSubtasks a list of issues that must contain the subtasks of issue. * @returns the time that the issue was last worked, or undefined if it has never been worked. */ export const issueLastWorked = ( issue: Issue, - issues: readonly Issue[] + issueSubtasks: readonly Issue[] ): ReadonlyDate | undefined => { const mostRecentTransition = mostRecentIssueTransition(issue); const mostRecentComment = mostRecentIssueComment(issue); - const mostRecentWorklog = mostRecentIssueWorklog(issue, issues); + const mostRecentWorklog = mostRecentIssueWorklog(issue, issueSubtasks); return [ mostRecentTransition?.created, @@ -494,7 +496,7 @@ export const issueLastWorked = ( export const enhancedIssue = ( issue: Issue, - issues: readonly Issue[], + issueSubtasks: readonly Issue[], viewLink: string, qualityFieldName: string, qualityReasonFieldName: string, @@ -507,7 +509,7 @@ export const enhancedIssue = ( const released = issue.fields.fixVersions.some((version) => version.released); - const lastWorked = issueLastWorked(issue, issues); + const lastWorked = issueLastWorked(issue, issueSubtasks); // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const quality = issue.fields[qualityFieldName] as string | undefined; From cca6508030d79b5b6c845b85b7183ed72fef02ad Mon Sep 17 00:00:00 2001 From: Leon Zolati Date: Fri, 16 Jun 2023 11:55:52 +1000 Subject: [PATCH 07/11] style: use mockResolvedValue, reduce complexity of searchIssues, and other stylistic changes --- lib/src/services/jira_api.test.ts | 34 +---- lib/src/services/jira_api.ts | 155 +++++++++++--------- lib/src/services/test_data/jira_api_data.ts | 134 ++++++----------- 3 files changed, 141 insertions(+), 182 deletions(-) diff --git a/lib/src/services/jira_api.test.ts b/lib/src/services/jira_api.test.ts index 60e7932..c3f478d 100644 --- a/lib/src/services/jira_api.test.ts +++ b/lib/src/services/jira_api.test.ts @@ -8,19 +8,11 @@ import { pipe } from "fp-ts/lib/function"; jest.mock("jira-client", () => { return jest.fn().mockImplementation(() => { return { - searchJira: jest - .fn() - .mockImplementation(() => - Promise.resolve({ issues: searchJiraReturn }) - ), + searchJira: jest.fn().mockResolvedValue({ issues: searchJiraReturn }), getAllBoards: jest .fn() - .mockImplementation(() => - Promise.resolve({ values: [{ id: 0, name: "0" }] }) - ), - getConfiguration: jest - .fn() - .mockImplementation(() => Promise.resolve(boardReturn)), + .mockResolvedValue({ values: [{ id: 0, name: "0" }] }), + getConfiguration: jest.fn().mockResolvedValue(boardReturn), genericGet: jest.fn().mockImplementation((input: string) => { if (input.includes("comment")) return Promise.resolve({ comments: [] }); else @@ -53,21 +45,11 @@ describe("Searching issues", () => { pipe( response, - E.map((issues) => { - issues.forEach((issue) => { - if (issue.key.includes("parent")) - // As the worklog is not defined in the mocked JQL search call - // nor are they defined in the mocked worklog API call for the parent - // the only way this can be defined is if the worklogs - // of the subtask is taken into account when you call search issues. - // eslint-disable-next-line jest/no-conditional-expect - expect(issue.mostRecentWorklog).toBeDefined(); - }); - }), - E.mapLeft((error) => { - console.error(error); - expect(false).toBeTruthy(); - }) + E.map((list) => + list.forEach((issue) => expect(issue.mostRecentWorklog).toBeDefined()) + ) ); + + expect.assertions(2); }); }); diff --git a/lib/src/services/jira_api.ts b/lib/src/services/jira_api.ts index 7b69a87..f18d5c5 100644 --- a/lib/src/services/jira_api.ts +++ b/lib/src/services/jira_api.ts @@ -529,6 +529,86 @@ export const jiraClient = ( const issueLink = (issue: Issue): string => `${jiraProtocol}://${jiraHost}/browse/${encodeURIComponent(issue.key)}`; + /** + * Adds the most recent comment to a given enhanced issue + * @param issue the EnhancedIssue without a comment + * @returns the EnhancedIssue with the mostRecentComment added + */ + const issueWithComment = ( + issue: EnhancedIssue + ): TE.TaskEither => { + const mostRecentCommentLoaded = + issue.fields.comment !== undefined && + issue.fields.comment.total === issue.fields.comment.comments.length; + + const recentComment = ( + worklogs: readonly IssueComment[] | undefined + ): IssueComment | undefined => + worklogs === undefined + ? undefined + : [...worklogs].sort((w1, w2) => + compareDesc(w1.created.valueOf(), w2.created.valueOf()) + )[0]; + + return pipe( + mostRecentCommentLoaded + ? TE.right(issue.fields.comment.comments) + : fetchMostRecentComments(issue.key), + TE.map((comments) => ({ + ...issue, + mostRecentComment: recentComment(comments), + })) + ); + }; + + /** + * Adds the most recent worklog to a given enhanced issue by either calculating + * that from the worklogs provided in the initial get request if that was not paginated + * or by making additional API requests to get all the worklogs for an issue. The worklogs + * for an issue also account for the worklogs of the subtasks of that issue. + * @param issue the EnhancedIssue to add worklogs to + * @returns the EnhancedIssue with the mostRecentWorklog added + */ + const issueWithWorklog = ( + issue: EnhancedIssue + ): TE.TaskEither => { + const mostRecentWorklogLoaded = + issue.fields.worklog !== undefined && + issue.fields.worklog.total === issue.fields.worklog.worklogs.length; + + const recentWorklog = ( + worklogs: readonly IssueWorklog[] | undefined + ): IssueWorklog | undefined => + worklogs === undefined + ? undefined + : [...worklogs].sort((w1, w2) => + compareDesc(w1.started.valueOf(), w2.started.valueOf()) + )[0]; + + const relevantKeys: string[] = mostRecentWorklogLoaded + ? [] + : issue.fields.subtasks.map((cur) => cur.key).concat([issue.key]); + + const worklogs: TE.TaskEither[] = + relevantKeys.map((key) => fetchMostRecentWorklogs(key)); + + const worklog: TE.TaskEither = pipe( + worklogs, + TE.sequenceArray, + TE.map((x): readonly IssueWorklog[] => x.flat()) + ); + + return pipe( + mostRecentWorklogLoaded + ? TE.right(issue.fields.worklog.worklogs) + : worklog, + TE.map((worklogs) => ({ + ...issue, + mostRecentWorklog: recentWorklog(worklogs), + })) + ); + }; + return { jiraApi, /** @@ -623,7 +703,6 @@ export const jiraClient = ( qualityReasonField: string, customFieldNames: readonly string[], descriptionFields: ReadonlyRecord - // eslint-disable-next-line sonarjs/cognitive-complexity ): Promise> => { const fetchIssues = TE.tryCatch( // eslint-disable-next-line functional/functional-parameters @@ -734,80 +813,16 @@ export const jiraClient = ( })(boardsByProject(issues, boardNamesToIgnore)); }; - const issueWithComment = ( - issue: EnhancedIssue - ): TE.TaskEither => { - const mostRecentCommentLoaded = - issue.fields.comment !== undefined && - issue.fields.comment.total === issue.fields.comment.comments.length; - - const recentComment = ( - worklogs: readonly IssueComment[] | undefined - ): IssueComment | undefined => - worklogs === undefined - ? undefined - : [...worklogs].sort((w1, w2) => - compareDesc(w1.created.valueOf(), w2.created.valueOf()) - )[0]; - - return pipe( - mostRecentCommentLoaded - ? TE.right(issue.fields.comment.comments) - : fetchMostRecentComments(issue.key), - TE.map((comments) => ({ - ...issue, - mostRecentComment: recentComment(comments), - })) - ); - }; - - const issueWithWorklog = ( - issue: EnhancedIssue - ): TE.TaskEither => { - const mostRecentWorklogLoaded = - issue.fields.worklog !== undefined && - issue.fields.worklog.total === issue.fields.worklog.worklogs.length; - - const recentWorklog = ( - worklogs: readonly IssueWorklog[] | undefined - ): IssueWorklog | undefined => - worklogs === undefined - ? undefined - : [...worklogs].sort((w1, w2) => - compareDesc(w1.started.valueOf(), w2.started.valueOf()) - )[0]; - - const relevantKeys: string[] = mostRecentWorklogLoaded - ? [] - : issue.fields.subtasks - .reduce((acc: string[], cur) => acc.concat([cur.key]), []) - .concat([issue.key]); - - const worklogs: TE.TaskEither[] = - relevantKeys.map((key) => fetchMostRecentWorklogs(key)); - - const worklog: TE.TaskEither = pipe( - worklogs, - TE.sequenceArray, - TE.map((x): readonly IssueWorklog[] => x.flat()) - ); - - return pipe( - mostRecentWorklogLoaded - ? TE.right(issue.fields.worklog.worklogs) - : worklog, - TE.map((worklogs) => ({ - ...issue, - mostRecentWorklog: recentWorklog(worklogs), - })) - ); - }; - return pipe( + // Get the issues from Jira that match the provided JQL query. fetchIssues, + // Then parse and convert the JSON response from the API into the Issue type. TE.chain(convertIssueType), + // Then convert the Issues into EnhancedIssues by adding information about the board TE.chain(enhancedIssues), + // Then add the most recent comment to those EnhancedIssues TE.chain(TE.traverseSeqArray((issue) => issueWithComment(issue))), + // Then add the most recent worklog to those EnhancedIssues TE.chain(TE.traverseSeqArray((issue) => issueWithWorklog(issue))) )(); }, diff --git a/lib/src/services/test_data/jira_api_data.ts b/lib/src/services/test_data/jira_api_data.ts index c6351d6..87264d3 100644 --- a/lib/src/services/test_data/jira_api_data.ts +++ b/lib/src/services/test_data/jira_api_data.ts @@ -20,64 +20,68 @@ export const boardReturn: Board = { }, }; +const baseFields = { + summary: "summary", + description: "description", + created: readonlyDate("2023-05-31T13:46:58.132+1000"), + project: { + key: "projectKey", + }, + assignee: { + displayName: "a really cool person", + }, + timetracking: { + originalEstimateSeconds: 0, + timeSpentSeconds: 0, + remainingEstimateSeconds: 0, + originalEstimate: "0d", + timeSpent: "0d", + }, + fixVersions: [], + // eslint-disable-next-line spellcheck/spell-checker + aggregateprogress: { + progress: 120, + total: 120, + percent: 100, + }, + status: { + id: "0", + name: "backlog", + statusCategory: { + id: 0, + name: "Backlog", + colorName: "yellow", + }, + }, + comment: undefined, + worklog: undefined, + duedate: undefined, + // eslint-disable-next-line spellcheck/spell-checker + aggregatetimeestimate: 0, + aggregatetimeoriginalestimate: 0, + aggregatetimespent: 0, + parent: { + id: "", + key: "", + }, +}; + export const searchJiraReturn: CloudIssue[] = [ { key: "parent key", self: "parent url atlassian", fields: { - summary: "parent issue summary", - description: "description", - created: readonlyDate("2023-05-31T13:46:58.132+1000"), - project: { - key: "projectKey", - }, - assignee: { - displayName: "a really cool dude", - }, - timetracking: { - originalEstimateSeconds: 0, - timeSpentSeconds: 0, - remainingEstimateSeconds: 0, - originalEstimate: "0d", - timeSpent: "0d", - }, - fixVersions: [], - // eslint-disable-next-line spellcheck/spell-checker - aggregateprogress: { - progress: 120, - total: 120, - percent: 100, - }, + ...baseFields, issuetype: { name: "issue type", subtask: false, }, - status: { - id: "0", - name: "backlog", - statusCategory: { - id: 0, - name: "Backlog", - colorName: "yellow", - }, - }, - comment: undefined, - worklog: undefined, - duedate: undefined, subtasks: [ { id: "subtask id", key: "subtask key", }, ], - // eslint-disable-next-line spellcheck/spell-checker - aggregatetimeestimate: 0, - aggregatetimeoriginalestimate: 0, - aggregatetimespent: 0, - parent: { - id: "", - key: "", - }, }, changelog: { histories: [], @@ -87,54 +91,12 @@ export const searchJiraReturn: CloudIssue[] = [ key: "subtask key", self: "subtask url atlassian", fields: { - summary: "subtask issue summary", - description: "description", - created: readonlyDate("2023-05-31T13:46:58.132+1000"), - project: { - key: "projectKey", - }, - assignee: { - displayName: "a really cool dude", - }, - timetracking: { - originalEstimateSeconds: 0, - timeSpentSeconds: 0, - remainingEstimateSeconds: 0, - originalEstimate: "0d", - timeSpent: "0d", - }, - fixVersions: [], - // eslint-disable-next-line spellcheck/spell-checker - aggregateprogress: { - progress: 120, - total: 120, - percent: 100, - }, + ...baseFields, issuetype: { name: "issue type", subtask: true, }, - status: { - id: "0", - name: "backlog", - statusCategory: { - id: 0, - name: "Backlog", - colorName: "yellow", - }, - }, - comment: undefined, - worklog: undefined, - duedate: undefined, subtasks: [], - // eslint-disable-next-line spellcheck/spell-checker - aggregatetimeestimate: 0, - aggregatetimeoriginalestimate: 0, - aggregatetimespent: 0, - parent: { - id: "", - key: "", - }, }, changelog: { histories: [], From 7c9f7214ae1c0a89637bf35f717e8e43d51642ae Mon Sep 17 00:00:00 2001 From: Leon Zolati Date: Fri, 16 Jun 2023 12:19:19 +1000 Subject: [PATCH 08/11] style: further reduce the complexity of searchIssues and other stylistic changes --- lib/src/services/jira_api.ts | 90 +++++++++-------- lib/src/services/test_data/issue_data.ts | 119 ++++++++--------------- 2 files changed, 87 insertions(+), 122 deletions(-) diff --git a/lib/src/services/jira_api.ts b/lib/src/services/jira_api.ts index f18d5c5..568a956 100644 --- a/lib/src/services/jira_api.ts +++ b/lib/src/services/jira_api.ts @@ -529,6 +529,50 @@ export const jiraClient = ( const issueLink = (issue: Issue): string => `${jiraProtocol}://${jiraHost}/browse/${encodeURIComponent(issue.key)}`; + const convertIssueType = ( + response: Readonly + ): TE.TaskEither => { + return jiraHost.includes("atlassian") + ? pipe( + parseCloudJira(response), //response to cloudIssueType + TE.chain( + TE.traverseSeqArray((cloudIssue) => cloudJiraToGeneric(cloudIssue)) + ) + ) + : pipe( + parseOnPremJira(response), + TE.chain( + TE.traverseSeqArray((onPremIssue) => + onPremJiraToGeneric(onPremIssue) + ) + ) + ); + }; + + const parseOnPremJira = ( + response: Readonly + ): TE.TaskEither => + TE.fromEither( + decode( + "issues", //name + response.issues, //input + // eslint-disable-next-line @typescript-eslint/unbound-method + T.readonly(T.array(OnPremIssue)).decode //decoder + ) + ); + + const parseCloudJira = ( + response: Readonly + ): TE.TaskEither => + TE.fromEither( + decode( + "issues", //name + response.issues, //input + // eslint-disable-next-line @typescript-eslint/unbound-method + T.readonly(T.array(CloudIssue)).decode //decoder) + ) + ); + /** * Adds the most recent comment to a given enhanced issue * @param issue the EnhancedIssue without a comment @@ -741,52 +785,6 @@ export const jiraClient = ( )}].` ); - const convertIssueType = ( - response: Readonly - ): TE.TaskEither => { - return jiraHost.includes("atlassian") - ? pipe( - parseCloudJira(response), //response to cloudIssueType - TE.chain( - TE.traverseSeqArray((cloudIssue) => - cloudJiraToGeneric(cloudIssue) - ) - ) - ) - : pipe( - parseOnPremJira(response), - TE.chain( - TE.traverseSeqArray((onPremIssue) => - onPremJiraToGeneric(onPremIssue) - ) - ) - ); - }; - - const parseOnPremJira = ( - response: Readonly - ): TE.TaskEither => - TE.fromEither( - decode( - "issues", //name - response.issues, //input - // eslint-disable-next-line @typescript-eslint/unbound-method - T.readonly(T.array(OnPremIssue)).decode //decoder - ) - ); - - const parseCloudJira = ( - response: Readonly - ): TE.TaskEither => - TE.fromEither( - decode( - "issues", //name - response.issues, //input - // eslint-disable-next-line @typescript-eslint/unbound-method - T.readonly(T.array(CloudIssue)).decode //decoder) - ) - ); - const enhancedIssues = ( issues: readonly Issue[] ): TE.TaskEither => { diff --git a/lib/src/services/test_data/issue_data.ts b/lib/src/services/test_data/issue_data.ts index dba33a0..fef8883 100644 --- a/lib/src/services/test_data/issue_data.ts +++ b/lib/src/services/test_data/issue_data.ts @@ -2,58 +2,62 @@ import type { EnhancedIssue, Issue, IssueWorklog } from "../jira"; import { readonlyDate } from "readonly-types"; +const baseFields = { + summary: "summary", + description: "description", + created: readonlyDate("2020-01-01"), + project: { + key: "project", + }, + timetracking: {}, + fixVersions: [], + aggregateprogress: { + progress: undefined, + total: undefined, + percent: undefined, + }, + assignee: { + name: "assignee", + }, + status: { + id: "id", + name: "status", + statusCategory: { + id: undefined, + name: undefined, + colorName: undefined, + }, + }, + comment: { + comments: [], + maxResults: 0, + total: 0, + startAt: 0, + }, + duedate: undefined, + worklog: { + worklogs: [], + maxResults: 0, + total: 0, + startAt: 0, + }, +}; + export const issue: Issue = { key: "ABC-123", self: "self", fields: { - summary: "summary", - description: "description", - created: readonlyDate("2020-01-01"), - project: { - key: "project", - }, - timetracking: {}, - fixVersions: [], - aggregateprogress: { - progress: undefined, - total: undefined, - percent: undefined, - }, + ...baseFields, issuetype: { name: "issue name", subtask: false, }, - assignee: { - name: "assignee", - }, - status: { - id: "id", - name: "status", - statusCategory: { - id: undefined, - name: undefined, - colorName: undefined, - }, - }, - comment: { - comments: [], - maxResults: 0, - total: 0, - startAt: 0, - }, subtasks: [ { id: "ABC-124 id", key: "ABC-124", }, ], - duedate: undefined, - worklog: { - worklogs: [], - maxResults: 0, - total: 0, - startAt: 0, - }, }, changelog: { histories: [], @@ -87,49 +91,12 @@ export const Subtask: Issue = { key: "ABC-124", self: "self", fields: { - summary: "summary", - description: "description", - created: readonlyDate("2020-01-01"), - project: { - key: "project", - }, - timetracking: {}, - fixVersions: [], - aggregateprogress: { - progress: undefined, - total: undefined, - percent: undefined, - }, + ...baseFields, issuetype: { name: "issue name", subtask: true, }, - assignee: { - name: "assignee", - }, - status: { - id: "id", - name: "status", - statusCategory: { - id: undefined, - name: undefined, - colorName: undefined, - }, - }, - comment: { - comments: [], - maxResults: 0, - total: 0, - startAt: 0, - }, subtasks: [], - duedate: undefined, - worklog: { - worklogs: [worklog], - maxResults: 1, - total: 1, - startAt: 0, - }, }, changelog: { histories: [], From 26a5c14f8604c4faad192564546acb31a910300f Mon Sep 17 00:00:00 2001 From: Leon Zolati Date: Fri, 16 Jun 2023 14:21:37 +1000 Subject: [PATCH 09/11] test(jira_api.test.ts): fix the test to better show the expected behaviour of the function --- lib/src/services/jira_api.test.ts | 52 ++++++++++++++++++------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/lib/src/services/jira_api.test.ts b/lib/src/services/jira_api.test.ts index c3f478d..d02a4ba 100644 --- a/lib/src/services/jira_api.test.ts +++ b/lib/src/services/jira_api.test.ts @@ -4,6 +4,14 @@ import { JiraClient, jiraClient } from "./jira_api"; import { boardReturn, searchJiraReturn } from "./test_data/jira_api_data"; import { readonlyDate } from "readonly-types"; import { pipe } from "fp-ts/lib/function"; +import { IssueWorklog } from "./jira"; + +const worklog: IssueWorklog = { + author: { name: "Jim" }, + started: readonlyDate("2023-05-31T13:46:58.132+1000"), + timeSpentSeconds: 1, + comment: "I did a thing", +}; jest.mock("jira-client", () => { return jest.fn().mockImplementation(() => { @@ -13,22 +21,17 @@ jest.mock("jira-client", () => { .fn() .mockResolvedValue({ values: [{ id: 0, name: "0" }] }), getConfiguration: jest.fn().mockResolvedValue(boardReturn), - genericGet: jest.fn().mockImplementation((input: string) => { - if (input.includes("comment")) return Promise.resolve({ comments: [] }); - else - return input.includes("parent") - ? Promise.resolve({ worklogs: [] }) - : Promise.resolve({ - worklogs: [ - { - author: { name: "Jim" }, - started: readonlyDate("2023-05-31T13:46:58.132+1000"), - timeSpentSeconds: 1, - comment: "I did a thing", - }, - ], - }); - }), + genericGet: jest + .fn() + .mockResolvedValueOnce({ comments: [] }) + .mockResolvedValueOnce({ comments: [] }) + .mockResolvedValueOnce({ + worklogs: [worklog], + }) + .mockResolvedValueOnce({ worklogs: [] }) + .mockResolvedValueOnce({ + worklogs: [worklog], + }), }; }); }); @@ -43,13 +46,20 @@ describe("Searching issues", () => { it("should account for worklogs of subtasks in the parent", async () => { const response = await client.searchIssues("", [], "", "", [], {}); - pipe( + const worklogsFromPipe = pipe( response, - E.map((list) => - list.forEach((issue) => expect(issue.mostRecentWorklog).toBeDefined()) - ) + E.map((list) => list.flatMap((issue) => issue.mostRecentWorklog)) ); - expect.assertions(2); + expect(E.isRight(worklogsFromPipe)).toBeTruthy(); + + //We know that worklogsFromPipe is right but to compile... + const worklogs: unknown[] = E.isRight(worklogsFromPipe) + ? worklogsFromPipe.right + : []; + + worklogs.forEach((log) => expect(log).toStrictEqual(worklog)); + + expect.assertions(3); }); }); From 5fd19be8635789deedbe9de9a3b938effd6cde13 Mon Sep 17 00:00:00 2001 From: Leon Zolati Date: Fri, 16 Jun 2023 14:33:03 +1000 Subject: [PATCH 10/11] docs: add a small comment to describe the assumptions made in the test to a reader --- lib/src/services/jira_api.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/src/services/jira_api.test.ts b/lib/src/services/jira_api.test.ts index d02a4ba..f73d584 100644 --- a/lib/src/services/jira_api.test.ts +++ b/lib/src/services/jira_api.test.ts @@ -53,11 +53,16 @@ describe("Searching issues", () => { expect(E.isRight(worklogsFromPipe)).toBeTruthy(); - //We know that worklogsFromPipe is right but to compile... + // We know that worklogsFromPipe is right from the above but a conditional + // is required to compile const worklogs: unknown[] = E.isRight(worklogsFromPipe) ? worklogsFromPipe.right : []; + // We should expect that both the parent and subtask's most recent + // work log are 'worklog' due to the fact that the worklog in subtasks + // should be accounted for when calculating the most recent worklog for + // a given task. worklogs.forEach((log) => expect(log).toStrictEqual(worklog)); expect.assertions(3); From 329175bd24cd1efd0531c78035737bae3989b9d5 Mon Sep 17 00:00:00 2001 From: Leon Zolati Date: Fri, 16 Jun 2023 17:34:39 +1000 Subject: [PATCH 11/11] test: update jira_api tests to be more readable -> this is not a good implementation --- lib/src/services/jira_api.test.ts | 141 +++++++++++++++----- lib/src/services/test_data/jira_api_data.ts | 2 +- 2 files changed, 109 insertions(+), 34 deletions(-) diff --git a/lib/src/services/jira_api.test.ts b/lib/src/services/jira_api.test.ts index f73d584..4eac2f5 100644 --- a/lib/src/services/jira_api.test.ts +++ b/lib/src/services/jira_api.test.ts @@ -1,70 +1,145 @@ import * as E from "fp-ts/lib/Either"; import JiraApi from "jira-client"; import { JiraClient, jiraClient } from "./jira_api"; -import { boardReturn, searchJiraReturn } from "./test_data/jira_api_data"; +import { boardReturn, issuesWithSubtask } from "./test_data/jira_api_data"; import { readonlyDate } from "readonly-types"; -import { pipe } from "fp-ts/lib/function"; -import { IssueWorklog } from "./jira"; +import { EnhancedIssue, IssueWorklog } from "./jira"; const worklog: IssueWorklog = { author: { name: "Jim" }, - started: readonlyDate("2023-05-31T13:46:58.132+1000"), + started: readonlyDate("2023-05-30T13:46:58.132+1000"), timeSpentSeconds: 1, comment: "I did a thing", }; +const worklogNew: IssueWorklog = { + author: { name: "Mary" }, + started: readonlyDate("2023-05-31T13:46:58.132+1000"), + timeSpentSeconds: 1, + comment: "I did a thing but later", +}; + +// eslint-disable-next-line functional/no-let +let testNumber = 0; + +const determineTestCase = (input: string) => { + switch (testNumber) { + case 0: + return { worklogs: [] }; + case 1: + return input.includes("parent") + ? { worklogs: [] } + : { worklogs: [worklog] }; + case 2: + return input.includes("parent") + ? { worklogs: [worklog] } + : { worklogs: [] }; + case 3: + return input.includes("parent") + ? { worklogs: [worklog] } + : { worklogs: [worklogNew] }; + case 4: + return input.includes("parent") + ? { worklogs: [worklogNew] } + : { worklogs: [worklog] }; + default: + return {}; + } +}; + jest.mock("jira-client", () => { return jest.fn().mockImplementation(() => { return { - searchJira: jest.fn().mockResolvedValue({ issues: searchJiraReturn }), + searchJira: jest.fn().mockResolvedValue({ issues: issuesWithSubtask }), getAllBoards: jest .fn() .mockResolvedValue({ values: [{ id: 0, name: "0" }] }), getConfiguration: jest.fn().mockResolvedValue(boardReturn), - genericGet: jest - .fn() - .mockResolvedValueOnce({ comments: [] }) - .mockResolvedValueOnce({ comments: [] }) - .mockResolvedValueOnce({ - worklogs: [worklog], - }) - .mockResolvedValueOnce({ worklogs: [] }) - .mockResolvedValueOnce({ - worklogs: [worklog], - }), + genericGet: jest.fn().mockImplementation((input: string) => { + const response = input.includes("comment") + ? { comments: [] } + : determineTestCase(input); + return Promise.resolve(response); + }), }; }); }); -describe("Searching issues", () => { +describe("Calculating the mostRecentWorklog", () => { const mockApi: Readonly = new JiraApi({ host: "host.atlassian.net", }); const client: JiraClient = jiraClient("https", "host.atlassian.net", mockApi); - it("should account for worklogs of subtasks in the parent", async () => { + it("should return undefined when the task has no worklog and subtask has no worklog", async () => { + testNumber = 0; + const response = await client.searchIssues("", [], "", "", [], {}); - const worklogsFromPipe = pipe( - response, - E.map((list) => list.flatMap((issue) => issue.mostRecentWorklog)) - ); + const issues: readonly EnhancedIssue[] = E.isRight(response) + ? response.right + : []; - expect(E.isRight(worklogsFromPipe)).toBeTruthy(); + // eslint-disable-next-line sonarjs/no-duplicate-string + const parent = issues.find((issue) => issue.key === "parent key"); + expect(parent).toBeDefined(); + expect(parent?.mostRecentWorklog).toBeUndefined(); + }); - // We know that worklogsFromPipe is right from the above but a conditional - // is required to compile - const worklogs: unknown[] = E.isRight(worklogsFromPipe) - ? worklogsFromPipe.right + it("should return the subtasks worklog when the task has no worklog and subtask has a worklog", async () => { + testNumber = 1; + + const response = await client.searchIssues("", [], "", "", [], {}); + + const issues: readonly EnhancedIssue[] = E.isRight(response) + ? response.right : []; - // We should expect that both the parent and subtask's most recent - // work log are 'worklog' due to the fact that the worklog in subtasks - // should be accounted for when calculating the most recent worklog for - // a given task. - worklogs.forEach((log) => expect(log).toStrictEqual(worklog)); + const parent = issues.find((issue) => issue.key === "parent key"); + expect(parent).toBeDefined(); + expect(parent?.mostRecentWorklog).toStrictEqual(worklog); + }); + + it("should return the tasks worklog when the task has a worklog and subtask has no worklog", async () => { + testNumber = 2; + + const response = await client.searchIssues("", [], "", "", [], {}); + + const issues: readonly EnhancedIssue[] = E.isRight(response) + ? response.right + : []; + + const parent = issues.find((issue) => issue.key === "parent key"); + expect(parent).toBeDefined(); + expect(parent?.mostRecentWorklog).toStrictEqual(worklog); + }); + + it("should return the subtasks worklog when the task has a worklog and subtask has a newer worklog", async () => { + testNumber = 3; + + const response = await client.searchIssues("", [], "", "", [], {}); + + const issues: readonly EnhancedIssue[] = E.isRight(response) + ? response.right + : []; + + const parent = issues.find((issue) => issue.key === "parent key"); + expect(parent).toBeDefined(); + expect(parent?.mostRecentWorklog).toStrictEqual(worklogNew); + }); + + it("should return the tasks worklog when the task has a worklog and subtask has a older worklog", async () => { + testNumber = 4; + + const response = await client.searchIssues("", [], "", "", [], {}); + + const issues: readonly EnhancedIssue[] = E.isRight(response) + ? response.right + : []; - expect.assertions(3); + const parent = issues.find((issue) => issue.key === "parent key"); + expect(parent).toBeDefined(); + expect(parent?.mostRecentWorklog).toStrictEqual(worklogNew); }); }); diff --git a/lib/src/services/test_data/jira_api_data.ts b/lib/src/services/test_data/jira_api_data.ts index 87264d3..d5f77a5 100644 --- a/lib/src/services/test_data/jira_api_data.ts +++ b/lib/src/services/test_data/jira_api_data.ts @@ -66,7 +66,7 @@ const baseFields = { }, }; -export const searchJiraReturn: CloudIssue[] = [ +export const issuesWithSubtask: CloudIssue[] = [ { key: "parent key", self: "parent url atlassian",