@allurereport/service 3.8.1 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export type * from "./model.js";
2
2
  export * from "./service.js";
3
- export * from "./legacyService.js";
3
+ export * from "./testops.js";
4
4
  export * from "./history.js";
5
- export { KnownError, UnknownError } from "./utils/http.js";
5
+ export { KnownError, UnknownError, createServiceHttpClient } from "./utils/http.js";
6
+ export { isReportDataFile } from "./utils/files.js";
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./service.js";
2
- export * from "./legacyService.js";
2
+ export * from "./testops.js";
3
3
  export * from "./history.js";
4
- export { KnownError, UnknownError } from "./utils/http.js";
4
+ export { KnownError, UnknownError, createServiceHttpClient } from "./utils/http.js";
5
+ export { isReportDataFile } from "./utils/files.js";
package/dist/model.d.ts CHANGED
@@ -1,8 +1,25 @@
1
- import type { HistoryDataPoint } from "@allurereport/core-api";
1
+ import type { AllureServiceConfig, HistoryDataPoint } from "@allurereport/core-api";
2
2
  export declare const DEFAULT_HISTORY_SERVICE_URL = "https://history.allurereport.org";
3
3
  export declare const ALLURE_FILES_DIRNAME: string;
4
4
  export declare const ALLURE_LOGIN_EXCHANGE_TOKEN_PATH: string;
5
5
  export declare const ALLURE_ACCESS_TOKEN_PATH: string;
