aiotests-playwright-reporter
Version:
AIO Tests reporter for reporting results from Playwright to AIO Tests
717 lines (662 loc) • 25.8 kB
text/typescript
import AioTestsLogger from "./AioTestsLogger";
import {
AioConfig,
BulkRequestBody,
CreateCycleBody,
CustomFieldUpdate,
CustomFieldUpdateData,
TestData,
TestDataEntry,
} from "./model";
import axios, { AxiosInstance, AxiosResponse, isAxiosError} from "axios";
import fs from "fs";
import FormData from "form-data";
import { TestCase, TestResult } from "@playwright/test/reporter";
const baseUrl = "https://tcms.aiojiraapps.com/aio-tcms/api/v1";
const restVersion = "/rest/aio-tcms-api/1.0";
const rateLimitWaitTime = 60 * 1000;
const apiTimeout = 45 * 1000;
const CREATE_IF_ABSENT = "CREATE_IF_ABSENT";
const createNewCycleOptions = [true, false, "true", "false", CREATE_IF_ABSENT];
const bulkRequestBody: BulkRequestBody = { testRuns: [] };
const browserKeyMap: Map<string, string[]> = new Map();
const browserBulkRequestMap: Map<String, BulkRequestBody> = new Map();
let debugMode = false;
let aioAPIClient: AxiosInstance | null;
let isAttachmentAPIAvailable: boolean | null = null;
let stopReporting: boolean = false;
let allCaseKeys: string[] = [];
let allFailedRuns: string[] = [];
let bulkTestRunUpdate = false;
let multiBrowser = false;
async function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function initAPIClient(aioConfig: AioConfig) {
debugMode = !!aioConfig.debugMode;
AioTestsLogger.setDebugMode(debugMode);
AioTestsLogger.debug("Debug Mode is set to true");
if (aioConfig.cloud || process.env.AIO_API_KEY) {
if (!aioConfig.cloud.apiKey && !process.env.AIO_API_KEY) {
AioTestsLogger.error(
'Cloud hosting config does not specify apiKey. Please add "cloud":{"apiKey":"auth token"}',
debugMode
);
} else {
let apiKey = process.env.AIO_API_KEY
? process.env.AIO_API_KEY
: aioConfig.cloud.apiKey;
aioAPIClient = axios.create({
baseURL: baseUrl,
timeout: apiTimeout,
headers: { Authorization: `AioAuth ${apiKey}` },
});
}
return;
} else if (aioConfig.hosted) {
if (!!!aioConfig.hosted.jiraUrl) {
AioTestsLogger.error(
'Server hosting config does not specify jiraUrl. Please add "server":{"jiraUrl":"https://companyhostedjira.com/jira"}'
);
} else {
aioAPIClient = axios.create({
baseURL: aioConfig.hosted.jiraUrl + restVersion,
timeout: apiTimeout,
});
if (aioConfig.hosted.jiraUsername || process.env.JIRA_USERNAME) {
let jUsername = process.env.JIRA_USERNAME
? process.env.JIRA_USERNAME
: aioConfig.hosted.jiraUsername;
let jPassword = process.env.JIRA_PASSWORD
? process.env.JIRA_PASSWORD
: aioConfig.hosted.jiraPassword;
aioAPIClient.defaults.auth = {
username: jUsername,
password: jPassword,
};
} else if (aioConfig.hosted.jiraPAT || process.env.JIRA_PAT) {
let jPAT = process.env.JIRA_PAT
? process.env.JIRA_PAT
: aioConfig.hosted.jiraPAT;
aioAPIClient.defaults.headers.common[
"Authorization"
] = `Bearer ${jPAT}`;
} else {
AioTestsLogger.error(
"Server hosting config missing Jira username or PAT. " +
' Please set JIRA_USERNAME/JIRA_PASSWORD or JIRA_PAT as an environment variable or add "hosted":{"jiraUrl":"yoururl", "jiraUsername":"un", "jiraPassword":"pwd"}' +
' or "hosted":{"jiraUrl":"your url", "jiraPAT":"PAT token"} to config file'
);
aioAPIClient = null;
}
}
return;
}
AioTestsLogger.error(
"No authentication information found. Please set AIO_API_KEY/JIRA_USERNAME/JIRA_PAT as an environment variable or " +
'add "cloud":{"apiKey":"auth token"} or "server":{"jiraServerUrl":"val"}) to config'
);
}
export const validateAIOConfig = (config: AioConfig, reportError: boolean): AioConfig | undefined => {
if (!!config.enableReporting) {
if (!!!config.jiraProjectId && !!reportError) {
AioTestsLogger.error(
"Jira Project Id is mandatory for AIO Tests Reporting.",
true
);
return;
}
initAPIClient(config);
if (!aioAPIClient) {
AioTestsLogger.error(
"Please specify valid credentials to connect with AIO Tests."
);
return;
}
return config;
} else {
if (reportError)
AioTestsLogger.error(
"AIO Tests reporting is not enabled. Please set env:{aioTests:{enableReporting:true}}",
true
);
}
return undefined;
};
const getOrCreateCycle = async (aioConfig: AioConfig): Promise<string | void> => {
if (!aioAPIClient) {
return Promise.resolve();
}
AioTestsLogger.logStartEnd("Determining cycle to update");
if (!aioConfig.cycleDetails) {
AioTestsLogger.error(
'Please specify cycleDetails in config. eg. "cycleDetails": {"cycleKey":"AT-CY-11"}',
true
);
return Promise.resolve();
} else {
let aioCycleConfig = aioConfig.cycleDetails;
if (
aioCycleConfig.createNewCycle &&
!createNewCycleOptions.includes(aioCycleConfig.createNewCycle)
) {
return Promise.resolve(
`Invalid value for createNewCycle "${aioCycleConfig.createNewCycle}". Valid values include ${createNewCycleOptions}.`
);
}
if ((!aioCycleConfig.createNewCycle || "false" === aioCycleConfig.createNewCycle) && aioCycleConfig.cycleKey) {
aioCycleConfig["cycleKeyToReportTo"] = aioCycleConfig.cycleKey;
return Promise.resolve();
}
if (aioCycleConfig.createNewCycle === true || "true" === aioCycleConfig.createNewCycle || aioCycleConfig.createNewCycle === CREATE_IF_ABSENT) {
let cycleTitle = aioCycleConfig.cycleName;
let customFields = aioCycleConfig.customFields;
if (!cycleTitle) {
return Promise.resolve("createNewCycle is set to " + aioCycleConfig.createNewCycle + " in config. Please set cycleName.");
}
if (aioCycleConfig.createNewCycle === CREATE_IF_ABSENT) {
let results: any = await findExistingCycleThroughName(aioConfig);
if (results === true) {
AioTestsLogger.debug("Existing cycle found");
//Cycle found and set.
return;
}
if (results !== false) {
//Error while finding cycle.
AioTestsLogger.log(results);
return;
}
}
AioTestsLogger.log("Creating cycle : " + cycleTitle);
let folderCreationPromise = getOrCreateFolder(
aioConfig.jiraProjectId,
aioCycleConfig
);
return folderCreationPromise
.then(async (folderCreationResponse) => {
AioTestsLogger.debug("Folder task resolved. Creating cycle.");
let createCycleBody: CreateCycleBody = {
title: cycleTitle,
customFields: customFields || null,
};
if (folderCreationResponse) {
createCycleBody.folder = folderCreationResponse.data;
}
if (aioCycleConfig.tasks && aioCycleConfig.tasks.length > 0 && Array.isArray(aioCycleConfig.tasks)) {
let jiraTasks = aioCycleConfig.tasks.filter((f) => f && !!f.trim());
if (jiraTasks.length > 0) {
createCycleBody.jiraTaskIDs = jiraTasks;
}
}
AioTestsLogger.debug("Cycle endpoint " + "/project/" + aioConfig.jiraProjectId + "/testcycle/detail");
try {
let response = await aioAPIClient!
.post(
"/project/" + aioConfig.jiraProjectId + "/testcycle/detail",
createCycleBody
);
aioCycleConfig["cycleKeyToReportTo"] = response.data.key;
AioTestsLogger.log("Cycle created successfully : " + aioCycleConfig.cycleKeyToReportTo);
} catch (error: any) {
debugLogError(error);
if (error.response) {
if (
error.response.status === 401 ||
error.response.status === 403
) {
return Promise.resolve(
"Authorization error. Please check credentials."
);
} else {
return Promise.resolve(
error.response.status + " : " + error.response.data
);
}
}
}
})
.catch((error: any) => {
debugLogError(error);
if (error.response) {
AioTestsLogger.error(
error.response.status + " : " + error.response.data
);
}
return Promise.resolve(
"Error in fetching or creating cycle folder. " +
'Please check format of folder, for eg. ["Cloud","Release1"]'
);
});
}
return Promise.resolve(
'createNewCycle is false in config. Please specify a cycle key (eg. AT-CY-11) as "cycleKey":"AT-CY=11" or cycle name as "cycleName":"Regression release 1" '
);
}
};
async function getOrCreateFolder(jiraProjectId: string, aioCycleConfig: { folder?: string[] }): Promise<any> {
if (aioCycleConfig.folder && Array.isArray(aioCycleConfig.folder) && aioCycleConfig.folder.length > 0) {
const userFolderHierarchy = aioCycleConfig.folder.filter(
(folder) => folder && folder.trim()
);
if (userFolderHierarchy.length > 0) {
AioTestsLogger.log(`Creating or fetching folder: ${userFolderHierarchy}`);
return aioAPIClient!.put(
`/project/${jiraProjectId}/testcycle/folder/hierarchy`,
{
folderHierarchy: userFolderHierarchy,
}
);
}
} else {
AioTestsLogger.debug("No cycle folder information set.");
}
return Promise.resolve();
}
async function findExistingCycleThroughName(aioConfig: AioConfig): Promise<boolean | string> {
if (aioConfig.parallelBuild && aioConfig.parallelBuild.masterBuild === false) {
let to = aioConfig.parallelBuild.waitForSeconds
? aioConfig.parallelBuild.waitForSeconds
: 2;
AioTestsLogger.log(`Waiting for ${to} seconds for master build to finish`);
await new Promise((resolve) => setTimeout(resolve, to * 1000));
}
let body = {
title: {
comparisonType: "EXACT_MATCH",
value: aioConfig.cycleDetails.cycleName!.trim(),
},
};
AioTestsLogger.log("Finding cycle with name : " + aioConfig.cycleDetails.cycleName!.trim());
return aioAPIClient!
.post(`/project/${aioConfig.jiraProjectId}/testcycle/search`, body)
.then(function (response) {
const items = response?.data?.items;
if (items && items.length > 0) {
aioConfig.cycleDetails["cycleKeyToReportTo"] = items[0]?.key;
return true;
} else {
return false;
}
})
.catch(function (error) {
debugLogError(error);
if (error.response) {
if (error.response.status === 401 || error.response.status === 403) {
return Promise.resolve(
"Authorization error. Please check credentials."
);
} else {
return Promise.resolve(
error.response.status + " : " + error.response.data
);
}
}
return Promise.resolve(
"An unknown error occurred while searching for the cycle."
);
});
}
function getCustomFieldValueToUpdate(customFieldsToUpdate: CustomFieldUpdate[]): CustomFieldUpdateData[] {
const cfUpdates: CustomFieldUpdateData[] = [];
if (customFieldsToUpdate) {
customFieldsToUpdate.forEach((cf) => {
const data: CustomFieldUpdateData = {
customValue: { name: cf.name, value: cf.value },
};
if (cf.operationType) {
data.customFieldUpdateOperationType = cf.operationType;
}
cfUpdates.push(data);
});
}
return cfUpdates;
}
const replaceProjectNameForRunFields = (customFieldUpdates: CustomFieldUpdateData[] | null, projectName: string): CustomFieldUpdateData[] | null => {
if (customFieldUpdates == null) {
return null;
}
return customFieldUpdates.map((customFieldUpdate) =>
replaceProjectName(customFieldUpdate, projectName)
);
};
const replaceProjectName = (customFieldUpdate: CustomFieldUpdateData, projectName: string): CustomFieldUpdateData => {
const { value } = customFieldUpdate.customValue;
let newValue;
if (typeof value === "string") {
// Case: value is a string
newValue = value.replace(/\$project/g, projectName);
} else if (Array.isArray(value)) {
if (value.every((item) => typeof item === "string")) {
// Case: value is a string[]
newValue = value.map((item) => item.replace(/\$project/g, projectName));
} else if (value.every((item) => typeof item === "object" && item !== null && "value" in item)) {
// Case: value is a { value: string }[]
newValue = value.map((item) => ({
value: (item as { value: string }).value.replace(/\$project/g, projectName),
}));
} else {
throw new Error("Invalid array structure in value field");
}
} else if (typeof value === "object" && value !== null) {
if ("value" in value) {
// Case: value is { value: string }
newValue = { value: value.value.replace(/\$project/g, projectName) };
} else {
throw new Error("Invalid object structure in value field");
}
} else {
newValue = value;
}
return {
customValue: { name: customFieldUpdate.customValue.name, value: newValue },
customFieldUpdateOperationType:
customFieldUpdate.customFieldUpdateOperationType,
};
};
function debugLogError(error: any): void {
if (debugMode) {
AioTestsLogger.error("*** AIO Debug Mode Error Reporting ***");
AioTestsLogger.errorObj(error.message);
if (error.response) {
const status = error.response.status;
const data = error.response.data;
AioTestsLogger.errorObj(`${status} ${data}`);
} else {
AioTestsLogger.errorObj(error);
}
AioTestsLogger.error("*** AIO Debug Mode Error Reporting End ***");
}
}
function getAIORunStatus(playwrightStatus: String): string {
switch (playwrightStatus) {
case "failed":
return "Failed";
case "passed":
return "Passed";
default:
return "Not Run";
}
}
async function uploadScreenshot(path: string|undefined, jiraProjectId: string, cyclekey: string, runId: string, trialCounter: number = 0): Promise<void> {
if(path) {
const form = new FormData();
form.append("file", fs.createReadStream(path));
try {
await aioAPIClient!.post(
`/project/${jiraProjectId}/testcycle/${cyclekey}/testrun/${runId}/attachment`,
form,
{
headers: form.getHeaders(),
}
);
AioTestsLogger.log(`Screenshot uploaded: ${path}`);
} catch (error: any) {
if (error.response) {
if (error.response.status === 429 && trialCounter < 3) {
AioTestsLogger.log("Reached AIO rate limits. Pausing...");
await sleep(rateLimitWaitTime);
return uploadScreenshot(
path,
jiraProjectId,
cyclekey,
runId,
trialCounter + 1
);
} else if (error.response.status === 404) {
AioTestsLogger.error(
"Attachment API is not supported in the current API version and hence attachments could not be uploaded. Please upgrade to the latest version of AIO Tests."
);
isAttachmentAPIAvailable = false;
}
} else {
if (error.data) {
AioTestsLogger.error(error.data);
} else {
AioTestsLogger.error(error.code);
}
}
}
}
}
async function initCycleDetails(aioConfig: AioConfig) {
try {
const data = await getOrCreateCycle(aioConfig);
if (aioConfig.cycleDetails.cycleKeyToReportTo) {
AioTestsLogger.log("Reporting results to cycle : " + aioConfig.cycleDetails.cycleKeyToReportTo);
} else {
stopReporting = true;
if (typeof data === "string") {
AioTestsLogger.error(data);
}
}
} catch (err) {
if (err instanceof Error) {
AioTestsLogger.error("An error occurred: " + err.message);
} else {
AioTestsLogger.error("An unknown error occurred");
}
stopReporting = true;
}
}
export const reportTestResults = async function (config: AioConfig, testSummary: TestData[], multiBrowsers: boolean): Promise<void> {
if(testSummary && testSummary.length) {
await initCycleDetails(config);
if (!aioAPIClient || stopReporting) {
return Promise.resolve();
}
multiBrowser = multiBrowsers;
const cfUpdates = config.runDetails? getCustomFieldValueToUpdate(config.runDetails.customFieldsToUpdate) : null;
let index = 0;
const total = testSummary.length;
AioTestsLogger.logStartEnd("Reporting results");
for (const {result, test} of testSummary) {
const isLast = index === total - 1;
await reportSpecResults(config, result, test, cfUpdates);
if (isLast && bulkTestRunUpdate) {
await bulkUpdateResult(config);
}
index++;
}
}else {
AioTestsLogger.log("No case keys found in specs");
}
};
const reportSpecResults = async function (config: AioConfig, results: TestResult, testCase: TestCase, cfUpdates: CustomFieldUpdateData[]|null): Promise<void> {
const testData = await findResults(results, testCase);
const passedCaseKeys: string[] = [];
const failedCaseKeys: string[] = [];
const targetKeys =
results.status === "passed" ? passedCaseKeys : failedCaseKeys;
targetKeys.push(...testData.keys());
allCaseKeys.push(...targetKeys);
if (failedCaseKeys.length > 0) {
AioTestsLogger.log("*".repeat(5) + " Updating failed cases " + "*".repeat(5));
try {
await postFailedResult(failedCaseKeys, testData, config, cfUpdates);
} catch (err) {
debugLogError(err);
}
}
await addDataForBulkUpdateResult(passedCaseKeys, testData, config, cfUpdates);
};
async function postFailedResult(failedCaseKeys: string[], testData: Map<string, TestDataEntry>, config: AioConfig, cfUpdates: CustomFieldUpdateData[]|null): Promise<void> {
return failedCaseKeys.reduce<Promise<void>>((promiseChain, caseKey) => {
const attemptData = testData.get(caseKey);
return promiseChain.then(async () => {
await postResult(config, caseKey, attemptData!, attemptData!.screenshot!, cfUpdates);
});
}, Promise.resolve());
}
async function findResults(results: TestResult, testCase: TestCase): Promise<Map<string, TestDataEntry>> {
let testData: Map<string, TestDataEntry> = new Map<string, TestDataEntry>();
let pattern = new RegExp("\\w+-TC-\\d+", "gi");
let tcKeys: string[] = [];
let match;
testCase.tags.forEach((tag) => {
do {
match = pattern.exec(tag);
if (match) {
tcKeys.push(match[0]);
}
} while (match != null);
});
if (tcKeys.length) {
tcKeys.forEach((tcKey) => {
// @ts-ignore
const comments = results.error || multiBrowser ? generateComments(testCase._projectId ? testCase._projectId : "", results.error) : null;
// @ts-ignore
testData.set(tcKey, {
testId: tcKey,
duration: results.duration / 1000,
testStatus: getAIORunStatus(results.status),
retires: results.retry,
comments: comments,
screenshot: results.attachments
.filter((attachment) => attachment.contentType.startsWith("image"))
.map((attachment) => attachment.path)
.filter((path) => path !== undefined),
// @ts-ignore
browser: testCase._projectId!
});
});
}
return testData;
}
function generateComments(browserName: string, error?: { message?: string; snippet?: string; stack?: string; value?: string; }): string[] {
return [
multiBrowser ? `Browser: ${browserName}` : undefined,
error?.message ? `Message: ${error.message}` : undefined,
error?.stack ? `Stack: ${error.stack}` : undefined,
].filter(Boolean) as string[];
}
async function addDataForBulkUpdateResult(passedCaseKeys: string[], testDataMap: Map<string, TestDataEntry>, config: AioConfig, cfUpdates: CustomFieldUpdateData[]|null): Promise<void> {
for (const passedCaseKey of passedCaseKeys) {
const testData = testDataMap.get(passedCaseKey);
if (!testData) continue;
const testRun = {
testCaseKey: passedCaseKey,
testRunStatus: testData.testStatus,
effort: testData.duration,
isAutomated: true,
comments: testData.comments,
customFieldValueToUpdate:
config.runDetails
? replaceProjectNameForRunFields(cfUpdates, testData.browser)
: cfUpdates,
};
if (multiBrowser) {
const existingRequest = browserBulkRequestMap.get(testData.browser);
if (existingRequest) {
existingRequest.testRuns.push(testRun);
} else {
browserBulkRequestMap.set(testData.browser, { testRuns: [testRun] });
}
} else {
bulkRequestBody.testRuns.push(testRun);
}
bulkTestRunUpdate = true;
}
}
async function bulkUpdateResult(aioConfig: AioConfig, trialCounter = 0): Promise<void> {
const { jiraProjectId, cycleDetails, addNewRun } = aioConfig;
const url = `/project/${jiraProjectId}/testcycle/${cycleDetails.cycleKeyToReportTo}/bulk/testrun/update?createNewRun=${!!addNewRun}`;
AioTestsLogger.log("*".repeat(5) + " Updating all passed cases " + "*".repeat(5));
AioTestsLogger.debug(`Reporting results in bulk: ${url}`);
try {
const processResponse = (response: AxiosResponse, browser?: String) => {
const { successCount, errorCount, errors } = response.data;
AioTestsLogger.log(`Successfully reported ${successCount} passed cases${browser ? ` for browser: ${browser}` : ""}.`);
if (errorCount > 0) {
AioTestsLogger.error("Failures in reporting passed cases:");
Object.entries(errors).forEach(([key, error]) => {
const errorMessage =
(error as { message?: string }).message || "Unknown error";
AioTestsLogger.error(`${key}: ${errorMessage}`);
});
}
};
if (multiBrowser) {
for (const [browser, bulkRequestBodyBrowser] of browserBulkRequestMap) {
const response = await aioAPIClient!.post(url, bulkRequestBodyBrowser);
processResponse(response, browser);
}
} else {
const response = await aioAPIClient!.post(url, bulkRequestBody);
processResponse(response);
}
} catch (err: any) {
debugLogError(err);
if (err.response?.status === 429 && trialCounter < 3) {
AioTestsLogger.log("Reached AIO rate limits. Retrying...");
await sleep(rateLimitWaitTime);
return bulkUpdateResult(aioConfig, trialCounter + 1);
}
const errorMsg = err.response
? `Status Code - ${err.response.status}: ${JSON.stringify(
err.response.data
)}`
: err.code;
AioTestsLogger.error(`Error in bulk cases reporting: ${errorMsg}`);
stopReporting = true;
}
}
async function postResult(aioConfig: AioConfig, caseKey: string, results: TestDataEntry, screenshots: (string|undefined)[], cfUpdates:CustomFieldUpdateData[]|null, trialCounter: number = 0): Promise<void> {
const data: Record<string, any> = {
testRunStatus: results.testStatus,
effort: results.duration,
isAutomated: true,
comments: results.comments,
customFieldValueToUpdate:
aioConfig.runDetails
? replaceProjectNameForRunFields(cfUpdates, results.browser)
: cfUpdates,
};
const createNewRun = aioConfig.addNewRun;
AioTestsLogger.debug(`Posting results to /project/${aioConfig.jiraProjectId}/testcycle/${aioConfig.cycleDetails.cycleKeyToReportTo}/testcase/${caseKey}/testrun?createNewRun=${createNewRun}`);
try {
const response = await aioAPIClient!.post(
`/project/${aioConfig.jiraProjectId}/testcycle/${aioConfig.cycleDetails.cycleKeyToReportTo}/testcase/${caseKey}/testrun?createNewRun=${createNewRun}`,
data);
if (multiBrowser) {
if (browserKeyMap.has(results.browser)) {
browserKeyMap.get(results.browser)?.push(response.data.ID);
} else {
browserKeyMap.set(results.browser, [response.data.ID]);
}
} else {
allFailedRuns.push(response.data.ID);
}
AioTestsLogger.log(`Successfully reported ${caseKey} as ${data.testRunStatus} with runID ${response.data.ID}.`);
const runId = response.data.ID;
if (aioConfig.addAttachmentToFailedCases && data.testRunStatus.toLowerCase() === "failed" && (isAttachmentAPIAvailable || isAttachmentAPIAvailable == null)) {
return await uploadAttachments(aioConfig.jiraProjectId, aioConfig.cycleDetails.cycleKeyToReportTo!, runId, screenshots);
}
} catch (err) {
debugLogError(err);
if (isAxiosError(err) && err.response) {
if (err.response.status === 429 && trialCounter < 3) {
AioTestsLogger.log("Reached AIO rate limits. Pausing...");
await sleep(rateLimitWaitTime);
return postResult(aioConfig, caseKey, results, screenshots, cfUpdates,trialCounter + 1);
} else {
AioTestsLogger.error(
`Error reporting ${caseKey} : Status Code - ${err.response.status} - ${err.response.data}`
);
}
} else {
AioTestsLogger.error(
`Error in bulk cases reporting : ${(err as Error).message}`
);
}
}
}
async function uploadAttachments(jiraProjectId: string, cycleKey: string, runId: string, resultScreenshots: (string|undefined)[]): Promise<void> {
if (resultScreenshots) {
return resultScreenshots.reduce(async (promise, screenshot) => {
await promise;
return uploadScreenshot(screenshot, jiraProjectId, cycleKey, runId);
}, Promise.resolve());
} else {
return Promise.resolve();
}
}