Skip to content

Commit

Permalink
refactor: improve http client backend flow (#23)
Browse files Browse the repository at this point in the history
  • Loading branch information
barroro authored Feb 16, 2025
2 parents d4a9b51 + 0f44648 commit d632207
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 170 deletions.
55 changes: 20 additions & 35 deletions src/lib/clients/backend.test.ts → src/lib/clients/artists.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { beforeEach, describe, it, expect, vi } from 'vitest';
import { Artist } from '@/lib/artists';
import { HTTPBackendClient } from './backend';
import { ArtistsHTTPClient } from './artists';
import { FakeHttpClient, HttpResponse, Method } from './http';
import { FakeAuthClient } from './auth';
import { AuthHeaderBuilderStub, AuthHeaderBuilder } from './auth';

describe('HTTPBackendClient', () => {
describe('ArtistsHTTPClient', () => {
let url: string;
let token: string;
let name: string;
let limit: number;
let httpClient: FakeHttpClient;
let response: HttpResponse;
let authHeaderBuilder: AuthHeaderBuilder;

beforeEach(() => {
url = 'http://some_url';
Expand All @@ -25,27 +26,16 @@ describe('HTTPBackendClient', () => {
status: 200,
};
httpClient = new FakeHttpClient(response);
authHeaderBuilder = new AuthHeaderBuilderStub();
});

it('should call the client with the correct parameters', async () => {
const client = new HTTPBackendClient(url, httpClient);
vi.spyOn(httpClient, 'send');

await client.searchArtists(token, name, limit);

expect(httpClient.send).toHaveBeenCalledWith({
url: `${url}/artists/search`,
method: Method.Get,
params: { name: name, limit: limit },
headers: { Authorization: `Bearer ${token}` },
});
});

it('should call the client with an additional auth header if auth client is provided', async () => {
const authHeader = 'Some-Header';
const authToken = 'some-token';
const authClient = new FakeAuthClient(authToken, authHeader);
const client = new HTTPBackendClient(url, httpClient, authClient);
const headers = { something: 'value' };
const client = new ArtistsHTTPClient(
url,
httpClient,
new AuthHeaderBuilderStub(headers)
);
vi.spyOn(httpClient, 'send');

await client.searchArtists(token, name, limit);
Expand All @@ -54,15 +44,12 @@ describe('HTTPBackendClient', () => {
url: `${url}/artists/search`,
method: Method.Get,
params: { name: name, limit: limit },
headers: {
Authorization: `Bearer ${token}`,
[authHeader]: `Bearer ${authToken}`,
},
headers: headers,
});
});

it('should return the list of artists returned by the HTTP client', async () => {
const client = new HTTPBackendClient(url, httpClient);
const client = new ArtistsHTTPClient(url, httpClient, authHeaderBuilder);

const actual = await client.searchArtists(token, name, limit);

Expand All @@ -76,21 +63,19 @@ describe('HTTPBackendClient', () => {
it('should throw an error if the HTTP client fails', async () => {
const errorMessage = 'Request failed';
httpClient.setSendErrorMessage(errorMessage);
const client = new HTTPBackendClient(url, httpClient);
const client = new ArtistsHTTPClient(url, httpClient, authHeaderBuilder);

await expect(client.searchArtists(token, name, limit)).rejects.toThrow(
errorMessage
);
});

it('should throw an error if the auth client fails', async () => {
const errorMessage = 'Auth request failed';
const authClient = new FakeAuthClient('some-token', 'Some-Header');
authClient.setGetTokenErrorMessage(errorMessage);
const client = new HTTPBackendClient(url, httpClient, authClient);
it('should throw an error if the header builder fails', async () => {
vi.spyOn(authHeaderBuilder, 'buildHeader').mockImplementation(() => {
throw new Error('test Error');
});
const client = new ArtistsHTTPClient(url, httpClient, authHeaderBuilder);

await expect(client.searchArtists(token, name, limit)).rejects.toThrow(
errorMessage
);
await expect(client.searchArtists(token, name, limit)).rejects.toThrow();
});
});
59 changes: 59 additions & 0 deletions src/lib/clients/artists.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Artist } from '../artists';
import { AuthHeaderBuilder, BaseAuthHeaderBuilder } from './auth';
import { HttpClient, Method } from './http';

export interface ArtistsClient {
searchArtists(
_token: string,
_name: string,
_limit: number
): Promise<Artist[]>;
}

export class ArtistsHTTPClient implements ArtistsClient {
private url: string;
private httpClient: HttpClient;
private httpAuthHeaderBuilder: AuthHeaderBuilder;

constructor(
url: string,
httpClient: HttpClient,
httpAuthHeaderBuilder: BaseAuthHeaderBuilder
) {
this.url = url;
this.httpClient = httpClient;
this.httpAuthHeaderBuilder = httpAuthHeaderBuilder;
}

async searchArtists(
token: string,
name: string,
limit: number
): Promise<Artist[]> {
const authHeader = await this.httpAuthHeaderBuilder.buildHeader(token);
return this.httpClient
.send({
url: `${this.url}/artists/search`,
method: Method.Get,
params: { name, limit },
headers: authHeader,
})
.then((response) =>
response.data.map(
(artist: any) => new Artist(artist.name, artist.imageUri)
)
);
}
}

export class ArtistsClientStub implements ArtistsClient {
private searchArtistResult: Artist[];

constructor(result: Artist[] = []) {
this.searchArtistResult = result;
}

async searchArtists(..._: any[]): Promise<Artist[]> {
return this.searchArtistResult;
}
}
65 changes: 60 additions & 5 deletions src/lib/clients/auth.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { beforeEach, describe, it, expect, vi } from 'vitest';
import { GCPHTTPAuthClient } from './auth';
import { AuthClientStub, GCPAuthClient, BaseAuthHeaderBuilder } from './auth';
import { FakeHttpClient, HttpResponse, Method } from './http';

describe('HTTPAuthClient', () => {
describe('GCPAuthClient', () => {
const audience = 'test-audience';
let httpClient: FakeHttpClient;
let response: HttpResponse;
Expand All @@ -13,7 +13,7 @@ describe('HTTPAuthClient', () => {
});

it('should call the client with the correct parameters', async () => {
const authClient = new GCPHTTPAuthClient(httpClient, audience);
const authClient = new GCPAuthClient(httpClient, audience);
const baseUrl = 'http://some_url/auth';
authClient.setBaseUrl(baseUrl);
vi.spyOn(httpClient, 'send');
Expand All @@ -31,7 +31,7 @@ describe('HTTPAuthClient', () => {
it('should return token from the client', async () => {
const expected = 'some-token';
httpClient.setResult({ data: expected, status: 200 });
const authClient = new GCPHTTPAuthClient(httpClient, audience);
const authClient = new GCPAuthClient(httpClient, audience);

const actual = await authClient.getToken();

Expand All @@ -41,8 +41,63 @@ describe('HTTPAuthClient', () => {
it('should throw an error if the client fails', async () => {
const errorMessage = 'Test error';
httpClient.setSendErrorMessage('Test error');
const authClient = new GCPHTTPAuthClient(httpClient, audience);
const authClient = new GCPAuthClient(httpClient, audience);

await expect(authClient.getToken()).rejects.toThrow(errorMessage);
});
});

describe('BaseAuthHeaderBuilder', () => {
function createAuthClient() {
const authHeader = 'X-Serverless-Authorization';
const authToken = 'test-token';
return new AuthClientStub(authToken, authHeader);
}

it('should call getToken and getHeaderName on authClient', async () => {
const authClient = createAuthClient();
vi.spyOn(authClient, 'getToken');
vi.spyOn(authClient, 'getHeaderName');
const client = new BaseAuthHeaderBuilder(authClient);

const token = 'app-token';
await client.buildHeader(token);

expect(authClient.getToken).toHaveBeenCalled();
expect(authClient.getHeaderName).toHaveBeenCalled();
});

it('should return the correct auth header if token is provided', async () => {
const client = new BaseAuthHeaderBuilder();

const token = 'app-token';
const headers = await client.buildHeader(token);

expect(headers).toEqual({ Authorization: 'Bearer app-token' });
});

it('should return the correct auth header if authClient and token are provided', async () => {
const authClient = createAuthClient();
const client = new BaseAuthHeaderBuilder(authClient);

const token = 'app-token';
const headers = await client.buildHeader(token);

expect(headers).toEqual({
Authorization: 'Bearer app-token',
'X-Serverless-Authorization': 'Bearer test-token',
});
});

it('should throw an error if the auth client fails', async () => {
const errorMessage = 'Auth request failed';
const authClient = createAuthClient();
vi.spyOn(authClient, 'getToken').mockImplementation(() => {
throw new Error(errorMessage);
});
const client = new BaseAuthHeaderBuilder(authClient);

const token = 'app-token';
await expect(client.buildHeader(token)).rejects.toThrow(errorMessage);
});
});
49 changes: 39 additions & 10 deletions src/lib/clients/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export interface AuthClient {
getHeaderName(): string;
}

export class GCPHTTPAuthClient {
export class GCPAuthClient implements AuthClient {
private httpClient: HttpClient;
private baseUrl: string =
'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity';
Expand Down Expand Up @@ -36,10 +36,9 @@ export class GCPHTTPAuthClient {
}
}

export class FakeAuthClient {
export class AuthClientStub implements AuthClient {
private header: string;
private token: string;
private getTokenErrorMessage: string | undefined = undefined;

constructor(token: string, header: string) {
this.token = token;
Expand All @@ -50,18 +49,48 @@ export class FakeAuthClient {
this.token = token;
}

setGetTokenErrorMessage(message: string) {
this.getTokenErrorMessage = message;
}

async getToken(): Promise<string> {
if (this.getTokenErrorMessage !== undefined) {
throw new Error(this.getTokenErrorMessage);
}
return this.token;
}

getHeaderName(): string {
return this.header;
}
}

export interface AuthHeaderBuilder {
buildHeader: (_token: string) => Promise<Record<string, string>>;
}

export class BaseAuthHeaderBuilder implements AuthHeaderBuilder {
private gcpAuthClient?: AuthClient | undefined;

constructor(gcpAuthClient?: AuthClient) {
this.gcpAuthClient = gcpAuthClient;
}

async buildHeader(token: string): Promise<Record<string, string>> {
const headers: Record<string, string> = {
Authorization: `Bearer ${token}`,
};

if (!this.gcpAuthClient) return headers;

return {
...headers,
[this.gcpAuthClient.getHeaderName()]: `Bearer ${await this.gcpAuthClient.getToken()}`,
};
}
}

export class AuthHeaderBuilderStub implements AuthHeaderBuilder {
private headers: Record<string, string>;

constructor(headers: Record<string, string> = {}) {
this.headers = headers;
}

async buildHeader(_token: string): Promise<Record<string, string>> {
return this.headers;
}
}
Loading

0 comments on commit d632207

Please sign in to comment.