6
+ export declare const ALLURE_SERVICE_STORAGE_PREFIX = "ars1.";
7
+ export declare const ALLURE_SERVICE_TESTOPS_PREFIX = "ato1.";
8
+ export type UploadReportConfig = Required<Pick<AllureServiceConfig, "uploadConcurrency" | "uploadMaxAttempts" | "uploadMaxSimultaneousFailures">>;
9
+ export type AllureServiceApiClientConfig = UploadReportConfig & {
10
+ accessToken: string;
11
+ private?: boolean;
12
+ };
13
+ export type UploadReportPayload = {
14
+ reportUuid: string;
15
+ pluginId?: string;
16
+ files: Record<string, string>;
17
+ onProgress?: () => void;
18
+ };
19
+ export type UploadReportResult = {
20
+ indexHref?: string;
21
+ hrefs: Record<string, string>;
22
+ };
6
23
  export interface AllureServiceApiClient {
7
24
  downloadHistory(payload: {
8
25
  repo?: string;
@@ -17,7 +34,7 @@ export interface AllureServiceApiClient {
17
34
  }): Promise<URL>;
18
35
  completeReport(payload: {
19
36
  reportUuid: string;
20
- historyPoint: HistoryDataPoint;
37
+ historyPoint?: HistoryDataPoint;
21
38
  }): Promise<unknown>;
22
39
  deleteReport(payload: {
23
40
  reportUuid: string;
@@ -37,4 +54,5 @@ export interface AllureServiceApiClient {
37
54
  filepath?: string;
38
55
  signal?: AbortSignal;
39
56
  }): Promise<string>;
57
+ uploadReport(payload: UploadReportPayload): Promise<UploadReportResult>;
40
58
  }
package/dist/model.js CHANGED
@@ -4,3 +4,5 @@ export const DEFAULT_HISTORY_SERVICE_URL = "https://history.allurereport.org";
4
4
  export const ALLURE_FILES_DIRNAME = resolve(homedir(), ".allure");
5
5
  export const ALLURE_LOGIN_EXCHANGE_TOKEN_PATH = join(ALLURE_FILES_DIRNAME, "exchange_token");
6
6
  export const ALLURE_ACCESS_TOKEN_PATH = join(ALLURE_FILES_DIRNAME, "access_token");
7
+ export const ALLURE_SERVICE_STORAGE_PREFIX = "ars1.";
8
+ export const ALLURE_SERVICE_TESTOPS_PREFIX = "ato1.";
package/dist/service.d.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  import { type HistoryDataPoint } from "@allurereport/core-api";
2
- import { type Config } from "@allurereport/plugin-api";
3
- import type { AllureServiceApiClient } from "./model.js";
2
+ import { type AllureServiceApiClient, type AllureServiceApiClientConfig, type UploadReportPayload } from "./model.js";
4
3
  export declare class AllureServiceClient implements AllureServiceApiClient {
5
4
  #private;
6
- readonly config: Config["allureService"];
7
- constructor(config: Config["allureService"]);
5
+ readonly config: AllureServiceApiClientConfig;
6
+ constructor(config: AllureServiceApiClientConfig);
8
7
  downloadHistory(payload: {
9
8
  repo?: string;
10
9
  branch?: string;
@@ -38,4 +37,5 @@ export declare class AllureServiceClient implements AllureServiceApiClient {
38
37
  filepath?: string;
39
38
  signal?: AbortSignal;
40
39
  }): Promise<string>;
40
+ uploadReport(payload: UploadReportPayload): Promise<import("./model.js").UploadReportResult>;
41
41
  }
package/dist/service.js CHANGED
@@ -12,9 +12,9 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
12
12
  var _AllureServiceClient_client, _AllureServiceClient_url;
13
13
  import { readFile } from "node:fs/promises";
14
14
  import { join as joinPosix } from "node:path/posix";
15
- import { createServiceHttpClient } from "./utils/http.js";
15
+ import { ALLURE_SERVICE_STORAGE_PREFIX, } from "./model.js";
16
+ import { createServiceHttpClient, uploadReport } from "./utils/http.js";
16
17
  import { parseServiceToken } from "./utils/token.js";
17
- const ASSET_MAX_FILE_SIZE = 200 * 1024 * 1024;
18
18
  const UPLOAD_CONTENT_TYPE = "application/octet-stream";
19
19
  const createUploadBlob = (content) => new Blob([content], { type: UPLOAD_CONTENT_TYPE });
20
20
  const createReportUrl = (baseUrl, reportUuid) => `${baseUrl}/${reportUuid}`;
@@ -27,9 +27,14 @@ export class AllureServiceClient {
27
27
  if (!config?.accessToken) {
28
28
  throw new Error("Allure service access token is required");
29
29
  }
30
+ if (!config.accessToken.startsWith(ALLURE_SERVICE_STORAGE_PREFIX)) {
31
+ throw new Error("Allure service access token is invalid");
32
+ }
30
33
  const { url } = parseServiceToken(config.accessToken);
31
34
  __classPrivateFieldSet(this, _AllureServiceClient_url, url.replace(/\/$/, ""), "f");
32
- __classPrivateFieldSet(this, _AllureServiceClient_client, createServiceHttpClient(__classPrivateFieldGet(this, _AllureServiceClient_url, "f"), config.accessToken), "f");
35
+ __classPrivateFieldSet(this, _AllureServiceClient_client, createServiceHttpClient(__classPrivateFieldGet(this, _AllureServiceClient_url, "f"), {
36
+ accessToken: config.accessToken,
37
+ }), "f");
33
38
  }
34
39
  async downloadHistory(payload) {
35
40
  const { repo, branch, limit } = payload ?? {};
@@ -68,7 +73,7 @@ export class AllureServiceClient {
68
73
  }
69
74
  async deleteReport(payload) {
70
75
  const { reportUuid, pluginId = "" } = payload;
71
- return __classPrivateFieldGet(this, _AllureServiceClient_client, "f").post(`/api/reports/${reportUuid}/delete`, {
76
+ return __classPrivateFieldGet(this, _AllureServiceClient_client, "f").post(`/api/report/${reportUuid}/delete`, {
72
77
  body: {
73
78
  pluginId,
74
79
  },
@@ -83,9 +88,6 @@ export class AllureServiceClient {
83
88
  if (!content) {
84
89
  content = signal ? await readFile(filepath, { signal }) : await readFile(filepath);
85
90
  }
86
- if (content.length > ASSET_MAX_FILE_SIZE) {
87
- throw new Error(`Asset size exceeds the maximum allowed size of ${ASSET_MAX_FILE_SIZE / (1024 * 1024)}MB`);
88
- }
89
91
  const form = new FormData();
90
92
  form.set("filename", filename);
91
93
  form.set("file", createUploadBlob(content), filename);
@@ -107,9 +109,6 @@ export class AllureServiceClient {
107
109
  if (!content) {
108
110
  content = signal ? await readFile(filepath, { signal }) : await readFile(filepath);
109
111
  }
110
- if (content.length > ASSET_MAX_FILE_SIZE) {
111
- throw new Error(`Report file size exceeds the maximum allowed size of ${ASSET_MAX_FILE_SIZE / (1024 * 1024)}MB`);
112
- }
113
112
  const form = new FormData();
114
113
  form.set("filename", reportFilename);
115
114
  form.set("file", createUploadBlob(content), reportFilename);
@@ -122,5 +121,15 @@ export class AllureServiceClient {
122
121
  });
123
122
  return createReportFileUrl(__classPrivateFieldGet(this, _AllureServiceClient_url, "f"), reportUuid, reportFilename);
124
123
  }
124
+ async uploadReport(payload) {
125
+ return uploadReport({
126
+ ...payload,
127
+ uploadConcurrency: this.config.uploadConcurrency,
128
+ uploadMaxAttempts: this.config.uploadMaxAttempts,
129
+ uploadMaxSimultaneousFailures: this.config.uploadMaxSimultaneousFailures,
130
+ addReportAsset: this.addReportAsset.bind(this),
131
+ addReportFile: this.addReportFile.bind(this),
132
+ });
133
+ }
125
134
  }
126
135
  _AllureServiceClient_client = new WeakMap(), _AllureServiceClient_url = new WeakMap();
@@ -1,24 +1,19 @@
1
1
  import { type HistoryDataPoint } from "@allurereport/core-api";
2
- import { type Config } from "@allurereport/plugin-api";
3
- import type { AllureServiceApiClient } from "./model.js";
4
- export declare class AllureLegacyServiceClient implements AllureServiceApiClient {
2
+ import { type AllureServiceApiClient, type AllureServiceApiClientConfig, type UploadReportPayload } from "./model.js";
3
+ export declare class AllureTestOpsClient implements AllureServiceApiClient {
5
4
  #private;
6
- readonly config: Config["allureService"];
7
- constructor(config: Config["allureService"]);
8
- downloadHistory(payload: {
9
- repo?: string;
10
- branch?: string;
11
- limit?: number;
12
- }): Promise<HistoryDataPoint[]>;
5
+ readonly config: AllureServiceApiClientConfig;
6
+ constructor(config: AllureServiceApiClientConfig);
7
+ downloadHistory(): Promise<HistoryDataPoint[]>;
13
8
  createReport(payload: {
14
9
  reportName: string;
15
10
  reportUuid?: string;
16
11
  repo?: string;
17
12
  branch?: string;
18
- }): Promise<URL>;
13
+ }): Promise<import("url").URL>;
19
14
  completeReport(payload: {
20
15
  reportUuid: string;
21
- historyPoint: HistoryDataPoint;
16
+ historyPoint?: HistoryDataPoint;
22
17
  }): Promise<unknown>;
23
18
  deleteReport(payload: {
24
19
  reportUuid: string;
@@ -38,4 +33,5 @@ export declare class AllureLegacyServiceClient implements AllureServiceApiClient
38
33
  filepath?: string;
39
34
  signal?: AbortSignal;
40
35
  }): Promise<string>;
36
+ uploadReport(payload: UploadReportPayload): Promise<import("./model.js").UploadReportResult>;
41
37
  }
@@ -0,0 +1,155 @@
1
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
2
+ if (kind === "m") throw new TypeError("Private method is not writable");
3
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
4
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
5
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
6
+ };
7
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
8
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
9
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
+ };
12
+ var _AllureTestOpsClient_instances, _AllureTestOpsClient_url, _AllureTestOpsClient_accessToken, _AllureTestOpsClient_projectId, _AllureTestOpsClient_client, _AllureTestOpsClient_authorizedClient;
13
+ import { readFile } from "node:fs/promises";
14
+ import { extname, join as joinPosix } from "node:path/posix";
15
+ import { ALLURE_SERVICE_TESTOPS_PREFIX, } from "./model.js";
16
+ import { createServiceHttpClient, uploadReport } from "./utils/http.js";
17
+ import { parseServiceToken } from "./utils/token.js";
18
+ const DEFAULT_UPLOAD_CONTENT_TYPE = "application/octet-stream";
19
+ const CONTENT_TYPES = {
20
+ ".css": "text/css",
21
+ ".gif": "image/gif",
22
+ ".html": "text/html",
23
+ ".jpeg": "image/jpeg",
24
+ ".jpg": "image/jpeg",
25
+ ".js": "application/javascript",
26
+ ".json": "application/json",
27
+ ".map": "application/json",
28
+ ".mjs": "application/javascript",
29
+ ".otf": "font/otf",
30
+ ".png": "image/png",
31
+ ".svg": "image/svg+xml",
32
+ ".ttf": "font/ttf",
33
+ ".webp": "image/webp",
34
+ ".woff": "font/woff",
35
+ ".woff2": "font/woff2",
36
+ };
37
+ const contentTypeByFilename = (filename) => CONTENT_TYPES[extname(filename)] ?? DEFAULT_UPLOAD_CONTENT_TYPE;
38
+ const createUploadBlob = (content, filename) => new Blob([content], { type: contentTypeByFilename(filename) });
39
+ const createReportFileUrl = (baseUrl, reportUuid, reportFilename) => `${baseUrl}/api/test-report/view/${joinPosix(reportUuid, reportFilename)}`;
40
+ export class AllureTestOpsClient {
41
+ constructor(config) {
42
+ _AllureTestOpsClient_instances.add(this);
43
+ this.config = config;
44
+ _AllureTestOpsClient_url.set(this, void 0);
45
+ _AllureTestOpsClient_accessToken.set(this, void 0);
46
+ _AllureTestOpsClient_projectId.set(this, void 0);
47
+ _AllureTestOpsClient_client.set(this, void 0);
48
+ if (!config?.accessToken) {
49
+ throw new Error("Allure TestOps access token is required");
50
+ }
51
+ if (!config.accessToken.startsWith(ALLURE_SERVICE_TESTOPS_PREFIX)) {
52
+ throw new Error("Allure service access token is invalid");
53
+ }
54
+ const accessTokenPayload = parseServiceToken(config.accessToken);
55
+ const projectId = Number(accessTokenPayload.projectId);
56
+ if (!Number.isFinite(projectId)) {
57
+ throw new Error("Given access token doesn't contain project id");
58
+ }
59
+ if (!accessTokenPayload.url) {
60
+ throw new Error("Given access token doesn't contain url");
61
+ }
62
+ __classPrivateFieldSet(this, _AllureTestOpsClient_accessToken, accessTokenPayload.accessToken, "f");
63
+ __classPrivateFieldSet(this, _AllureTestOpsClient_projectId, projectId, "f");
64
+ __classPrivateFieldSet(this, _AllureTestOpsClient_url, accessTokenPayload.url.replace(/\/$/, ""), "f");
65
+ }
66
+ async downloadHistory() {
67
+ return [];
68
+ }
69
+ async createReport(payload) {
70
+ const client = await __classPrivateFieldGet(this, _AllureTestOpsClient_instances, "m", _AllureTestOpsClient_authorizedClient).call(this);
71
+ const { reportName, reportUuid } = payload;
72
+ const { url } = await client.post("/api/test-report", {
73
+ body: {
74
+ projectId: __classPrivateFieldGet(this, _AllureTestOpsClient_projectId, "f"),
75
+ isPublic: !this.config?.private,
76
+ reportName,
77
+ reportUuid,
78
+ },
79
+ });
80
+ return new URL(url, __classPrivateFieldGet(this, _AllureTestOpsClient_url, "f"));
81
+ }
82
+ async completeReport(payload) {
83
+ const client = await __classPrivateFieldGet(this, _AllureTestOpsClient_instances, "m", _AllureTestOpsClient_authorizedClient).call(this);
84
+ const { reportUuid } = payload;
85
+ return client.post(`/api/test-report/${reportUuid}/complete`);
86
+ }
87
+ async deleteReport(payload) {
88
+ const client = await __classPrivateFieldGet(this, _AllureTestOpsClient_instances, "m", _AllureTestOpsClient_authorizedClient).call(this);
89
+ const { reportUuid } = payload;
90
+ return client.delete(`/api/test-report/${reportUuid}`);
91
+ }
92
+ async addReportAsset(payload) {
93
+ const { filename, file, filepath, signal } = payload;
94
+ if (!file && !filepath) {
95
+ throw new Error("File or filepath is required");
96
+ }
97
+ let content = file;
98
+ if (!content) {
99
+ content = signal ? await readFile(filepath, { signal }) : await readFile(filepath);
100
+ }
101
+ const form = new FormData();
102
+ form.set("filename", filename);
103
+ form.set("file", createUploadBlob(content, filename), filename);
104
+ const client = await __classPrivateFieldGet(this, _AllureTestOpsClient_instances, "m", _AllureTestOpsClient_authorizedClient).call(this);
105
+ return client.post("/api/test-report/upload", {
106
+ body: form,
107
+ headers: {
108
+ "Content-Type": "multipart/form-data",
109
+ },
110
+ ...(signal ? { signal } : {}),
111
+ });
112
+ }
113
+ async addReportFile(payload) {
114
+ const { reportUuid, filename, file, filepath, pluginId, signal } = payload;
115
+ const reportFilename = pluginId ? joinPosix(pluginId, filename) : filename;
116
+ if (!file && !filepath) {
117
+ throw new Error("File or filepath is required");
118
+ }
119
+ let content = file;
120
+ if (!content) {
121
+ content = signal ? await readFile(filepath, { signal }) : await readFile(filepath);
122
+ }
123
+ const form = new FormData();
124
+ form.set("filename", reportFilename);
125
+ form.set("file", createUploadBlob(content, reportFilename), reportFilename);
126
+ const client = await __classPrivateFieldGet(this, _AllureTestOpsClient_instances, "m", _AllureTestOpsClient_authorizedClient).call(this);
127
+ await client.post(`/api/test-report/${reportUuid}/upload`, {
128
+ body: form,
129
+ headers: {
130
+ "Content-Type": "multipart/form-data",
131
+ },
132
+ ...(signal ? { signal } : {}),
133
+ });
134
+ return createReportFileUrl(__classPrivateFieldGet(this, _AllureTestOpsClient_url, "f"), reportUuid, reportFilename);
135
+ }
136
+ async uploadReport(payload) {
137
+ return uploadReport({
138
+ ...payload,
139
+ uploadConcurrency: this.config.uploadConcurrency,
140
+ uploadMaxAttempts: this.config.uploadMaxAttempts,
141
+ uploadMaxSimultaneousFailures: this.config.uploadMaxSimultaneousFailures,
142
+ addReportAsset: this.addReportAsset.bind(this),
143
+ addReportFile: this.addReportFile.bind(this),
144
+ });
145
+ }
146
+ }
147
+ _AllureTestOpsClient_url = new WeakMap(), _AllureTestOpsClient_accessToken = new WeakMap(), _AllureTestOpsClient_projectId = new WeakMap(), _AllureTestOpsClient_client = new WeakMap(), _AllureTestOpsClient_instances = new WeakSet(), _AllureTestOpsClient_authorizedClient = async function _AllureTestOpsClient_authorizedClient() {
148
+ if (__classPrivateFieldGet(this, _AllureTestOpsClient_client, "f")) {
149
+ return __classPrivateFieldGet(this, _AllureTestOpsClient_client, "f");
150
+ }
151
+ __classPrivateFieldSet(this, _AllureTestOpsClient_client, createServiceHttpClient(__classPrivateFieldGet(this, _AllureTestOpsClient_url, "f"), {
152
+ apiToken: __classPrivateFieldGet(this, _AllureTestOpsClient_accessToken, "f"),
153
+ }), "f");
154
+ return __classPrivateFieldGet(this, _AllureTestOpsClient_client, "f");
155
+ };
@@ -0,0 +1 @@
1
+ export declare const isReportDataFile: (filename: string) => boolean;
@@ -0,0 +1,5 @@
1
+ export const isReportDataFile = (filename) => filename === "index.html" ||
2
+ filename === "summary.json" ||
3
+ filename.startsWith("data/") ||
4
+ filename.startsWith("widgets/") ||
5
+ filename.startsWith("history/");
@@ -1,4 +1,5 @@
1
1
  import { type AxiosRequestConfig } from "axios";
2
+ import type { UploadReportConfig, UploadReportPayload, UploadReportResult } from "../model.js";
2
3
  export declare class KnownError extends Error {
3
4
  status?: number;
4
5
  constructor(message: string, status?: number);
@@ -7,7 +8,19 @@ export declare class UnknownError extends Error {
7
8
  stack?: string;
8
9
  constructor(message: string, stack?: string);
9
10
  }
10
- export declare const createServiceHttpClient: (serviceUrl: string, accessToken: string) => {
11
+ export declare const formatResponseErrorData: (data: unknown) => string | undefined;
12
+ export declare const formatServiceHttpErrorMessage: (payload: {
13
+ method: string;
14
+ endpoint: string;
15
+ status?: number;
16
+ statusText?: string;
17
+ data?: unknown;
18
+ fallbackMessage?: string;
19
+ }) => string;
20
+ export declare const createServiceHttpClient: (serviceUrl: string, params?: {
21
+ accessToken?: string;
22
+ apiToken?: string;
23
+ }) => {
11
24
  get: <T>(endpoint: string, payload?: AxiosRequestConfig & {
12
25
  params?: Record<string, any>;
13
26
  body?: any;
@@ -26,3 +39,17 @@ export declare const createServiceHttpClient: (serviceUrl: string, accessToken:
26
39
  }) => Promise<T>;
27
40
  };
28
41
  export type HttpClient = ReturnType<typeof createServiceHttpClient>;
42
+ export declare const uploadReport: (payload: UploadReportPayload & UploadReportConfig & {
43
+ addReportAsset: (payload: {
44
+ filename: string;
45
+ filepath: string;
46
+ signal?: AbortSignal;
47
+ }) => Promise<unknown>;
48
+ addReportFile: (payload: {
49
+ reportUuid: string;
50
+ pluginId?: string;
51
+ filename: string;
52
+ filepath: string;
53
+ signal?: AbortSignal;
54
+ }) => Promise<string>;
55
+ }) => Promise<UploadReportResult>;
@@ -1,4 +1,5 @@
1
1
  import axios, { isAxiosError } from "axios";
2
+ import { isReportDataFile } from "./files.js";
2
3
  export class KnownError extends Error {
3
4
  constructor(message, status) {
4
5
  super(message);
@@ -13,27 +14,74 @@ export class UnknownError extends Error {
13
14
  this.stack = stack;
14
15
  }
15
16
  }
16
- export const createServiceHttpClient = (serviceUrl, accessToken) => {
17
+ const ERROR_MESSAGE_FIELDS = ["message", "error_description", "error", "detail", "title", "description"];
18
+ const stringifyErrorObject = (value) => {
19
+ try {
20
+ return JSON.stringify(value);
21
+ }
22
+ catch {
23
+ const entries = Object.entries(value).map(([key, entryValue]) => `${key}=${String(entryValue)}`);
24
+ return entries.length > 0 ? `{ ${entries.join(", ")} }` : undefined;
25
+ }
26
+ };
27
+ export const formatResponseErrorData = (data) => {
28
+ if (data === undefined || data === null || data === "") {
29
+ return undefined;
30
+ }
31
+ if (typeof data === "string") {
32
+ return data;
33
+ }
34
+ if (typeof data === "number" || typeof data === "boolean" || typeof data === "bigint") {
35
+ return String(data);
36
+ }
37
+ if (Array.isArray(data)) {
38
+ const items = data.map(formatResponseErrorData).filter(Boolean);
39
+ return items.length > 0 ? items.join("; ") : undefined;
40
+ }
41
+ if (typeof data !== "object") {
42
+ return String(data);
43
+ }
44
+ const errorData = data;
45
+ for (const field of ERROR_MESSAGE_FIELDS) {
46
+ const message = formatResponseErrorData(errorData[field]);
47
+ if (message) {
48
+ return message;
49
+ }
50
+ }
51
+ return stringifyErrorObject(errorData);
52
+ };
53
+ export const formatServiceHttpErrorMessage = (payload) => {
54
+ const { method, endpoint, status, statusText, data, fallbackMessage } = payload;
55
+ const request = `${method.toUpperCase()} ${endpoint}`;
56
+ const statusMessage = status ? ` responded with ${status}${statusText ? ` ${statusText}` : ""}` : " failed";
57
+ const details = formatResponseErrorData(data) || fallbackMessage;
58
+ return `Allure service request failed: ${request}${statusMessage}${details ? `: ${details}` : ""}`;
59
+ };
60
+ export const createServiceHttpClient = (serviceUrl, params) => {
17
61
  const client = axios.create({
18
62
  baseURL: serviceUrl,
19
- withCredentials: true,
20
63
  validateStatus: (status) => status < 400,
21
64
  });
22
65
  const sendRequest = (method) => async (endpoint, payload) => {
23
66
  const headers = {
24
67
  ...(payload?.headers ?? {}),
25
- Authorization: `Bearer ${accessToken}`,
26
68
  };
69
+ if (params?.accessToken) {
70
+ headers.Authorization = `Bearer ${params.accessToken}`;
71
+ }
72
+ if (params?.apiToken) {
73
+ headers.Authorization = `api-token ${params.apiToken}`;
74
+ }
27
75
  try {
28
76
  let res;
29
- if (payload?.body) {
30
- res = await client[method](endpoint, payload.body, {
77
+ if (method === "get" || method === "delete") {
78
+ res = await client[method](endpoint, {
31
79
  ...payload,
32
80
  headers,
33
81
  });
34
82
  }
35
83
  else {
36
- res = await client[method](endpoint, {
84
+ res = await client[method](endpoint, payload?.body, {
37
85
  ...payload,
38
86
  headers,
39
87
  });
@@ -45,10 +93,18 @@ export const createServiceHttpClient = (serviceUrl, accessToken) => {
45
93
  if (!axiosError) {
46
94
  throw err;
47
95
  }
48
- const { status = 500 } = err.response ?? {};
49
- const errorMessage = err.response?.data?.error || err.response?.data || err.message;
50
- if (status < 500) {
51
- throw new KnownError(errorMessage, status);
96
+ const { data, status, statusText } = err.response ?? {};
97
+ const responseStatus = status ?? 500;
98
+ const errorMessage = formatServiceHttpErrorMessage({
99
+ method,
100
+ endpoint,
101
+ status,
102
+ statusText,
103
+ data,
104
+ fallbackMessage: err.message,
105
+ });
106
+ if (responseStatus < 500) {
107
+ throw new KnownError(errorMessage, responseStatus);
52
108
  }
53
109
  throw new UnknownError(errorMessage, err.stack);
54
110
  }
@@ -60,3 +116,90 @@ export const createServiceHttpClient = (serviceUrl, accessToken) => {
60
116
  delete: sendRequest("delete"),
61
117
  };
62
118
  };
119
+ const uploadWithRetry = async (filename, uploadAbortController, failedUploads, maxAttempts, maxSimultaneousFailures, uploadFn) => {
120
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
121
+ if (uploadAbortController.signal.aborted) {
122
+ return false;
123
+ }
124
+ try {
125
+ await uploadFn();
126
+ failedUploads.delete(filename);
127
+ return true;
128
+ }
129
+ catch (error) {
130
+ if (uploadAbortController.signal.aborted) {
131
+ return false;
132
+ }
133
+ failedUploads.add(filename);
134
+ if (failedUploads.size > maxSimultaneousFailures || attempt >= maxAttempts) {
135
+ throw error;
136
+ }
137
+ }
138
+ }
139
+ return false;
140
+ };
141
+ export const uploadReport = async (payload) => {
142
+ const { reportUuid, pluginId, files, onProgress, addReportAsset, addReportFile, uploadConcurrency, uploadMaxAttempts, uploadMaxSimultaneousFailures, } = payload;
143
+ const fileEntries = Object.entries(files);
144
+ if (fileEntries.length === 0) {
145
+ return {
146
+ hrefs: {},
147
+ };
148
+ }
149
+ const uploadAbortController = new AbortController();
150
+ const failedUploads = new Set();
151
+ const hrefs = {};
152
+ let indexHref;
153
+ let nextFileIndex = 0;
154
+ const uploadNext = async () => {
155
+ while (!uploadAbortController.signal.aborted) {
156
+ const fileIndex = nextFileIndex++;
157
+ if (fileIndex >= fileEntries.length) {
158
+ return;
159
+ }
160
+ const [filename, filepath] = fileEntries[fileIndex];
161
+ let fileUrl;
162
+ const uploaded = await uploadWithRetry(filename, uploadAbortController, failedUploads, uploadMaxAttempts, uploadMaxSimultaneousFailures, async () => {
163
+ if (isReportDataFile(filename)) {
164
+ fileUrl = await addReportFile({
165
+ reportUuid,
166
+ pluginId,
167
+ filename,
168
+ filepath,
169
+ signal: uploadAbortController.signal,
170
+ });
171
+ }
172
+ else {
173
+ await addReportAsset({
174
+ filename,
175
+ filepath,
176
+ signal: uploadAbortController.signal,
177
+ });
178
+ }
179
+ });
180
+ if (!uploaded || uploadAbortController.signal.aborted) {
181
+ return;
182
+ }
183
+ if (fileUrl) {
184
+ hrefs[filename] = fileUrl;
185
+ if (filename === "index.html") {
186
+ indexHref = fileUrl;
187
+ }
188
+ }
189
+ onProgress?.();
190
+ }
191
+ };
192
+ const uploadTasks = Array.from({ length: Math.min(uploadConcurrency, fileEntries.length) }, () => uploadNext());
193
+ try {
194
+ await Promise.all(uploadTasks);
195
+ }
196
+ catch (error) {
197
+ uploadAbortController.abort();
198
+ await Promise.allSettled(uploadTasks);
199
+ throw error;
200
+ }
201
+ return {
202
+ indexHref,
203
+ hrefs,
204
+ };
205
+ };
@@ -1,4 +1,6 @@
1
- export declare const parseServiceToken: (token: string) => {
1
+ interface ServiceTokenPayload {
2
2
  accessToken: string;
3
3
  url: string;
4
- };
4
+ }
5
+ export declare const parseServiceToken: <T = ServiceTokenPayload>(token: string) => T & ServiceTokenPayload;
6
+ export {};
@@ -11,10 +11,7 @@ export const parseServiceToken = (token) => {
11
11
  if (!payload.url) {
12
12
  throw new Error("missing url");
13
13
  }
14
- return {
15
- accessToken: payload.accessToken,
16
- url: payload.url,
17
- };
14
+ return payload;
18
15
  }
19
16
  catch {
20
17
  throw new Error("Allure service access token is invalid");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allurereport/service",
3
- "version": "3.8.1",
3
+ "version": "3.9.0",
4
4
  "description": "Allure Service API",
5
5
  "keywords": [
6
6
  "allure",
@@ -30,8 +30,8 @@
30
30
  "lint:fix": "oxlint --import-plugin --fix src test features stories"
31
31
  },
32
32
  "dependencies": {
33
- "@allurereport/core-api": "3.8.1",
34
- "@allurereport/plugin-api": "3.8.1",
33
+ "@allurereport/core-api": "3.9.0",
34
+ "@allurereport/plugin-api": "3.9.0",
35
35
  "axios": "^1.15.2",
36
36
  "open": "^10.1.0"
37
37
  },
@@ -39,6 +39,7 @@
39
39
  "@types/node": "^20.17.9",
40
40
  "@vitest/runner": "^2.1.8",
41
41
  "@vitest/snapshot": "^2.1.8",
42
+ "allure-js-commons": "^3.3.3",
42
43
  "allure-vitest": "^3.3.3",
43
44
  "rimraf": "^6.0.1",
44
45
  "typescript": "^5.6.3",
@@ -1,128 +0,0 @@
1
- var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
2
- if (kind === "m") throw new TypeError("Private method is not writable");
3
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
4
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
5
- return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
6
- };
7
- var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
8
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
9
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
- return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
- };
12
- var _AllureLegacyServiceClient_client, _AllureLegacyServiceClient_url, _AllureLegacyServiceClient_reportUrl;
13
- import { readFile } from "node:fs/promises";
14
- import { join as joinPosix } from "node:path/posix";
15
- import { createServiceHttpClient } from "./utils/http.js";
16
- import { parseServiceToken } from "./utils/token.js";
17
- const ASSET_MAX_FILE_SIZE = 200 * 1024 * 1024;
18
- const UPLOAD_CONTENT_TYPE = "application/octet-stream";
19
- const createUploadBlob = (content) => new Blob([content], { type: UPLOAD_CONTENT_TYPE });
20
- const createReportFileUrl = (reportUrl, reportFilename) => {
21
- const fileUrl = new URL(reportUrl);
22
- fileUrl.pathname = joinPosix(fileUrl.pathname, reportFilename);
23
- fileUrl.search = "";
24
- fileUrl.hash = "";
25
- return fileUrl.toString();
26
- };
27
- const createFallbackReportUrl = (baseUrl, reportUuid) => {
28
- const reportUrl = new URL(`${baseUrl}/`);
29
- reportUrl.pathname = joinPosix(reportUrl.pathname, reportUuid);
30
- return reportUrl;
31
- };
32
- export class AllureLegacyServiceClient {
33
- constructor(config) {
34
- this.config = config;
35
- _AllureLegacyServiceClient_client.set(this, void 0);
36
- _AllureLegacyServiceClient_url.set(this, void 0);
37
- _AllureLegacyServiceClient_reportUrl.set(this, void 0);
38
- if (!config?.accessToken) {
39
- throw new Error("Allure service access token is required");
40
- }
41
- const { accessToken, url } = parseServiceToken(config.accessToken);
42
- __classPrivateFieldSet(this, _AllureLegacyServiceClient_url, url.replace(/\/$/, ""), "f");
43
- __classPrivateFieldSet(this, _AllureLegacyServiceClient_client, createServiceHttpClient(__classPrivateFieldGet(this, _AllureLegacyServiceClient_url, "f"), accessToken), "f");
44
- }
45
- async downloadHistory(payload) {
46
- const { branch, limit } = payload ?? {};
47
- const { history } = await __classPrivateFieldGet(this, _AllureLegacyServiceClient_client, "f").get("/projects/history", {
48
- params: {
49
- limit: limit ? encodeURIComponent(limit) : undefined,
50
- branch: branch ? encodeURIComponent(branch) : undefined,
51
- },
52
- });
53
- return history;
54
- }
55
- async createReport(payload) {
56
- const { reportName, reportUuid, branch } = payload;
57
- const { url } = await __classPrivateFieldGet(this, _AllureLegacyServiceClient_client, "f").post("/reports", {
58
- body: {
59
- reportName,
60
- reportUuid,
61
- branch,
62
- },
63
- });
64
- __classPrivateFieldSet(this, _AllureLegacyServiceClient_reportUrl, new URL(url), "f");
65
- return __classPrivateFieldGet(this, _AllureLegacyServiceClient_reportUrl, "f");
66
- }
67
- async completeReport(payload) {
68
- const { reportUuid, historyPoint } = payload;
69
- return __classPrivateFieldGet(this, _AllureLegacyServiceClient_client, "f").post(`/reports/${reportUuid}/complete`, {
70
- body: {
71
- historyPoint,
72
- },
73
- });
74
- }
75
- async deleteReport(payload) {
76
- const { reportUuid, pluginId = "" } = payload;
77
- return __classPrivateFieldGet(this, _AllureLegacyServiceClient_client, "f").post(`/reports/${reportUuid}/delete`, {
78
- body: {
79
- pluginId,
80
- },
81
- });
82
- }
83
- async addReportAsset(payload) {
84
- const { filename, file, filepath, signal } = payload;
85
- if (!file && !filepath) {
86
- throw new Error("File or filepath is required");
87
- }
88
- let content = file;
89
- if (!content) {
90
- content = signal ? await readFile(filepath, { signal }) : await readFile(filepath);
91
- }
92
- if (content.length > ASSET_MAX_FILE_SIZE) {
93
- throw new Error(`Asset size exceeds the maximum allowed size of ${ASSET_MAX_FILE_SIZE / (1024 * 1024)}MB`);
94
- }
95
- const form = new FormData();
96
- form.set("filename", filename);
97
- form.set("file", createUploadBlob(content), filename);
98
- return __classPrivateFieldGet(this, _AllureLegacyServiceClient_client, "f").post("/assets/upload", {
99
- body: form,
100
- headers: { "Content-Type": "multipart/form-data" },
101
- ...(signal ? { signal } : {}),
102
- });
103
- }
104
- async addReportFile(payload) {
105
- const { reportUuid, filename, file, filepath, pluginId, signal } = payload;
106
- const reportFilename = pluginId ? joinPosix(pluginId, filename) : filename;
107
- if (!file && !filepath) {
108
- throw new Error("File or filepath is required");
109
- }
110
- let content = file;
111
- if (!content) {
112
- content = signal ? await readFile(filepath, { signal }) : await readFile(filepath);
113
- }
114
- if (content.length > ASSET_MAX_FILE_SIZE) {
115
- throw new Error(`Report file size exceeds the maximum allowed size of ${ASSET_MAX_FILE_SIZE / (1024 * 1024)}MB`);
116
- }
117
- const form = new FormData();
118
- form.set("filename", reportFilename);
119
- form.set("file", createUploadBlob(content), reportFilename);
120
- await __classPrivateFieldGet(this, _AllureLegacyServiceClient_client, "f").post(`/reports/${reportUuid}/upload`, {
121
- body: form,
122
- headers: { "Content-Type": "multipart/form-data" },
123
- ...(signal ? { signal } : {}),
124
- });
125
- return createReportFileUrl(__classPrivateFieldGet(this, _AllureLegacyServiceClient_reportUrl, "f") ?? createFallbackReportUrl(__classPrivateFieldGet(this, _AllureLegacyServiceClient_url, "f"), reportUuid), reportFilename);
126
- }
127
- }
128
- _AllureLegacyServiceClient_client = new WeakMap(), _AllureLegacyServiceClient_url = new WeakMap(), _AllureLegacyServiceClient_reportUrl = new WeakMap();