diff --git a/package.json b/package.json index 2bd1162..e82d623 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,12 @@ "sort-exports": "perl -i -pe 's/^export/import/' src/**/index.ts ; npm run prettier ; perl -i -pe 's/^import/export/' src/**/index.ts", "start": "NODE_ENV=development webpack serve --config config/start.config.js", "test": "true", + "test:check-ts": "node -e \"process.versions.node.localeCompare('22.10.0',undefined,{numeric:true})>=0||(console.error('Testing uses feature flags requiring Node >= 22.10.0, but is using '+process.versions.node),process.exit(1))\"", + "test:check-api-status": "curl -sf \"${PULP_BASE_URL:-http://localhost:8080/pulp/api/v3/}status/\" -o /dev/null || (echo 'Integration tests require the Pulp API, is it running?' >&2; exit 1)", + "test:check": "npm run test:check-ts && npm run test:check-api-status", + "test:unit": "npm run test:check && node --test --experimental-strip-types --test-name-pattern '^Unit: '", + "test:integration": "npm run test:check && node --test --experimental-strip-types --test-name-pattern '^Integration: '", + "test:coverage": "npm run test:check && node --test --experimental-strip-types --experimental-test-coverage --test-coverage-exclude '**/*.test.ts' --test-coverage-exclude 'src/api/test-utils/**'", "upgrade": "npx npm-check-updates -u -t minor" }, "engines": { diff --git a/src/api/common.ts b/src/api/common.ts index a5f64b8..8c4064a 100644 --- a/src/api/common.ts +++ b/src/api/common.ts @@ -98,6 +98,99 @@ interface GenericRemote extends GenericResource { rate_limit?: number | null; } +/** + * Generic Filters shared across PulpCore & Plugin Endpoints. + * + * @see https://github.com/pulp/pulpcore/blob/934c752dae916857b2005e1fe0ef75496accc082/pulpcore/filters.py#L290 + */ +interface GenericFilterParams { + pulp_id__in?: string; + pulp_href__in?: string; + prn__in?: string; + q?: string; + exclude_fields?: string; + fields?: string; + limit?: number; + minimal?: boolean; + offset?: number; + page_size?: number; + ordering?: string; + format?: string; +} + +type LookupFilterParams< + Field extends string, + Lookup extends string, + Value, +> = Partial>; +type NameFilterOptions = + | 'iexact' + | 'in' + | 'contains' + | 'icontains' + | 'startswith' + | 'istartswith' + | 'regex' + | 'iregex'; +type NullableNumericFilterOptions = + | 'ne' + | 'lt' + | 'lte' + | 'gt' + | 'gte' + | 'range' + | 'isnull'; +type NameFilterParams = LookupFilterParams<'name', NameFilterOptions, string>; +type RetainRepoVersionsFilterParams = LookupFilterParams< + 'retain_repo_versions', + NullableNumericFilterOptions, + number | string +>; +type RetainCheckpointsFilterParams = LookupFilterParams< + 'retain_checkpoints', + NullableNumericFilterOptions, + number | string +>; + +/** + * Generic Repository Filters shared across PulpCore & Plugin Endpoints. + * + * @see https://github.com/pulp/pulpcore/blob/934c752dae916857b2005e1fe0ef75496accc082/pulpcore/app/viewsets/repository.py#L88 + */ +interface GenericRepositoryFilterParams + extends + GenericFilterParams, + NameFilterParams, + RetainRepoVersionsFilterParams, + RetainCheckpointsFilterParams { + pulp_label_select?: string; + remote?: string | null; + with_content?: string; + latest_with_content?: string; +} + +/** + * Paginated Response shared across PulpCore & Plugin Endpoints. + * + * @see https://github.com/pulp/pulpcore/blob/934c752dae916857b2005e1fe0ef75496accc082/pulpcore/app/settings.py#L186 + * @see https://github.com/encode/django-rest-framework/blob/6f0b74def3fcc81e126b87b08e59abdb6c2ad056/rest_framework/pagination.py#L364 + */ +interface PaginatedResponse { + count: number; + next: string | null; + previous: string | null; + results: TResult[]; +} + +/** + * Async Task Dispatch Response shared across PulpCore & Plugin Endpoints. + * + * @see https://github.com/pulp/pulpcore/blob/dea04fa79a6ca590f2943a0a7754c219061be10c/pulpcore/app/response.py#L6 + */ +interface DispatchedTaskResponse { + task: string; +} + /** * -------------------- * These are shared Plugin Types outside the PulpCore Generics. @@ -123,5 +216,8 @@ export type { GenericDistribution, GenericPublication, GenericRemote, + GenericRepositoryFilterParams, + PaginatedResponse, + DispatchedTaskResponse, AnsibleLastSyncType, }; diff --git a/src/api/plugins/rpm/client.ts b/src/api/plugins/rpm/client.ts new file mode 100644 index 0000000..7118497 --- /dev/null +++ b/src/api/plugins/rpm/client.ts @@ -0,0 +1,8 @@ +import { PulpAPI } from 'src/api/pulp'; +import { createRepositoryAPI } from './repository'; + +class RpmClient extends PulpAPI { + repository = createRepositoryAPI(this); +} + +export { RpmClient }; diff --git a/src/api/plugins/rpm/repository.test.ts b/src/api/plugins/rpm/repository.test.ts new file mode 100644 index 0000000..0cc131f --- /dev/null +++ b/src/api/plugins/rpm/repository.test.ts @@ -0,0 +1,402 @@ +import { isAxiosError } from 'axios'; +import assert from 'node:assert/strict'; +import { after, afterEach, before, beforeEach, describe, it } from 'node:test'; +import { + testAxiosClient, + testPulpAPI, + waitForTaskCompletion, +} from '../../test-utils/integration-client.ts'; +import { + type RPMRepositoryType, + type RPMRepositoryUpsertType, + createRepositoryAPI, +} from './repository.ts'; + +describe('Integration: RPM Repository API Client', () => { + describe('RPM Repository - list()', () => { + const testRpmRepoName = 'test-rpm-repo-existent-list'; + let repositoryHref: string; + + before(async () => { + const res = await testAxiosClient('repositories/rpm/rpm/', { + method: 'POST', + data: JSON.stringify({ name: testRpmRepoName }), + }); + + if (!res.data.pulp_href) { + throw new Error('Failed to create test repository'); + } + + repositoryHref = res.data.pulp_href; + }); + + after(async () => { + await testAxiosClient(repositoryHref, { method: 'DELETE' }); + }); + + it('list() find the created repository by exact name', async () => { + const client = createRepositoryAPI(testPulpAPI()); + + const res = await client.list({ name: testRpmRepoName }); + + assert.strictEqual(res.status, 200); + assert.strictEqual(res.data.count, 1); + assert.strictEqual(res.data.count, res.data.results.length); + assert.strictEqual(res.data.results[0].name, testRpmRepoName); + assert.strictEqual(res.data.results[0].pulp_href, repositoryHref); + }); + + it('list() when passed a non-existent repository name returns empty result', async () => { + const nonExistentRepositoryName = 'test-rpm-repo-non-existent-list'; + const client = createRepositoryAPI(testPulpAPI()); + + const res = await client.list({ name: nonExistentRepositoryName }); + + assert.strictEqual(res.status, 200); + assert.strictEqual(res.data.count, 0); + assert.strictEqual(res.data.results.length, 0); + assert.strictEqual(res.data.count, res.data.results.length); + }); + }); + + describe('RPM Repository - retrieve()', () => { + const testRpmRepoName = 'test-rpm-repo-existent-retrieve'; + let repositoryPrn: string; + let repositoryHref: string; + + before(async () => { + const res = await testAxiosClient('repositories/rpm/rpm/', { + method: 'POST', + data: JSON.stringify({ name: testRpmRepoName }), + }); + + if (!res.data.prn || !res.data.pulp_href) { + throw new Error('Failed to create test repository'); + } + + repositoryPrn = res.data.prn; + repositoryHref = res.data.pulp_href; + }); + + after(async () => { + await testAxiosClient(repositoryHref, { method: 'DELETE' }); + }); + + it('retrieve() when passed correct identifier returns expected repository', async () => { + const repositoryIdentifier = repositoryPrn.split(':', 3)[2]; + assert.strictEqual(typeof repositoryIdentifier, 'string'); + + const client = createRepositoryAPI(testPulpAPI()); + + const res = await client.retrieve(repositoryIdentifier); + + assert.strictEqual(res.status, 200); + assert.strictEqual(res.data.prn, repositoryPrn); + assert.strictEqual(res.data.name, testRpmRepoName); + }); + + it('retrieve() when passed an non-existent identifier returns empty object', async () => { + const nonExistentRepositoryIdentifier = '1234567890'; + + const client = createRepositoryAPI(testPulpAPI()); + + await assert.rejects( + () => client.retrieve(nonExistentRepositoryIdentifier), + (err: unknown) => { + assert.ok(isAxiosError(err)); + assert.strictEqual(err.response?.status, 404); + return true; + }, + ); + }); + }); + + describe('RPM Repository - create()', () => { + let repositoryHref: string | undefined; + + afterEach(async () => { + if (repositoryHref) { + await testAxiosClient(repositoryHref, { method: 'DELETE' }); + repositoryHref = undefined; + } + }); + + it('create() when giving only the repository name returns with the created repository defaults', async () => { + const testRpmRepoName = 'test-rpm-repo-existent-create-minimal'; + const client = createRepositoryAPI(testPulpAPI()); + + const res = await client.create({ name: testRpmRepoName }); + assert.notStrictEqual(res.data.pulp_href, undefined); + repositoryHref = res.data.pulp_href; + + assert.strictEqual(res.status, 201); + assert.strictEqual(res.data.name, testRpmRepoName); + assert.strictEqual(res.data.autopublish, false); + assert.strictEqual(res.data.retain_package_versions, 0); + assert.strictEqual(res.data.checksum_type, null); + }); + + it('create() when giving additional data fields from required returns them back on created repository', async () => { + const testRpmRepoName = 'test-rpm-existent-create-full'; + const client = createRepositoryAPI(testPulpAPI()); + const payload = { + name: testRpmRepoName, + autopublish: true, + retain_package_versions: 3, + checksum_type: 'sha256', + description: 'This is a test description', + } satisfies RPMRepositoryType; + + const res = await client.create(payload); + assert.notStrictEqual(res.data.pulp_href, repositoryHref); + repositoryHref = res.data.pulp_href; + + assert.strictEqual(res.status, 201); + assert.strictEqual(res.data.autopublish, true); + assert.strictEqual(res.data.name, testRpmRepoName); + assert.strictEqual(res.data.retain_package_versions, 3); + assert.strictEqual(res.data.checksum_type, 'sha256'); + assert.strictEqual(res.data.description, 'This is a test description'); + }); + + it('create() when passed with a duplicate name returns 400', async () => { + const testRpmRepoName = 'test-rpm-existent-create-duplicate'; + const client = createRepositoryAPI(testPulpAPI()); + + const res = await client.create({ name: testRpmRepoName }); + assert.notStrictEqual(res.data.pulp_href, undefined); + repositoryHref = res.data.pulp_href; + + await assert.rejects( + () => client.create({ name: testRpmRepoName }), + (err: unknown) => { + assert.ok(isAxiosError(err)); + assert.strictEqual(err.response?.status, 400); + return true; + }, + ); + }); + + it('create() when missing the required name field returns 400', async () => { + const client = createRepositoryAPI(testPulpAPI()); + + await assert.rejects( + () => client.create({} as RPMRepositoryUpsertType), + (err: unknown) => { + assert.ok(isAxiosError(err)); + assert.strictEqual(err.response?.status, 400); + return true; + }, + ); + }); + + it('create() when an invalid field is passed returns 400', async () => { + const testRpmRepoName = 'test-rpm-existent-create-duplicate'; + const client = createRepositoryAPI(testPulpAPI()); + const invalidPayload = { + name: testRpmRepoName, + compression_type: + 'I am a test' as RPMRepositoryUpsertType['compression_type'], + }; + + await assert.rejects( + () => client.create(invalidPayload), + (err: unknown) => { + assert.ok(isAxiosError(err)); + assert.strictEqual(err.response?.status, 400); + return true; + }, + ); + }); + + it('create() when a not allowed checksum_type is passed returns 400', async () => { + const testRpmRepoName = 'test-rpm-existent-create-duplicate'; + const client = createRepositoryAPI(testPulpAPI()); + const invalidPayload = { + name: testRpmRepoName, + checksum_type: 'md5' as RPMRepositoryUpsertType['checksum_type'], + }; + + await assert.rejects( + () => client.create(invalidPayload), + (err: unknown) => { + assert.ok(isAxiosError(err)); + assert.strictEqual(err.response?.status, 400); + return true; + }, + ); + }); + }); + + describe('RPM Repository - update()', () => { + let repositoryHref: string | undefined; + let repositoryPrn: string | undefined; + + beforeEach(async () => { + const res = await testAxiosClient('repositories/rpm/rpm/', { + method: 'POST', + data: JSON.stringify({ name: 'test-rpm-repo-update' }), + }); + + if (!res.data.prn || !res.data.pulp_href) { + throw new Error('Failed to create test repository'); + } + + repositoryHref = res.data.pulp_href; + repositoryPrn = res.data.prn; + }); + + afterEach(async () => { + await testAxiosClient(repositoryHref, { method: 'DELETE' }); + repositoryHref = undefined; + repositoryPrn = undefined; + }); + + it('update() when updating a field with the same value returns 200', async () => { + const sameNameValue = 'test-rpm-repo-update'; + const repositoryIdentifier = repositoryPrn.split(':', 3)[2]; + assert.strictEqual(typeof repositoryIdentifier, 'string'); + const client = createRepositoryAPI(testPulpAPI()); + + const res = await client.update(repositoryIdentifier, { + name: sameNameValue, + }); + const expected = await client.retrieve(repositoryIdentifier); + + assert.strictEqual(res.status, 200); + assert.strictEqual(expected.data.name, sameNameValue); + }); + + it('update() with a single changed field returns 202 and updates only that field once the task completes', async () => { + const repositoryIdentifier = repositoryPrn.split(':', 3)[2]; + assert.strictEqual(typeof repositoryIdentifier, 'string'); + const client = createRepositoryAPI(testPulpAPI()); + + const res = await client.update(repositoryIdentifier, { + description: 'Updated description', + }); + + let actual: RPMRepositoryType; + assert.strictEqual(res.status, 202); + if (res.status === 202 && 'task' in res.data) { + await waitForTaskCompletion(res.data.task); + actual = (await client.retrieve(repositoryIdentifier)).data; + } + + assert.strictEqual(res.status, 202); + assert.strictEqual(actual.description, 'Updated description'); + }); + + it('update() when changing multiple fields returns 202, and all changes are reflected after the task completes', async () => { + const repositoryIdentifier = repositoryPrn.split(':', 3)[2]; + assert.strictEqual(typeof repositoryPrn, 'string'); + const client = createRepositoryAPI(testPulpAPI()); + const payload = { + autopublish: true, + retain_package_versions: 5, + checksum_type: 'sha256', + compression_type: 'zstd', + } satisfies Partial; + + const res = await client.update(repositoryIdentifier, payload); + + let actual: RPMRepositoryType; + assert.strictEqual(res.status, 202); + if (res.status === 202 && 'task' in res.data) { + await waitForTaskCompletion(res.data.task); + actual = (await client.retrieve(repositoryIdentifier)).data; + } + + assert.strictEqual(actual.autopublish, true); + assert.strictEqual(actual.retain_package_versions, 5); + assert.strictEqual(actual.checksum_type, 'sha256'); + assert.strictEqual(actual.compression_type, 'zstd'); + }); + + it('update() when a not allowed checksum_type is passed returns 400', async () => { + const repositoryIdentifier = repositoryPrn.split(':', 3)[2]; + assert.strictEqual(typeof repositoryPrn, 'string'); + const client = createRepositoryAPI(testPulpAPI()); + const invalidPayload = { + checksum_type: 'sha1' as RPMRepositoryUpsertType['checksum_type'], + }; + + await assert.rejects( + () => client.update(repositoryIdentifier, invalidPayload), + (err: unknown) => { + assert.ok(isAxiosError(err)); + assert.strictEqual(err.response?.status, 400); + return true; + }, + ); + }); + + it('update() when passed a non-existent identifier returns 404', async () => { + const nonExistentRepositoryIdentifier = '1234567890'; + const client = createRepositoryAPI(testPulpAPI()); + + await assert.rejects( + () => + client.update(nonExistentRepositoryIdentifier, { + description: 'This will not update', + }), + (err: unknown) => { + assert.ok(isAxiosError(err)); + assert.strictEqual(err.response?.status, 404); + return true; + }, + ); + }); + }); + + describe('RPM Repository - delete()', () => { + it('delete() returns 202 and dispatches a task', async () => { + const created = await testAxiosClient('repositories/rpm/rpm/', { + method: 'POST', + data: JSON.stringify({ name: 'test-rpm-repo-delete-valid' }), + }); + const repositoryIdentifier = created.data.prn.split(':', 3)[2]; + const client = createRepositoryAPI(testPulpAPI()); + + const res = await client.delete(repositoryIdentifier); + + assert.strictEqual(res.status, 202); + assert.notStrictEqual(res.data.task, undefined); + }); + + it('delete() when passed a non-existent identifier returns 404', async () => { + const nonExistentRepositoryIdentifier = '1234567890'; + const client = createRepositoryAPI(testPulpAPI()); + + await assert.rejects( + () => client.delete(nonExistentRepositoryIdentifier), + (err: unknown) => { + assert.ok(isAxiosError(err)); + assert.strictEqual(err.response?.status, 404); + return true; + }, + ); + }); + + it('delete() removes the repository once the dispatched task completes', async () => { + const created = await testAxiosClient('repositories/rpm/rpm/', { + method: 'POST', + data: JSON.stringify({ name: 'test-rpm-repo-delete-completion' }), + }); + const repositoryIdentifier = created.data.prn.split(':', 3)[2]; + const client = createRepositoryAPI(testPulpAPI()); + + const res = await client.delete(repositoryIdentifier); + await waitForTaskCompletion(res.data.task); + + await assert.rejects( + () => client.retrieve(repositoryIdentifier), + (err: unknown) => { + assert.ok(isAxiosError(err)); + assert.strictEqual(err.response?.status, 404); + return true; + }, + ); + }); + }); +}); diff --git a/src/api/plugins/rpm/repository.ts b/src/api/plugins/rpm/repository.ts new file mode 100644 index 0000000..d8b047e --- /dev/null +++ b/src/api/plugins/rpm/repository.ts @@ -0,0 +1,88 @@ +import type { AxiosResponse } from 'axios'; +import type { + DispatchedTaskResponse, + GenericRepository, + GenericRepositoryFilterParams, + PaginatedResponse, +} from '../../common'; +import type { PulpAPI } from '../../pulp'; + +type RPMChecksumType = + | 'unknown' + | 'md5' + | 'sha' + | 'sha1' + | 'sha224' + | 'sha256' + | 'sha384' + | 'sha512'; +type RPMAllowedUpsertChecksumsType = 'sha256' | 'sha384' | 'sha512'; +type RPMCompressionType = 'zstd' | 'gz' | 'none'; +type RPMLayoutType = 'nested_alphabetically' | 'flat' | 'nested_by_digest'; + +/** + * RPM Repository Type. + * + * @see https://github.com/pulp/pulp_rpm/blob/dc333a99db6c44d70d6103540cb592f6e55a8682/pulp_rpm/app/serializers/repository.py#L177 + */ +interface RPMRepositoryType extends GenericRepository { + autopublish?: boolean; + metadata_signing_service?: string | null; + package_signing_service?: string | null; + package_signing_fingerprint?: string | null; + retain_package_versions?: number; + checksum_type?: RPMChecksumType | null; + compression_type?: RPMCompressionType | null; + layout?: RPMLayoutType | null; + repo_config?: Record; + osv_config?: { name: string; releases: unknown }[] | null; +} + +/** + * RPM Create / Update Type. + * + * @see https://github.com/pulp/pulp_rpm/blob/dc333a99db6c44d70d6103540cb592f6e55a8682/pulp_rpm/app/serializers/repository.py#L326 + */ +interface RPMRepositoryUpsertType extends Omit< + RPMRepositoryType, + 'checksum_type' +> { + checksum_type?: RPMAllowedUpsertChecksumsType | null; +} + +// FIXME: Move AxiosResponse type to PulpAPI Base Class. +// NOTE: The FIXME implementation is not easy as to avoid major type issues with legacy API calls. +interface RPMRepositoryClient { + list: ( + params?: GenericRepositoryFilterParams, + ) => Promise>>; + retrieve: (id: string) => Promise>; + create: ( + data: RPMRepositoryUpsertType, + ) => Promise>; + update: ( + id: string, + data: Partial, + ) => Promise>; + delete: (id: string) => Promise>; +} + +/** + * RPM Repository API Client + * @param {PulpAPI} base + * @returns {RPMRepositoryClient} + * + * @see https://github.com/pulp/pulp_rpm/blob/dc333a99db6c44d70d6103540cb592f6e55a8682/pulp_rpm/app/viewsets/repository.py#L76 + */ +function createRepositoryAPI(base: PulpAPI): RPMRepositoryClient { + return { + list: (params?) => base.list(`repositories/rpm/rpm/`, params), + retrieve: (id) => base.http.get(`repositories/rpm/rpm/${id}/`), + create: (data) => base.http.post(`repositories/rpm/rpm/`, data), + update: (id, data) => base.http.patch(`repositories/rpm/rpm/${id}/`, data), + delete: (id) => base.http.delete(`repositories/rpm/rpm/${id}/`), + }; +} + +export { createRepositoryAPI }; +export type { RPMRepositoryType, RPMRepositoryUpsertType }; diff --git a/src/api/rpm-repository.ts b/src/api/rpm-repository.ts index 2dd8f24..376b806 100644 --- a/src/api/rpm-repository.ts +++ b/src/api/rpm-repository.ts @@ -33,6 +33,9 @@ interface RPMRepositoryType extends GenericRepository { const base = new PulpAPI(); +/** + * @deprecated Use `RpmClient` from `src/api/plugins/rpm/client.ts` instead. + */ export const RPMRepositoryAPI = { list: (params?) => base.list(`repositories/rpm/rpm/`, params), }; diff --git a/src/api/test-utils/integration-client.ts b/src/api/test-utils/integration-client.ts new file mode 100644 index 0000000..a52311e --- /dev/null +++ b/src/api/test-utils/integration-client.ts @@ -0,0 +1,104 @@ +import axios, { type AxiosRequestConfig, type AxiosResponse } from 'axios'; +import type { PulpAPI } from '../pulp.ts'; + +const baseUrl: string = + process.env.PULP_BASE_URL ?? 'http://localhost:8080/pulp/api/v3/'; +const testUsername: string = process.env.PULP_USERNAME ?? 'admin'; +const testPassword: string = process.env.PULP_PASSWORD ?? 'admin'; +const authorization: string = + 'Basic ' + Buffer.from(`${testUsername}:${testPassword}`).toString('base64'); + +async function testAxiosClient( + path: string, + config: AxiosRequestConfig = {}, +): Promise { + const url: string = new URL(path, baseUrl).toString(); + const axiosRequest = { + ...config, + url, + headers: { + Authorization: authorization, + 'Content-Type': 'application/json', + ...config.headers, + }, + } satisfies AxiosRequestConfig; + const res = await axios.request(axiosRequest); + + return res; +} + +function testPulpAPI(): PulpAPI { + return { + list: (url: string, params?: Record) => + testAxiosClient(`${url}${buildQueryParams(params)}`, { + method: 'GET', + }), + http: { + get: (url: string, config: { params?: Record }) => + testAxiosClient(`${url}${buildQueryParams(config?.params)}`, { + method: 'GET', + }), + post: (url: string, data: unknown) => + testAxiosClient(url, { method: 'POST', data }), + patch: ( + url: string, + data: unknown, + config: { params?: Record }, + ) => + testAxiosClient(`${url}${buildQueryParams(config?.params)}`, { + method: 'PATCH', + data, + }), + delete: (url: string, config: { params?: Record }) => + testAxiosClient(`${url}${buildQueryParams(config?.params)}`, { + method: 'DELETE', + }), + }, + } as unknown as PulpAPI; +} + +function buildQueryParams(params?: Record): string { + const search = new URLSearchParams(); + + for (const [key, value] of Object.entries(params ?? {})) { + if (value !== undefined) { + search.set(key, String(value)); + } + } + + const queryString = search.toString(); + return queryString ? `?${queryString}` : ''; +} + +async function waitForTaskCompletion( + taskHref: string, + { + waitMs = 500, + attemptsLeft = 10, + }: { waitMs?: number; attemptsLeft?: number } = {}, +): Promise { + const res = await testAxiosClient(taskHref, { method: 'GET' }); + const state: string = res.data.state; + + if (['skipped', 'failed', 'canceled'].includes(state)) { + throw new Error(`Task ${taskHref} ended with state "${state}"`); + } + + if (state === 'completed') { + return; + } + + if (attemptsLeft <= 0) { + throw new Error( + `Task ${taskHref} did not complete within the allowed attempts`, + ); + } + + await new Promise((r) => setTimeout(r, waitMs)); + return waitForTaskCompletion(taskHref, { + waitMs: Math.round(waitMs * 1.5), + attemptsLeft: attemptsLeft - 1, + }); +} + +export { testPulpAPI, testAxiosClient, waitForTaskCompletion }; diff --git a/tsconfig.json b/tsconfig.json index ab414f1..7bf8359 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "jsx": "react-jsx", "lib": ["es2021", "dom"], "module": "es2020", + "allowImportingTsExtensions": true, "moduleResolution": "bundler", "noImplicitAny": false, "outDir": "./dist/",