diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 691ae68a..0bf07c06 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,11 @@ on: branches: - main +permissions: + contents: read + actions: read + checks: write + jobs: test: runs-on: ubuntu-latest @@ -49,5 +54,56 @@ jobs: - name: Build packages run: pnpm -r run build - - name: Run tests and lint - run: pnpm -r run test + - name: Run SDK tests + id: sdk-test + working-directory: packages/b2c-tooling-sdk + run: pnpm run test:ci && pnpm run lint + + - name: Run CLI tests + id: cli-test + if: always() && steps.sdk-test.conclusion != 'cancelled' + working-directory: packages/b2c-cli + run: pnpm run test:ci && pnpm run lint + + - name: Test Report + uses: dorny/test-reporter@fe45e9537387dac839af0d33ba56eed8e24189e8 # v2.3.0 + if: always() && steps.sdk-test.conclusion != 'cancelled' + with: + name: Test Results (Node ${{ matrix.node-version }}) + path: 'packages/*/test-results.json' + reporter: mocha-json + + - name: Generate coverage summary + if: always() && steps.sdk-test.conclusion != 'cancelled' + run: | + echo "## Coverage (Node ${{ matrix.node-version }})" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # SDK Coverage + if [ -f "packages/b2c-tooling-sdk/coverage/lcov.info" ]; then + echo "### @salesforce/b2c-tooling-sdk" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + cd packages/b2c-tooling-sdk && pnpm c8 report --reporter=text-summary 2>/dev/null | tail -n 6 >> $GITHUB_STEP_SUMMARY + cd ../.. + echo '```' >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + fi + + # CLI Coverage + if [ -f "packages/b2c-cli/coverage/lcov.info" ]; then + echo "### @salesforce/b2c-cli" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + cd packages/b2c-cli && pnpm c8 report --reporter=text-summary 2>/dev/null | tail -n 6 >> $GITHUB_STEP_SUMMARY + cd ../.. + echo '```' >> $GITHUB_STEP_SUMMARY + fi + + - name: Upload coverage reports + if: always() && steps.sdk-test.conclusion != 'cancelled' + uses: actions/upload-artifact@v4 + with: + name: coverage-reports-node-${{ matrix.node-version }} + path: | + packages/b2c-tooling-sdk/coverage/ + packages/b2c-cli/coverage/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index 12cdd3f2..44890c0c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /dist /tmp /node_modules +/coverage oclif.manifest.json diff --git a/AGENTS.md b/AGENTS.md index 9c2753ff..94682b76 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -79,3 +79,75 @@ Features: - Dynamic column widths based on content - Supports `extended` flag on columns for optional fields - Use `TableRenderer` class directly for column validation helpers (e.g., `--columns` flag support) + +## Testing + +Tests use Mocha + Chai with c8 for coverage. HTTP mocking uses MSW (Mock Service Worker). + +### Running Tests + +```bash +# Run all tests with coverage +pnpm run test + +# Run tests for specific package +pnpm --filter @salesforce/b2c-tooling-sdk run test + +# Run single test file (no coverage, faster) +cd packages/b2c-tooling-sdk +pnpm mocha "test/clients/webdav.test.ts" + +# Run tests matching pattern +pnpm mocha --grep "mkcol" "test/**/*.test.ts" + +# Watch mode for TDD +pnpm --filter @salesforce/b2c-tooling-sdk run test:watch +``` + +### Writing Tests + +- Place tests in `packages//test/` mirroring the src structure +- Use `.test.ts` suffix for test files +- Import from package names, not relative paths: + ```typescript + // Good - uses package exports + import { WebDavClient } from '@salesforce/b2c-tooling-sdk/clients'; + + // Avoid - relative paths + import { WebDavClient } from '../../src/clients/webdav.js'; + ``` + +### HTTP Mocking with MSW + +For testing HTTP clients, use MSW to mock at the network level: + +```typescript +import { http, HttpResponse } from 'msw'; +import { setupServer } from 'msw/node'; + +const server = setupServer(); + +before(() => server.listen({ onUnhandledRequest: 'error' })); +afterEach(() => server.resetHandlers()); +after(() => server.close()); + +it('makes HTTP request', async () => { + server.use( + http.get('https://example.com/api/*', () => { + return HttpResponse.json({ data: 'test' }); + }), + ); + + // Test code that makes HTTP requests... +}); +``` + +### Test Helpers + +- `test/helpers/mock-auth.ts` - Mock AuthStrategy for testing HTTP clients + +### Coverage + +- Coverage reports generated in `coverage/` directory +- SDK package has 5% minimum threshold (will increase as tests are added) +- CI publishes coverage summary to GitHub Actions job summary diff --git a/README.md b/README.md index d12b3aa0..a71ec116 100644 --- a/README.md +++ b/README.md @@ -59,12 +59,42 @@ pnpm --filter @salesforce/b2c-cli run build pnpm --filter @salesforce/b2c-tooling-sdk run build ``` -### Testing and Linting +### Testing + +Tests use [Mocha](https://mochajs.org/) + [Chai](https://www.chaijs.com/) with [c8](https://github.com/bcoe/c8) for coverage. HTTP mocking uses [MSW](https://mswjs.io/). ```bash -# Run all tests (also runs linter after tests) +# Run all tests with coverage (also runs linter after tests) pnpm test +# Run tests for a specific package +pnpm --filter @salesforce/b2c-tooling-sdk run test + +# Run tests without coverage (faster) +pnpm --filter @salesforce/b2c-tooling-sdk run test:unit + +# Watch mode for TDD +pnpm --filter @salesforce/b2c-tooling-sdk run test:watch + +# Run a specific test file +cd packages/b2c-tooling-sdk +pnpm mocha "test/clients/webdav.test.ts" + +# Run tests matching a pattern +pnpm mocha --grep "uploads a file" "test/**/*.test.ts" +``` + +#### Coverage + +Coverage reports are generated in each package's `coverage/` directory: +- `coverage/index.html` - HTML report +- `coverage/lcov.info` - LCOV format for CI integration + +The SDK package has a 5% coverage threshold that will fail the build if not met. + +### Linting + +```bash # Run linter only pnpm --filter @salesforce/b2c-cli run lint pnpm --filter @salesforce/b2c-tooling-sdk run lint diff --git a/package.json b/package.json index ba7b2bc1..44c52677 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,8 @@ "scripts": { "start": "pnpm --filter @salesforce/b2c-cli run dev", "test": "pnpm -r test", + "test:unit": "pnpm -r run test:unit", + "coverage": "pnpm -r run coverage", "format": "pnpm -r run format", "lint": "pnpm -r run lint", "build": "pnpm -r run build", @@ -16,7 +18,7 @@ }, "keywords": [], "author": "", - "license": "ISC", + "license": "MIT", "packageManager": "pnpm@10.17.1", "devDependencies": { "typedoc": "^0.28.14", diff --git a/packages/b2c-cli/.c8rc.json b/packages/b2c-cli/.c8rc.json new file mode 100644 index 00000000..7bd1b1b3 --- /dev/null +++ b/packages/b2c-cli/.c8rc.json @@ -0,0 +1,10 @@ +{ + "all": true, + "src": ["src"], + "exclude": [ + "test/**", + "**/*.d.ts" + ], + "reporter": ["text", "text-summary", "html", "lcov"], + "report-dir": "coverage" +} diff --git a/packages/b2c-cli/.gitignore b/packages/b2c-cli/.gitignore index a3426a1a..0fd5a82f 100644 --- a/packages/b2c-cli/.gitignore +++ b/packages/b2c-cli/.gitignore @@ -6,6 +6,8 @@ /build /tmp /node_modules +/coverage +test-results.json oclif.manifest.json diff --git a/packages/b2c-cli/package.json b/packages/b2c-cli/package.json index 5ae7dc31..8d6f105d 100644 --- a/packages/b2c-cli/package.json +++ b/packages/b2c-cli/package.json @@ -26,6 +26,7 @@ "@types/chai": "^4", "@types/mocha": "^10", "@types/node": "^18", + "c8": "^10.1.3", "chai": "^4", "eslint": "^9", "eslint-config-oclif": "^6", @@ -116,7 +117,10 @@ "posttest": "pnpm run lint", "prepack": "oclif manifest && oclif readme", "pretest": "tsc --noEmit -p test", - "test": "OCLIF_TEST_ROOT=. mocha --forbid-only \"test/**/*.test.ts\"", + "test": "c8 env OCLIF_TEST_ROOT=. mocha --forbid-only \"test/**/*.test.ts\"", + "test:ci": "c8 env OCLIF_TEST_ROOT=. mocha --forbid-only --reporter json --reporter-option output=test-results.json \"test/**/*.test.ts\"", + "test:unit": "env OCLIF_TEST_ROOT=. mocha --forbid-only \"test/**/*.test.ts\"", + "coverage": "c8 report", "version": "oclif readme && git add README.md", "dev": "node ./bin/dev.js" }, diff --git a/packages/b2c-tooling-sdk/.c8rc.json b/packages/b2c-tooling-sdk/.c8rc.json new file mode 100644 index 00000000..d4459b30 --- /dev/null +++ b/packages/b2c-tooling-sdk/.c8rc.json @@ -0,0 +1,16 @@ +{ + "all": true, + "src": ["src"], + "exclude": [ + "src/clients/*.generated.ts", + "test/**", + "**/*.d.ts" + ], + "reporter": ["text", "text-summary", "html", "lcov"], + "report-dir": "coverage", + "check-coverage": true, + "lines": 5, + "functions": 5, + "branches": 5, + "statements": 5 +} diff --git a/packages/b2c-tooling-sdk/.gitignore b/packages/b2c-tooling-sdk/.gitignore index 1f32c5c2..95502d3d 100644 --- a/packages/b2c-tooling-sdk/.gitignore +++ b/packages/b2c-tooling-sdk/.gitignore @@ -5,6 +5,8 @@ /dist /tmp /node_modules +/coverage +test-results.json yarn.lock diff --git a/packages/b2c-tooling-sdk/.mocharc.json b/packages/b2c-tooling-sdk/.mocharc.json new file mode 100644 index 00000000..1722876a --- /dev/null +++ b/packages/b2c-tooling-sdk/.mocharc.json @@ -0,0 +1,6 @@ +{ + "node-option": ["import=tsx", "conditions=development"], + "timeout": 10000, + "recursive": true, + "extension": ["ts"] +} diff --git a/packages/b2c-tooling-sdk/eslint.config.mjs b/packages/b2c-tooling-sdk/eslint.config.mjs index 41eb685d..134f0960 100644 --- a/packages/b2c-tooling-sdk/eslint.config.mjs +++ b/packages/b2c-tooling-sdk/eslint.config.mjs @@ -49,4 +49,11 @@ export default [ 'new-cap': 'off', }, }, + { + // Allow Chai property-based assertions in test files (e.g., expect(x).to.be.true) + files: ['test/**/*.ts'], + rules: { + '@typescript-eslint/no-unused-expressions': 'off', + }, + }, ]; diff --git a/packages/b2c-tooling-sdk/package.json b/packages/b2c-tooling-sdk/package.json index 3c647f81..b63d7bdd 100644 --- a/packages/b2c-tooling-sdk/package.json +++ b/packages/b2c-tooling-sdk/package.json @@ -162,7 +162,12 @@ "lint": "eslint", "format": "prettier --write src", "format:check": "prettier --check src", - "test": "echo \"Error: no test specified\" && exit 0", + "pretest": "tsc --noEmit -p test", + "test": "c8 mocha --forbid-only \"test/**/*.test.ts\"", + "test:ci": "c8 mocha --forbid-only --reporter json --reporter-option output=test-results.json \"test/**/*.test.ts\"", + "test:unit": "mocha --forbid-only \"test/**/*.test.ts\"", + "test:watch": "mocha --watch \"test/**/*.test.ts\"", + "coverage": "c8 report", "posttest": "pnpm run lint" }, "devDependencies": { @@ -172,14 +177,21 @@ "@salesforce/dev-config": "^4.3.2", "@tony.ganchev/eslint-plugin-header": "^3.1.11", "@types/archiver": "^7.0.0", + "@types/chai": "^4.3.20", + "@types/mocha": "^10.0.10", "@types/node": "^18.19.130", "@types/xml2js": "^0.4.14", + "c8": "^10.1.3", + "chai": "^4.5.0", "eslint": "^9", "eslint-config-prettier": "^10", "eslint-plugin-prettier": "^5.5.4", + "mocha": "^10.8.2", + "msw": "^2.12.4", "openapi-typescript": "^7.10.1", "prettier": "^3.6.2", "shx": "^0.3.3", + "tsx": "^4.20.6", "typescript": "^5", "typescript-eslint": "^8" }, diff --git a/packages/b2c-tooling-sdk/test/auth/resolve.test.ts b/packages/b2c-tooling-sdk/test/auth/resolve.test.ts new file mode 100644 index 00000000..3a2d7eaa --- /dev/null +++ b/packages/b2c-tooling-sdk/test/auth/resolve.test.ts @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {expect} from 'chai'; +import {checkAvailableAuthMethods} from '@salesforce/b2c-tooling-sdk/auth'; + +describe('auth/resolve', () => { + describe('checkAvailableAuthMethods', () => { + it('returns client-credentials when clientId and clientSecret are provided', () => { + const result = checkAvailableAuthMethods({ + clientId: 'test-client', + clientSecret: 'test-secret', + }); + + expect(result.available).to.include('client-credentials'); + }); + + it('returns implicit when only clientId is provided', () => { + const result = checkAvailableAuthMethods({ + clientId: 'test-client', + }); + + expect(result.available).to.include('implicit'); + expect(result.available).to.not.include('client-credentials'); + }); + + it('returns basic when username and password are provided', () => { + const result = checkAvailableAuthMethods({ + username: 'test-user', + password: 'test-pass', + }); + + expect(result.available).to.include('basic'); + }); + + it('returns api-key when apiKey is provided', () => { + const result = checkAvailableAuthMethods({ + apiKey: 'test-api-key', + }); + + expect(result.available).to.include('api-key'); + }); + + it('returns unavailable with reason when clientSecret is missing for client-credentials', () => { + const result = checkAvailableAuthMethods({clientId: 'test-client'}, ['client-credentials']); + + expect(result.available).to.have.length(0); + expect(result.unavailable).to.have.length(1); + expect(result.unavailable[0]).to.deep.equal({ + method: 'client-credentials', + reason: 'clientSecret is required', + }); + }); + + it('returns unavailable with reason when clientId is missing', () => { + const result = checkAvailableAuthMethods({}, ['client-credentials', 'implicit']); + + expect(result.unavailable).to.have.length(2); + expect(result.unavailable[0].reason).to.equal('clientId is required'); + expect(result.unavailable[1].reason).to.equal('clientId is required'); + }); + + it('only checks allowed methods', () => { + const result = checkAvailableAuthMethods( + { + clientId: 'test-client', + clientSecret: 'test-secret', + username: 'test-user', + password: 'test-pass', + }, + ['basic'], + ); + + expect(result.available).to.deep.equal(['basic']); + expect(result.unavailable).to.have.length(0); + }); + + it('returns all available methods when credentials support multiple', () => { + const result = checkAvailableAuthMethods({ + clientId: 'test-client', + clientSecret: 'test-secret', + username: 'test-user', + password: 'test-pass', + apiKey: 'test-key', + }); + + expect(result.available).to.have.length(4); + expect(result.available).to.include('client-credentials'); + expect(result.available).to.include('implicit'); + expect(result.available).to.include('basic'); + expect(result.available).to.include('api-key'); + }); + }); +}); diff --git a/packages/b2c-tooling-sdk/test/clients/mrt.test.ts b/packages/b2c-tooling-sdk/test/clients/mrt.test.ts new file mode 100644 index 00000000..34799d06 --- /dev/null +++ b/packages/b2c-tooling-sdk/test/clients/mrt.test.ts @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {expect} from 'chai'; +import {http, HttpResponse} from 'msw'; +import {setupServer} from 'msw/node'; +import {createMrtClient, DEFAULT_MRT_ORIGIN} from '@salesforce/b2c-tooling-sdk/clients'; +import {MockAuthStrategy} from '../helpers/mock-auth.js'; + +const DEFAULT_BASE_URL = DEFAULT_MRT_ORIGIN; + +describe('clients/mrt', () => { + describe('createMrtClient', () => { + const server = setupServer(); + + before(() => { + server.listen({onUnhandledRequest: 'error'}); + }); + + afterEach(() => { + server.resetHandlers(); + }); + + after(() => { + server.close(); + }); + + it('creates a client with default origin', async () => { + server.use( + http.get(`${DEFAULT_BASE_URL}/api/projects/`, ({request}) => { + expect(request.headers.get('Authorization')).to.equal('Bearer test-token'); + return HttpResponse.json({results: []}); + }), + ); + + const auth = new MockAuthStrategy(); + const client = createMrtClient({}, auth); + + const {data, error} = await client.GET('/api/projects/', {}); + + expect(error).to.be.undefined; + expect(data).to.deep.equal({results: []}); + }); + + it('creates a client with custom origin', async () => { + const customOrigin = 'https://custom.mobify.com'; + + server.use( + http.get(`${customOrigin}/api/projects/`, () => { + return HttpResponse.json({results: [{slug: 'test-project'}]}); + }), + ); + + const auth = new MockAuthStrategy(); + const client = createMrtClient({origin: customOrigin}, auth); + + const {data} = await client.GET('/api/projects/', {}); + + expect(data?.results).to.have.length(1); + }); + + it('normalizes origin without protocol', async () => { + server.use( + http.get('https://custom.mobify.com/api/projects/', () => { + return HttpResponse.json({results: []}); + }), + ); + + const auth = new MockAuthStrategy(); + // Origin without https:// prefix + const client = createMrtClient({origin: 'custom.mobify.com'}, auth); + + const {error} = await client.GET('/api/projects/', {}); + + expect(error).to.be.undefined; + }); + + it('makes authenticated requests with path parameters', async () => { + server.use( + http.get(`${DEFAULT_BASE_URL}/api/projects/:project_slug/`, ({params}) => { + return HttpResponse.json({ + slug: params.project_slug, + name: 'Test Project', + }); + }), + ); + + const auth = new MockAuthStrategy(); + const client = createMrtClient({}, auth); + + const {data} = await client.GET('/api/projects/{project_slug}/', { + params: {path: {project_slug: 'my-project'}}, + }); + + expect(data?.slug).to.equal('my-project'); + }); + + it('handles POST requests with body', async () => { + let receivedBody: unknown; + + server.use( + http.post(`${DEFAULT_BASE_URL}/api/projects/:project_slug/builds/`, async ({request}) => { + receivedBody = await request.json(); + return HttpResponse.json({ + bundle_id: 123, + message: 'Bundle created', + url: 'https://runtime.commercecloud.com/...', + bundle_preview_url: null, + warnings: [], + }); + }), + ); + + const auth = new MockAuthStrategy(); + const client = createMrtClient({}, auth); + + const {data} = await client.POST('/api/projects/{project_slug}/builds/', { + params: {path: {project_slug: 'my-project'}}, + body: { + message: 'Test bundle', + encoding: 'base64', + data: 'dGVzdA==', + ssr_parameters: {}, + ssr_only: ['ssr.js'], + ssr_shared: ['shared.js'], + }, + }); + + expect(receivedBody).to.deep.include({ + message: 'Test bundle', + encoding: 'base64', + }); + expect(data).to.have.property('bundle_id'); + }); + + it('handles API errors', async () => { + server.use( + http.get(`${DEFAULT_BASE_URL}/api/projects/:project_slug/`, () => { + return HttpResponse.json({detail: 'Project not found'}, {status: 404}); + }), + ); + + const auth = new MockAuthStrategy(); + const client = createMrtClient({}, auth); + + const {data, error} = await client.GET('/api/projects/{project_slug}/', { + params: {path: {project_slug: 'nonexistent'}}, + }); + + expect(data).to.be.undefined; + expect(error).to.deep.equal({detail: 'Project not found'}); + }); + }); +}); diff --git a/packages/b2c-tooling-sdk/test/clients/webdav.test.ts b/packages/b2c-tooling-sdk/test/clients/webdav.test.ts new file mode 100644 index 00000000..bd438b09 --- /dev/null +++ b/packages/b2c-tooling-sdk/test/clients/webdav.test.ts @@ -0,0 +1,458 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {expect} from 'chai'; +import {http, HttpResponse} from 'msw'; +import {setupServer} from 'msw/node'; +import {WebDavClient} from '@salesforce/b2c-tooling-sdk/clients'; +import {HTTPError} from '@salesforce/b2c-tooling-sdk/errors'; +import {MockAuthStrategy} from '../helpers/mock-auth.js'; + +const TEST_HOST = 'test.demandware.net'; +const BASE_URL = `https://${TEST_HOST}/on/demandware.servlet/webdav/Sites`; + +// Sample PROPFIND XML response +const PROPFIND_RESPONSE = ` + + + /on/demandware.servlet/webdav/Sites/Cartridges/ + + + Cartridges + + Mon, 01 Jan 2024 00:00:00 GMT + + HTTP/1.1 200 OK + + + + /on/demandware.servlet/webdav/Sites/Cartridges/app_storefront.zip + + + app_storefront.zip + + 12345 + Tue, 02 Jan 2024 12:00:00 GMT + application/zip + + HTTP/1.1 200 OK + + +`; + +describe('clients/webdav', () => { + describe('WebDavClient', () => { + let client: WebDavClient; + let mockAuth: MockAuthStrategy; + + // Track requests for assertions + const requests: {method: string; url: string; headers: Headers; body?: string}[] = []; + + const server = setupServer(); + + before(() => { + server.listen({onUnhandledRequest: 'error'}); + }); + + afterEach(() => { + server.resetHandlers(); + requests.length = 0; + }); + + after(() => { + server.close(); + }); + + beforeEach(() => { + mockAuth = new MockAuthStrategy(); + client = new WebDavClient(TEST_HOST, mockAuth); + }); + + describe('buildUrl', () => { + it('builds correct URL for path without leading slash', () => { + const url = client.buildUrl('Cartridges/v1'); + expect(url).to.equal(`${BASE_URL}/Cartridges/v1`); + }); + + it('builds correct URL for path with leading slash', () => { + const url = client.buildUrl('/Cartridges/v1'); + expect(url).to.equal(`${BASE_URL}/Cartridges/v1`); + }); + }); + + describe('mkcol', () => { + it('creates a directory successfully', async () => { + server.use( + http.all(`${BASE_URL}/*`, async ({request}) => { + requests.push({ + method: request.method, + url: request.url, + headers: request.headers, + }); + + if (request.method === 'MKCOL') { + return new HttpResponse(null, {status: 201}); + } + return new HttpResponse(null, {status: 405}); + }), + ); + + await client.mkcol('Cartridges/v1'); + + expect(requests).to.have.length(1); + expect(requests[0].method).to.equal('MKCOL'); + expect(requests[0].url).to.equal(`${BASE_URL}/Cartridges/v1`); + expect(requests[0].headers.get('Authorization')).to.equal('Bearer test-token'); + }); + + it('does not throw when directory already exists (405)', async () => { + server.use( + http.all(`${BASE_URL}/*`, ({request}) => { + if (request.method === 'MKCOL') { + return new HttpResponse(null, {status: 405, statusText: 'Method Not Allowed'}); + } + return new HttpResponse(null, {status: 404}); + }), + ); + + // Should not throw + await client.mkcol('Cartridges/existing'); + }); + + it('throws HTTPError on failure', async () => { + server.use( + http.all(`${BASE_URL}/*`, ({request}) => { + if (request.method === 'MKCOL') { + return new HttpResponse(null, {status: 403, statusText: 'Forbidden'}); + } + return new HttpResponse(null, {status: 404}); + }), + ); + + try { + await client.mkcol('Cartridges/forbidden'); + expect.fail('Should have thrown'); + } catch (error) { + expect(error).to.be.instanceOf(HTTPError); + expect((error as HTTPError).message).to.include('MKCOL failed'); + expect((error as HTTPError).message).to.include('403'); + } + }); + }); + + describe('put', () => { + it('uploads a file successfully', async () => { + server.use( + http.all(`${BASE_URL}/*`, async ({request}) => { + const body = await request.text(); + requests.push({ + method: request.method, + url: request.url, + headers: request.headers, + body, + }); + + if (request.method === 'PUT') { + return new HttpResponse(null, {status: 201}); + } + return new HttpResponse(null, {status: 404}); + }), + ); + + await client.put('Cartridges/v1/test.zip', Buffer.from('zip content'), 'application/zip'); + + expect(requests).to.have.length(1); + expect(requests[0].method).to.equal('PUT'); + expect(requests[0].url).to.equal(`${BASE_URL}/Cartridges/v1/test.zip`); + expect(requests[0].headers.get('Content-Type')).to.equal('application/zip'); + expect(requests[0].body).to.equal('zip content'); + }); + + it('uploads string content', async () => { + server.use( + http.all(`${BASE_URL}/*`, async ({request}) => { + const body = await request.text(); + requests.push({ + method: request.method, + url: request.url, + headers: request.headers, + body, + }); + + if (request.method === 'PUT') { + return new HttpResponse(null, {status: 201}); + } + return new HttpResponse(null, {status: 404}); + }), + ); + + await client.put('Cartridges/v1/config.xml', 'test', 'application/xml'); + + expect(requests[0].body).to.equal('test'); + }); + + it('throws HTTPError on failure', async () => { + server.use( + http.all(`${BASE_URL}/*`, ({request}) => { + if (request.method === 'PUT') { + return new HttpResponse(null, {status: 507, statusText: 'Insufficient Storage'}); + } + return new HttpResponse(null, {status: 404}); + }), + ); + + try { + await client.put('Cartridges/v1/large.zip', Buffer.from('content')); + expect.fail('Should have thrown'); + } catch (error) { + expect(error).to.be.instanceOf(HTTPError); + expect((error as HTTPError).message).to.include('PUT failed'); + expect((error as HTTPError).message).to.include('507'); + } + }); + }); + + describe('get', () => { + it('downloads a file successfully', async () => { + const fileContent = 'file content here'; + + server.use( + http.all(`${BASE_URL}/*`, ({request}) => { + requests.push({ + method: request.method, + url: request.url, + headers: request.headers, + }); + + if (request.method === 'GET') { + return new HttpResponse(fileContent, { + status: 200, + headers: {'Content-Type': 'application/octet-stream'}, + }); + } + return new HttpResponse(null, {status: 404}); + }), + ); + + const result = await client.get('Cartridges/v1/test.zip'); + + expect(requests).to.have.length(1); + expect(requests[0].method).to.equal('GET'); + expect(result).to.be.instanceOf(ArrayBuffer); + + const text = new TextDecoder().decode(result); + expect(text).to.equal(fileContent); + }); + + it('throws HTTPError when file not found', async () => { + server.use( + http.all(`${BASE_URL}/*`, ({request}) => { + if (request.method === 'GET') { + return new HttpResponse(null, {status: 404, statusText: 'Not Found'}); + } + return new HttpResponse(null, {status: 404}); + }), + ); + + try { + await client.get('Cartridges/v1/nonexistent.zip'); + expect.fail('Should have thrown'); + } catch (error) { + expect(error).to.be.instanceOf(HTTPError); + expect((error as HTTPError).message).to.include('GET failed'); + expect((error as HTTPError).message).to.include('404'); + } + }); + }); + + describe('delete', () => { + it('deletes a file successfully', async () => { + server.use( + http.all(`${BASE_URL}/*`, ({request}) => { + requests.push({ + method: request.method, + url: request.url, + headers: request.headers, + }); + + if (request.method === 'DELETE') { + return new HttpResponse(null, {status: 204}); + } + return new HttpResponse(null, {status: 404}); + }), + ); + + await client.delete('Cartridges/v1/old.zip'); + + expect(requests).to.have.length(1); + expect(requests[0].method).to.equal('DELETE'); + expect(requests[0].url).to.equal(`${BASE_URL}/Cartridges/v1/old.zip`); + }); + + it('throws HTTPError when file not found', async () => { + server.use( + http.all(`${BASE_URL}/*`, ({request}) => { + if (request.method === 'DELETE') { + return new HttpResponse(null, {status: 404, statusText: 'Not Found'}); + } + return new HttpResponse(null, {status: 404}); + }), + ); + + try { + await client.delete('Cartridges/v1/nonexistent.zip'); + expect.fail('Should have thrown'); + } catch (error) { + expect(error).to.be.instanceOf(HTTPError); + expect((error as HTTPError).message).to.include('DELETE failed'); + } + }); + }); + + describe('propfind', () => { + it('lists directory contents', async () => { + server.use( + http.all(`${BASE_URL}/*`, ({request}) => { + requests.push({ + method: request.method, + url: request.url, + headers: request.headers, + }); + + if (request.method === 'PROPFIND') { + return new HttpResponse(PROPFIND_RESPONSE, { + status: 207, + headers: {'Content-Type': 'application/xml'}, + }); + } + return new HttpResponse(null, {status: 404}); + }), + ); + + const entries = await client.propfind('Cartridges'); + + expect(requests).to.have.length(1); + expect(requests[0].method).to.equal('PROPFIND'); + expect(requests[0].headers.get('Depth')).to.equal('1'); + expect(requests[0].headers.get('Content-Type')).to.equal('application/xml'); + + expect(entries).to.have.length(2); + + // First entry is a collection (directory) + expect(entries[0].displayName).to.equal('Cartridges'); + expect(entries[0].isCollection).to.equal(true); + + // Second entry is a file + expect(entries[1].displayName).to.equal('app_storefront.zip'); + expect(entries[1].isCollection).to.equal(false); + expect(entries[1].contentLength).to.equal(12345); + expect(entries[1].contentType).to.equal('application/zip'); + }); + + it('uses specified depth', async () => { + server.use( + http.all(`${BASE_URL}/*`, ({request}) => { + requests.push({ + method: request.method, + url: request.url, + headers: request.headers, + }); + + if (request.method === 'PROPFIND') { + return new HttpResponse(PROPFIND_RESPONSE, { + status: 207, + headers: {'Content-Type': 'application/xml'}, + }); + } + return new HttpResponse(null, {status: 404}); + }), + ); + + await client.propfind('Cartridges', '0'); + + expect(requests[0].headers.get('Depth')).to.equal('0'); + }); + + it('throws HTTPError on failure', async () => { + server.use( + http.all(`${BASE_URL}/*`, ({request}) => { + if (request.method === 'PROPFIND') { + return new HttpResponse(null, {status: 403, statusText: 'Forbidden'}); + } + return new HttpResponse(null, {status: 404}); + }), + ); + + try { + await client.propfind('Cartridges/forbidden'); + expect.fail('Should have thrown'); + } catch (error) { + expect(error).to.be.instanceOf(HTTPError); + expect((error as HTTPError).message).to.include('PROPFIND failed'); + } + }); + + it('handles empty response', async () => { + const emptyResponse = ` + +`; + + server.use( + http.all(`${BASE_URL}/*`, ({request}) => { + if (request.method === 'PROPFIND') { + return new HttpResponse(emptyResponse, { + status: 207, + headers: {'Content-Type': 'application/xml'}, + }); + } + return new HttpResponse(null, {status: 404}); + }), + ); + + const entries = await client.propfind('Cartridges/empty'); + expect(entries).to.have.length(0); + }); + }); + + describe('exists', () => { + it('returns true when path exists', async () => { + server.use( + http.all(`${BASE_URL}/*`, ({request}) => { + requests.push({ + method: request.method, + url: request.url, + headers: request.headers, + }); + + if (request.method === 'HEAD') { + return new HttpResponse(null, {status: 200}); + } + return new HttpResponse(null, {status: 404}); + }), + ); + + const result = await client.exists('Cartridges/v1'); + + expect(result).to.equal(true); + expect(requests[0].method).to.equal('HEAD'); + }); + + it('returns false when path does not exist', async () => { + server.use( + http.all(`${BASE_URL}/*`, ({request}) => { + if (request.method === 'HEAD') { + return new HttpResponse(null, {status: 404}); + } + return new HttpResponse(null, {status: 404}); + }), + ); + + const result = await client.exists('Cartridges/nonexistent'); + + expect(result).to.equal(false); + }); + }); + }); +}); diff --git a/packages/b2c-tooling-sdk/test/helpers/mock-auth.ts b/packages/b2c-tooling-sdk/test/helpers/mock-auth.ts new file mode 100644 index 00000000..5db81be0 --- /dev/null +++ b/packages/b2c-tooling-sdk/test/helpers/mock-auth.ts @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import type {AuthStrategy} from '@salesforce/b2c-tooling-sdk/auth'; + +/** + * Mock auth strategy for testing. + * Simply passes through fetch requests with an Authorization header. + */ +export class MockAuthStrategy implements AuthStrategy { + constructor(private token: string = 'test-token') {} + + async fetch(url: string, init?: RequestInit): Promise { + const headers = new Headers(init?.headers); + headers.set('Authorization', `Bearer ${this.token}`); + + return fetch(url, { + ...init, + headers, + }); + } + + async getAuthorizationHeader(): Promise { + return `Bearer ${this.token}`; + } +} diff --git a/packages/b2c-tooling-sdk/test/operations/mrt/env-var.test.ts b/packages/b2c-tooling-sdk/test/operations/mrt/env-var.test.ts new file mode 100644 index 00000000..aeaeb8aa --- /dev/null +++ b/packages/b2c-tooling-sdk/test/operations/mrt/env-var.test.ts @@ -0,0 +1,351 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {expect} from 'chai'; +import {http, HttpResponse} from 'msw'; +import {setupServer} from 'msw/node'; +import {DEFAULT_MRT_ORIGIN} from '@salesforce/b2c-tooling-sdk/clients'; +import {listEnvVars, setEnvVar, setEnvVars, deleteEnvVar} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {MockAuthStrategy} from '../../helpers/mock-auth.js'; + +const DEFAULT_BASE_URL = DEFAULT_MRT_ORIGIN; + +describe('operations/mrt/env-var', () => { + const server = setupServer(); + + before(() => { + server.listen({onUnhandledRequest: 'error'}); + }); + + afterEach(() => { + server.resetHandlers(); + }); + + after(() => { + server.close(); + }); + + describe('listEnvVars', () => { + it('lists environment variables with paginated format', async () => { + server.use( + http.get(`${DEFAULT_BASE_URL}/api/projects/:projectSlug/target/:targetSlug/env-var/`, () => { + return HttpResponse.json({ + count: 2, + next: null, + previous: null, + results: [ + { + API_KEY: { + value: '***key', + created_by: 'user@example.com', + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-02T00:00:00Z', + updated_by: 'user@example.com', + publishing_status: 1, + publishing_status_description: 'published', + }, + }, + { + SECRET: { + value: '***ret', + created_by: 'admin@example.com', + created_at: '2025-01-03T00:00:00Z', + updated_at: '2025-01-03T00:00:00Z', + updated_by: 'admin@example.com', + publishing_status: 1, + publishing_status_description: 'published', + }, + }, + ], + }); + }), + ); + + const auth = new MockAuthStrategy(); + const result = await listEnvVars( + { + projectSlug: 'my-project', + environment: 'staging', + }, + auth, + ); + + expect(result.count).to.equal(2); + expect(result.variables).to.have.length(2); + expect(result.variables[0]).to.deep.include({ + name: 'API_KEY', + value: '***key', + createdBy: 'user@example.com', + }); + expect(result.variables[1]).to.deep.include({ + name: 'SECRET', + value: '***ret', + }); + }); + + it('lists environment variables with direct object format', async () => { + server.use( + http.get(`${DEFAULT_BASE_URL}/api/projects/:projectSlug/target/:targetSlug/env-var/`, () => { + return HttpResponse.json({ + API_KEY: { + value: '***key', + created_by: 'user@example.com', + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-02T00:00:00Z', + updated_by: 'user@example.com', + publishing_status: 1, + publishing_status_description: 'published', + }, + }); + }), + ); + + const auth = new MockAuthStrategy(); + const result = await listEnvVars( + { + projectSlug: 'my-project', + environment: 'staging', + }, + auth, + ); + + expect(result.variables).to.have.length(1); + expect(result.variables[0].name).to.equal('API_KEY'); + }); + + it('handles empty results', async () => { + server.use( + http.get(`${DEFAULT_BASE_URL}/api/projects/:projectSlug/target/:targetSlug/env-var/`, () => { + return HttpResponse.json({ + count: 0, + next: null, + previous: null, + results: [], + }); + }), + ); + + const auth = new MockAuthStrategy(); + const result = await listEnvVars( + { + projectSlug: 'my-project', + environment: 'staging', + }, + auth, + ); + + expect(result.count).to.equal(0); + expect(result.variables).to.have.length(0); + }); + + it('uses custom origin when provided', async () => { + const customOrigin = 'https://custom.mobify.com'; + let requestedUrl = ''; + + server.use( + http.get(`${customOrigin}/api/projects/:projectSlug/target/:targetSlug/env-var/`, ({request}) => { + requestedUrl = request.url; + return HttpResponse.json({results: []}); + }), + ); + + const auth = new MockAuthStrategy(); + await listEnvVars( + { + projectSlug: 'my-project', + environment: 'staging', + origin: customOrigin, + }, + auth, + ); + + expect(requestedUrl).to.include(customOrigin); + }); + + it('throws error on API failure', async () => { + server.use( + http.get(`${DEFAULT_BASE_URL}/api/projects/:projectSlug/target/:targetSlug/env-var/`, () => { + return HttpResponse.json({detail: 'Project not found'}, {status: 404}); + }), + ); + + const auth = new MockAuthStrategy(); + + try { + await listEnvVars( + { + projectSlug: 'nonexistent', + environment: 'staging', + }, + auth, + ); + expect.fail('Should have thrown'); + } catch (error) { + expect((error as Error).message).to.include('Failed to list environment variables'); + } + }); + }); + + describe('setEnvVar', () => { + it('sets a single environment variable', async () => { + let receivedBody: unknown; + + server.use( + http.patch(`${DEFAULT_BASE_URL}/api/projects/:projectSlug/target/:targetSlug/env-var/`, async ({request}) => { + receivedBody = await request.json(); + return HttpResponse.json({}, {status: 200}); + }), + ); + + const auth = new MockAuthStrategy(); + await setEnvVar( + { + projectSlug: 'my-project', + environment: 'staging', + key: 'NEW_VAR', + value: 'new-value', + }, + auth, + ); + + expect(receivedBody).to.deep.equal({ + NEW_VAR: {value: 'new-value'}, + }); + }); + + it('throws error on API failure', async () => { + server.use( + http.patch(`${DEFAULT_BASE_URL}/api/projects/:projectSlug/target/:targetSlug/env-var/`, () => { + return HttpResponse.json({detail: 'Unauthorized'}, {status: 401}); + }), + ); + + const auth = new MockAuthStrategy(); + + try { + await setEnvVar( + { + projectSlug: 'my-project', + environment: 'staging', + key: 'VAR', + value: 'value', + }, + auth, + ); + expect.fail('Should have thrown'); + } catch (error) { + expect((error as Error).message).to.include('Failed to set environment variable'); + } + }); + }); + + describe('setEnvVars', () => { + it('sets multiple environment variables', async () => { + let receivedBody: unknown; + + server.use( + http.patch(`${DEFAULT_BASE_URL}/api/projects/:projectSlug/target/:targetSlug/env-var/`, async ({request}) => { + receivedBody = await request.json(); + return HttpResponse.json({}, {status: 200}); + }), + ); + + const auth = new MockAuthStrategy(); + await setEnvVars( + { + projectSlug: 'my-project', + environment: 'staging', + variables: { + VAR1: 'value1', + VAR2: 'value2', + VAR3: 'value3', + }, + }, + auth, + ); + + expect(receivedBody).to.deep.equal({ + VAR1: {value: 'value1'}, + VAR2: {value: 'value2'}, + VAR3: {value: 'value3'}, + }); + }); + + it('throws error on API failure', async () => { + server.use( + http.patch(`${DEFAULT_BASE_URL}/api/projects/:projectSlug/target/:targetSlug/env-var/`, () => { + return HttpResponse.json({detail: 'Invalid request'}, {status: 400}); + }), + ); + + const auth = new MockAuthStrategy(); + + try { + await setEnvVars( + { + projectSlug: 'my-project', + environment: 'staging', + variables: {VAR: 'value'}, + }, + auth, + ); + expect.fail('Should have thrown'); + } catch (error) { + expect((error as Error).message).to.include('Failed to set environment variables'); + } + }); + }); + + describe('deleteEnvVar', () => { + it('deletes an environment variable by setting value to null', async () => { + let receivedBody: unknown; + + server.use( + http.patch(`${DEFAULT_BASE_URL}/api/projects/:projectSlug/target/:targetSlug/env-var/`, async ({request}) => { + receivedBody = await request.json(); + return HttpResponse.json({}, {status: 200}); + }), + ); + + const auth = new MockAuthStrategy(); + await deleteEnvVar( + { + projectSlug: 'my-project', + environment: 'staging', + key: 'OLD_VAR', + }, + auth, + ); + + expect(receivedBody).to.deep.equal({ + OLD_VAR: {value: null}, + }); + }); + + it('throws error on API failure', async () => { + server.use( + http.patch(`${DEFAULT_BASE_URL}/api/projects/:projectSlug/target/:targetSlug/env-var/`, () => { + return HttpResponse.json({detail: 'Variable not found'}, {status: 404}); + }), + ); + + const auth = new MockAuthStrategy(); + + try { + await deleteEnvVar( + { + projectSlug: 'my-project', + environment: 'staging', + key: 'NONEXISTENT', + }, + auth, + ); + expect.fail('Should have thrown'); + } catch (error) { + expect((error as Error).message).to.include('Failed to delete environment variable'); + } + }); + }); +}); diff --git a/packages/b2c-tooling-sdk/test/operations/mrt/push.test.ts b/packages/b2c-tooling-sdk/test/operations/mrt/push.test.ts new file mode 100644 index 00000000..c5b47173 --- /dev/null +++ b/packages/b2c-tooling-sdk/test/operations/mrt/push.test.ts @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2025, Salesforce, Inc. + * SPDX-License-Identifier: Apache-2 + * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0 + */ +import {expect} from 'chai'; +import {http, HttpResponse} from 'msw'; +import {setupServer} from 'msw/node'; +import {createMrtClient, DEFAULT_MRT_ORIGIN} from '@salesforce/b2c-tooling-sdk/clients'; +import {uploadBundle, listBundles} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import type {Bundle} from '@salesforce/b2c-tooling-sdk/operations/mrt'; +import {MockAuthStrategy} from '../../helpers/mock-auth.js'; + +const DEFAULT_BASE_URL = DEFAULT_MRT_ORIGIN; + +describe('operations/mrt/push', () => { + const server = setupServer(); + + before(() => { + server.listen({onUnhandledRequest: 'error'}); + }); + + afterEach(() => { + server.resetHandlers(); + }); + + after(() => { + server.close(); + }); + + describe('uploadBundle', () => { + const testBundle: Bundle = { + message: 'Test bundle', + encoding: 'base64', + data: 'dGVzdC1kYXRh', // base64 encoded "test-data" + ssr_parameters: {SSRFunctionNodeVersion: '22.x'}, + ssr_only: ['ssr.js'], + ssr_shared: ['static/index.html', 'static/app.js'], + }; + + it('uploads bundle without deployment', async () => { + let receivedBody: unknown; + + server.use( + http.post(`${DEFAULT_BASE_URL}/api/projects/:projectSlug/builds/`, async ({request, params}) => { + receivedBody = await request.json(); + expect(params.projectSlug).to.equal('my-project'); + return HttpResponse.json({ + bundle_id: 123, + message: 'Bundle created', + url: 'https://runtime.commercecloud.com/...', + bundle_preview_url: null, + warnings: [], + }); + }), + ); + + const auth = new MockAuthStrategy(); + const client = createMrtClient({}, auth); + + const result = await uploadBundle(client, 'my-project', testBundle); + + expect(result.bundleId).to.equal(123); + expect(result.projectSlug).to.equal('my-project'); + expect(result.deployed).to.be.false; + expect(result.target).to.be.undefined; + expect(result.message).to.equal('Test bundle'); + + expect(receivedBody).to.deep.include({ + message: 'Test bundle', + encoding: 'base64', + data: 'dGVzdC1kYXRh', + }); + }); + + it('uploads bundle with deployment to target', async () => { + let receivedBody: unknown; + let targetSlug: string | undefined; + + server.use( + http.post(`${DEFAULT_BASE_URL}/api/projects/:projectSlug/builds/:targetSlug/`, async ({request, params}) => { + receivedBody = await request.json(); + targetSlug = params.targetSlug as string; + return HttpResponse.json({ + bundle_id: 456, + message: 'Bundle created and deployed', + url: 'https://runtime.commercecloud.com/...', + bundle_preview_url: 'https://preview.staging.mobify.com/...', + warnings: [], + }); + }), + ); + + const auth = new MockAuthStrategy(); + const client = createMrtClient({}, auth); + + const result = await uploadBundle(client, 'my-project', testBundle, 'staging'); + + expect(result.bundleId).to.equal(456); + expect(result.projectSlug).to.equal('my-project'); + expect(result.deployed).to.be.true; + expect(result.target).to.equal('staging'); + expect(targetSlug).to.equal('staging'); + + expect(receivedBody).to.deep.include({ + message: 'Test bundle', + ssr_only: ['ssr.js'], + ssr_shared: ['static/index.html', 'static/app.js'], + }); + }); + + it('throws error on upload failure without target', async () => { + server.use( + http.post(`${DEFAULT_BASE_URL}/api/projects/:projectSlug/builds/`, () => { + return HttpResponse.json({detail: 'Project not found'}, {status: 404}); + }), + ); + + const auth = new MockAuthStrategy(); + const client = createMrtClient({}, auth); + + try { + await uploadBundle(client, 'nonexistent', testBundle); + expect.fail('Should have thrown'); + } catch (error) { + expect((error as Error).message).to.include('Failed to push bundle'); + } + }); + + it('throws error on upload failure with target', async () => { + server.use( + http.post(`${DEFAULT_BASE_URL}/api/projects/:projectSlug/builds/:targetSlug/`, () => { + return HttpResponse.json({detail: 'Target not found'}, {status: 404}); + }), + ); + + const auth = new MockAuthStrategy(); + const client = createMrtClient({}, auth); + + try { + await uploadBundle(client, 'my-project', testBundle, 'invalid-target'); + expect.fail('Should have thrown'); + } catch (error) { + expect((error as Error).message).to.include('Failed to push bundle'); + } + }); + }); + + describe('listBundles', () => { + it('lists bundles for a project', async () => { + server.use( + http.get(`${DEFAULT_BASE_URL}/api/projects/:projectSlug/bundles/`, ({params}) => { + expect(params.projectSlug).to.equal('my-project'); + return HttpResponse.json({ + count: 2, + next: null, + previous: null, + results: [ + { + id: 123, + message: 'Bundle 1', + created_at: '2025-01-01T00:00:00Z', + created_by: 'user@example.com', + }, + { + id: 124, + message: 'Bundle 2', + created_at: '2025-01-02T00:00:00Z', + created_by: 'user@example.com', + }, + ], + }); + }), + ); + + const auth = new MockAuthStrategy(); + const client = createMrtClient({}, auth); + + const bundles = await listBundles(client, 'my-project'); + + expect(bundles).to.have.length(2); + expect(bundles[0]).to.deep.include({id: 123, message: 'Bundle 1'}); + expect(bundles[1]).to.deep.include({id: 124, message: 'Bundle 2'}); + }); + + it('passes pagination options', async () => { + let queryParams: URLSearchParams | undefined; + + server.use( + http.get(`${DEFAULT_BASE_URL}/api/projects/:projectSlug/bundles/`, ({request}) => { + queryParams = new URL(request.url).searchParams; + return HttpResponse.json({results: []}); + }), + ); + + const auth = new MockAuthStrategy(); + const client = createMrtClient({}, auth); + + await listBundles(client, 'my-project', {limit: 10, offset: 20}); + + expect(queryParams?.get('limit')).to.equal('10'); + expect(queryParams?.get('offset')).to.equal('20'); + }); + + it('returns empty array when no bundles', async () => { + server.use( + http.get(`${DEFAULT_BASE_URL}/api/projects/:projectSlug/bundles/`, () => { + return HttpResponse.json({ + count: 0, + results: [], + }); + }), + ); + + const auth = new MockAuthStrategy(); + const client = createMrtClient({}, auth); + + const bundles = await listBundles(client, 'my-project'); + + expect(bundles).to.have.length(0); + }); + + it('throws error on API failure', async () => { + server.use( + http.get(`${DEFAULT_BASE_URL}/api/projects/:projectSlug/bundles/`, () => { + return HttpResponse.json({detail: 'Unauthorized'}, {status: 401}); + }), + ); + + const auth = new MockAuthStrategy(); + const client = createMrtClient({}, auth); + + try { + await listBundles(client, 'my-project'); + expect.fail('Should have thrown'); + } catch (error) { + expect((error as Error).message).to.include('Failed to list bundles'); + } + }); + }); +}); diff --git a/packages/b2c-tooling-sdk/test/tsconfig.json b/packages/b2c-tooling-sdk/test/tsconfig.json new file mode 100644 index 00000000..783bb46c --- /dev/null +++ b/packages/b2c-tooling-sdk/test/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "noEmit": true, + "types": ["node", "mocha", "chai"], + "paths": { + "@salesforce/b2c-tooling-sdk": ["../src/index.ts"], + "@salesforce/b2c-tooling-sdk/*": ["../src/*/index.ts", "../src/*"] + } + }, + "include": ["./**/*", "../src/**/*"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 52fa36dd..0b840f3e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,6 +72,9 @@ importers: '@types/node': specifier: ^18 version: 18.19.130 + c8: + specifier: ^10.1.3 + version: 10.1.3 chai: specifier: ^4 version: 4.5.0 @@ -163,12 +166,24 @@ importers: '@types/archiver': specifier: ^7.0.0 version: 7.0.0 + '@types/chai': + specifier: ^4.3.20 + version: 4.3.20 + '@types/mocha': + specifier: ^10.0.10 + version: 10.0.10 '@types/node': specifier: ^18.19.130 version: 18.19.130 '@types/xml2js': specifier: ^0.4.14 version: 0.4.14 + c8: + specifier: ^10.1.3 + version: 10.1.3 + chai: + specifier: ^4.5.0 + version: 4.5.0 eslint: specifier: ^9 version: 9.39.1 @@ -178,6 +193,12 @@ importers: eslint-plugin-prettier: specifier: ^5.5.4 version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.1))(eslint@9.39.1)(prettier@3.6.2) + mocha: + specifier: ^10.8.2 + version: 10.8.2 + msw: + specifier: ^2.12.4 + version: 2.12.4(@types/node@18.19.130)(typescript@5.9.3) openapi-typescript: specifier: ^7.10.1 version: 7.10.1(typescript@5.9.3) @@ -187,6 +208,9 @@ importers: shx: specifier: ^0.3.3 version: 0.3.4 + tsx: + specifier: ^4.20.6 + version: 4.20.6 typescript: specifier: ^5 version: 5.9.3 @@ -456,6 +480,10 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + '@docsearch/css@3.8.2': resolution: {integrity: sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==} @@ -1056,9 +1084,24 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@mswjs/interceptors@0.40.0': + resolution: {integrity: sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==} + engines: {node: '>=18'} + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -1115,6 +1158,15 @@ packages: peerDependencies: '@oclif/core': '>= 3.0.0' + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} @@ -1570,6 +1622,9 @@ packages: '@types/http-cache-semantics@4.0.4': resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1606,6 +1661,9 @@ packages: '@types/readdir-glob@1.1.5': resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==} + '@types/statuses@2.0.6': + resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -2067,6 +2125,16 @@ packages: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} + c8@10.1.3: + resolution: {integrity: sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + monocart-coverage-reports: ^2 + peerDependenciesMeta: + monocart-coverage-reports: + optional: true + cacheable-lookup@7.0.0: resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} engines: {node: '>=14.16'} @@ -2168,6 +2236,10 @@ packages: cliui@7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + cliui@9.0.1: resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} engines: {node: '>=20'} @@ -2212,6 +2284,13 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + copy-anything@4.0.5: resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} engines: {node: '>=18'} @@ -2887,6 +2966,10 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + graphql@16.12.0: + resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -2927,6 +3010,9 @@ packages: header-case@2.0.4: resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==} + headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + help-me@5.0.0: resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} @@ -2940,6 +3026,9 @@ packages: resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} engines: {node: ^16.14.0 || >=18.0.0} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} @@ -3111,6 +3200,9 @@ packages: resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} engines: {node: '>= 0.4'} + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + is-number-object@1.1.1: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} @@ -3200,6 +3292,18 @@ packages: resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} engines: {node: '>=16'} + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} @@ -3329,6 +3433,10 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + mark.js@8.11.1: resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} @@ -3420,6 +3528,16 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msw@2.12.4: + resolution: {integrity: sha512-rHNiVfTyKhzc0EjoXUBVGteNKBevdjOlVC6GlIRXpy+/3LHEIGRovnB5WPjcvmNODVQ1TNFnoa7wsGbd0V3epg==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + mute-stream@1.0.0: resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -3611,6 +3729,9 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + own-keys@1.0.1: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} @@ -3703,6 +3824,9 @@ packages: resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} engines: {node: 20 || >=22} + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + pathval@1.1.1: resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} @@ -3911,6 +4035,9 @@ packages: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} + rettime@0.7.0: + resolution: {integrity: sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -4085,6 +4212,10 @@ packages: stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -4092,6 +4223,9 @@ packages: streamx@2.23.0: resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==} + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -4179,6 +4313,10 @@ packages: tabbable@6.3.0: resolution: {integrity: sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==} + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + tapable@2.3.0: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} @@ -4186,6 +4324,10 @@ packages: tar-stream@3.1.7: resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + test-exclude@7.0.1: + resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} + engines: {node: '>=18'} + text-decoder@1.2.3: resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} @@ -4199,10 +4341,21 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tldts-core@7.0.19: + resolution: {integrity: sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==} + + tldts@7.0.19: + resolution: {integrity: sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==} + hasBin: true + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + engines: {node: '>=16'} + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -4259,6 +4412,10 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} + type-fest@5.3.1: + resolution: {integrity: sha512-VCn+LMHbd4t6sF3wfU/+HKT63C9OoyrSIf4b+vtWHpt2U7/4InZG467YDNMFMR70DdHjAdpPWmw2lzRdg0Xqqg==} + engines: {node: '>=20'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -4340,6 +4497,9 @@ packages: unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + until-async@3.0.2: + resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} + update-browserslist-db@1.1.4: resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} hasBin: true @@ -4358,6 +4518,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} @@ -4521,6 +4685,10 @@ packages: resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} engines: {node: '>=10'} + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + yarn@1.22.22: resolution: {integrity: sha512-prL3kGtyG7o9Z9Sv8IPfBNrWTDmXB4Qbes8A9rEzt6wkJV8mUvoirjU0Mp3GGAU06Y0XQyA3/2/RQFVuK7MTfg==} engines: {node: '>=4.0.0'} @@ -5190,6 +5358,8 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@bcoe/v8-coverage@1.0.2': {} + '@docsearch/css@3.8.2': {} '@docsearch/js@3.8.2(@algolia/client-search@5.44.0)(search-insights@2.17.3)': @@ -5676,8 +5846,26 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@istanbuljs/schema@0.1.3': {} + + '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@mswjs/interceptors@0.40.0': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.7.0 @@ -5784,6 +5972,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + '@pinojs/redact@0.4.0': {} '@pkgjs/parseargs@0.11.0': @@ -6343,6 +6540,8 @@ snapshots: '@types/http-cache-semantics@4.0.4': {} + '@types/istanbul-lib-coverage@2.0.6': {} + '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} @@ -6380,6 +6579,8 @@ snapshots: dependencies: '@types/node': 18.19.130 + '@types/statuses@2.0.6': {} + '@types/unist@3.0.3': {} '@types/web-bluetooth@0.0.21': {} @@ -6857,6 +7058,20 @@ snapshots: dependencies: run-applescript: 7.1.0 + c8@10.1.3: + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@istanbuljs/schema': 0.1.3 + find-up: 5.0.0 + foreground-child: 3.3.1 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + test-exclude: 7.0.1 + v8-to-istanbul: 9.3.0 + yargs: 17.7.2 + yargs-parser: 21.1.1 + cacheable-lookup@7.0.0: {} cacheable-request@10.2.14: @@ -6985,6 +7200,12 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + cliui@9.0.1: dependencies: string-width: 7.2.0 @@ -7030,6 +7251,10 @@ snapshots: content-type@1.0.5: {} + convert-source-map@2.0.0: {} + + cookie@1.1.1: {} + copy-anything@4.0.5: dependencies: is-what: 5.5.0 @@ -7906,6 +8131,8 @@ snapshots: graphemer@1.4.0: {} + graphql@16.12.0: {} + has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -7953,6 +8180,8 @@ snapshots: capital-case: 1.0.4 tslib: 2.8.1 + headers-polyfill@4.0.3: {} + help-me@5.0.0: {} hookable@5.5.3: {} @@ -7963,6 +8192,8 @@ snapshots: dependencies: lru-cache: 10.4.3 + html-escaper@2.0.2: {} + html-void-elements@3.0.0: {} http-cache-semantics@4.2.0: {} @@ -8124,6 +8355,8 @@ snapshots: is-negative-zero@2.0.3: {} + is-node-process@1.2.0: {} + is-number-object@1.1.1: dependencies: call-bound: 1.0.4 @@ -8198,6 +8431,19 @@ snapshots: isexe@3.1.1: {} + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 @@ -8315,6 +8561,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + make-dir@4.0.0: + dependencies: + semver: 7.7.3 + mark.js@8.11.1: {} markdown-it@14.1.0: @@ -8423,6 +8673,31 @@ snapshots: ms@2.1.3: {} + msw@2.12.4(@types/node@18.19.130)(typescript@5.9.3): + dependencies: + '@inquirer/confirm': 5.1.20(@types/node@18.19.130) + '@mswjs/interceptors': 0.40.0 + '@open-draft/deferred-promise': 2.2.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.12.0 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.7.0 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.0 + type-fest: 5.3.1 + until-async: 3.0.2 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/node' + mute-stream@1.0.0: {} mute-stream@3.0.0: {} @@ -8584,6 +8859,8 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + outvariant@1.4.3: {} + own-keys@1.0.1: dependencies: get-intrinsic: 1.3.0 @@ -8677,6 +8954,8 @@ snapshots: lru-cache: 11.2.2 minipass: 7.1.2 + path-to-regexp@6.3.0: {} + pathval@1.1.1: {} perfect-debounce@1.0.0: {} @@ -8889,6 +9168,8 @@ snapshots: retry@0.13.1: {} + rettime@0.7.0: {} + reusify@1.1.0: {} rfdc@1.4.1: {} @@ -9109,6 +9390,8 @@ snapshots: stable-hash@0.0.5: {} + statuses@2.0.2: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -9123,6 +9406,8 @@ snapshots: - bare-abort-controller - react-native-b4a + strict-event-emitter@0.5.1: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -9219,6 +9504,8 @@ snapshots: tabbable@6.3.0: {} + tagged-tag@1.0.0: {} + tapable@2.3.0: {} tar-stream@3.1.7: @@ -9230,6 +9517,12 @@ snapshots: - bare-abort-controller - react-native-b4a + test-exclude@7.0.1: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 10.5.0 + minimatch: 9.0.5 + text-decoder@1.2.3: dependencies: b4a: 1.7.3 @@ -9247,10 +9540,20 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tldts-core@7.0.19: {} + + tldts@7.0.19: + dependencies: + tldts-core: 7.0.19 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 + tough-cookie@6.0.0: + dependencies: + tldts: 7.0.19 + trim-lines@3.0.1: {} ts-api-utils@2.1.0(typescript@5.9.3): @@ -9298,6 +9601,10 @@ snapshots: type-fest@4.41.0: {} + type-fest@5.3.1: + dependencies: + tagged-tag: 1.0.0 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -9423,6 +9730,8 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + until-async@3.0.2: {} + update-browserslist-db@1.1.4(browserslist@4.28.0): dependencies: browserslist: 4.28.0 @@ -9443,6 +9752,12 @@ snapshots: util-deprecate@1.0.2: {} + v8-to-istanbul@9.3.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + validate-npm-package-license@3.0.4: dependencies: spdx-correct: 3.2.0 @@ -9652,6 +9967,16 @@ snapshots: y18n: 5.0.8 yargs-parser: 20.2.9 + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yarn@1.22.22: {} yocto-queue@0.1.0: {}