Skip to content

Commit

Permalink
feat: provider.url now returns auth url (#3147)
Browse files Browse the repository at this point in the history
* feat: auth URL now returned from provider

* chore: changeset

* chore: updated changeset

* lint

* chore: added `INVALID_URL` throwing

* Update .changeset/orange-hornets-help.md
  • Loading branch information
petertonysmith94 authored Sep 11, 2024
1 parent c968ac0 commit aef7282
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 50 deletions.
6 changes: 6 additions & 0 deletions .changeset/orange-hornets-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@fuel-ts/account": patch
"@fuel-ts/errors": patch
---

feat: `provider.url` now returns auth url
6 changes: 6 additions & 0 deletions apps/docs/src/guide/errors/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,12 @@ When the word list length is not equal to 2048.

The word list provided to the mnemonic length should be equal to 2048.

### `INVALID_URL`

When the URL provided is invalid.

Ensure that the URL is valid.

### `JSON_ABI_ERROR`

When an ABI type does not conform to the correct format.
Expand Down
148 changes: 130 additions & 18 deletions packages/account/src/providers/provider.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Address } from '@fuel-ts/address';
import { ZeroBytes32 } from '@fuel-ts/address/configs';
import { randomBytes } from '@fuel-ts/crypto';
import { randomBytes, randomUUID } from '@fuel-ts/crypto';
import { FuelError, ErrorCode } from '@fuel-ts/errors';
import { expectToThrowFuelError, safeExec } from '@fuel-ts/errors/test-utils';
import { BN, bn } from '@fuel-ts/math';
Expand Down Expand Up @@ -56,53 +56,165 @@ const getCustomFetch =
return fetch(url, options);
};

const createBasicAuth = (launchNodeUrl: string) => {
const username: string = randomUUID();
const password: string = randomUUID();
const usernameAndPassword = `${username}:${password}`;

const parsedUrl = new URL(launchNodeUrl);
const hostAndPath = `${parsedUrl.host}${parsedUrl.pathname}`;
const urlWithAuth = `http://${usernameAndPassword}@${hostAndPath}`;

return {
urlWithAuth,
urlWithoutAuth: launchNodeUrl,
usernameAndPassword,
expectedHeaders: {
Authorization: `Basic ${btoa(usernameAndPassword)}`,
},
};
};

/**
* @group node
*/
describe('Provider', () => {
it('supports basic auth', async () => {
it('should ensure supports basic auth', async () => {
using launched = await setupTestProviderAndWallets();
const {
provider: { url },
} = launched;

const usernameAndPassword = 'securest:ofpasswords';
const parsedUrl = new URL(url);
const hostAndPath = `${parsedUrl.host}${parsedUrl.pathname}`;
const authUrl = `http://${usernameAndPassword}@${hostAndPath}`;
const provider = await Provider.create(authUrl);
const { urlWithAuth, expectedHeaders } = createBasicAuth(url);
const provider = await Provider.create(urlWithAuth);

const fetchSpy = vi.spyOn(global, 'fetch');

await provider.operations.getChain();

const expectedAuthToken = `Basic ${btoa(usernameAndPassword)}`;
const [requestUrl, request] = fetchSpy.mock.calls[0];
expect(requestUrl).toEqual(url);
expect(request?.headers).toMatchObject({
Authorization: expectedAuthToken,
});
expect(request?.headers).toMatchObject(expectedHeaders);
});

it('custom requestMiddleware is not overwritten by basic auth', async () => {
it('should ensure we can reuse provider URL to connect to a authenticated endpoint', async () => {
using launched = await setupTestProviderAndWallets();
const {
provider: { url },
} = launched;

const usernameAndPassword = 'securest:ofpasswords';
const parsedUrl = new URL(url);
const hostAndPath = `${parsedUrl.host}${parsedUrl.pathname}`;
const authUrl = `http://${usernameAndPassword}@${hostAndPath}`;
const { urlWithAuth, expectedHeaders } = createBasicAuth(url);
const provider = await Provider.create(urlWithAuth);

const fetchSpy = vi.spyOn(global, 'fetch');

await provider.operations.getChain();

const [requestUrlA, requestA] = fetchSpy.mock.calls[0];
expect(requestUrlA).toEqual(url);
expect(requestA?.headers).toMatchObject(expectedHeaders);

const requestMiddleware = vi.fn();
await Provider.create(authUrl, {
// Reuse the provider URL to connect to an authenticated endpoint
const newProvider = await Provider.create(provider.url);

fetchSpy.mockClear();

await newProvider.operations.getChain();
const [requestUrl, request] = fetchSpy.mock.calls[0];
expect(requestUrl).toEqual(url);
expect(request?.headers).toMatchObject(expectedHeaders);
});

it('should ensure that custom requestMiddleware is not overwritten by basic auth', async () => {
using launched = await setupTestProviderAndWallets();
const {
provider: { url },
} = launched;

const { urlWithAuth } = createBasicAuth(url);

const requestMiddleware = vi.fn().mockImplementation((options) => options);

await Provider.create(urlWithAuth, {
requestMiddleware,
});

expect(requestMiddleware).toHaveBeenCalled();
});

it('should ensure that we can connect to a new entrypoint with basic auth', async () => {
using launchedA = await setupTestProviderAndWallets();
using launchedB = await setupTestProviderAndWallets();
const {
provider: { url: urlA },
} = launchedA;
const {
provider: { url: urlB },
} = launchedB;

// Should enable connection via `create` method
const basicAuthA = createBasicAuth(urlA);
const provider = await Provider.create(basicAuthA.urlWithAuth);

const fetchSpy = vi.spyOn(global, 'fetch');

await provider.operations.getChain();

const [requestUrlA, requestA] = fetchSpy.mock.calls[0];
expect(requestUrlA, 'expect to request with the unauthenticated URL').toEqual(urlA);
expect(requestA?.headers).toMatchObject({
Authorization: basicAuthA.expectedHeaders.Authorization,
});
expect(provider.url).toEqual(basicAuthA.urlWithAuth);

fetchSpy.mockClear();

// Should enable reconnection
const basicAuthB = createBasicAuth(urlB);

await provider.connect(basicAuthB.urlWithAuth);
await provider.operations.getChain();

const [requestUrlB, requestB] = fetchSpy.mock.calls[0];
expect(requestUrlB, 'expect to request with the unauthenticated URL').toEqual(urlB);
expect(requestB?.headers).toMatchObject(
expect.objectContaining({
Authorization: basicAuthB.expectedHeaders.Authorization,
})
);
expect(provider.url).toEqual(basicAuthB.urlWithAuth);
});

it('should ensure that custom headers can be passed', async () => {
using launched = await setupTestProviderAndWallets();
const {
provider: { url },
} = launched;

const customHeaders = {
'X-Custom-Header': 'custom-value',
};

const provider = await Provider.create(url, {
headers: customHeaders,
});

const fetchSpy = vi.spyOn(global, 'fetch');
await provider.operations.getChain();

const [, request] = fetchSpy.mock.calls[0];
expect(request?.headers).toMatchObject(customHeaders);
});

it('should throw an error if the URL is no in the correct format', async () => {
const url = 'immanotavalidurl';

await expectToThrowFuelError(
async () => Provider.create(url),
new FuelError(ErrorCode.INVALID_URL, 'Invalid URL provided.')
);
});

it('should throw an error when retrieving a transaction with an unknown transaction type', async () => {
using launched = await setupTestProviderAndWallets();
const { provider } = launched;
Expand Down
Loading

0 comments on commit aef7282

Please sign in to comment.