Skip to content

Instantly share code, notes, and snippets.

@brandonbryant12
Created November 1, 2024 11:27
Show Gist options
  • Save brandonbryant12/52a951e9b2af2933b1dab925da892bac to your computer and use it in GitHub Desktop.
Save brandonbryant12/52a951e9b2af2933b1dab925da892bac to your computer and use it in GitHub Desktop.
graphql-proxy
import { GraphQLRelayService } from './GraphQLRelayService';
import superagent, { Response } from 'superagent';
import { jest } from '@jest/globals';
jest.mock('superagent');
interface TokenResponse {
body: {
access_token: string;
expires_in: number;
};
}
interface GraphQLResponse {
body: {
data: {
hello: string;
};
};
}
describe('GraphQLRelayService', () => {
const clientId = 'test-client-id';
const clientSecret = 'test-client-secret';
let cache: any;
let service: GraphQLRelayService;
beforeEach(() => {
jest.clearAllMocks();
cache = {
get: jest.fn(),
set: jest.fn(),
delete: jest.fn(),
};
service = new GraphQLRelayService(clientId, clientSecret, cache);
});
it('should retrieve access token from cache', async () => {
cache.get.mockResolvedValue('cached-token');
const token = await (service as any)['retrieveAccessToken']();
expect(token).toBe('cached-token');
expect(cache.get).toHaveBeenCalledWith('access_token');
});
it('should fetch new access token when cache is empty', async () => {
cache.get.mockResolvedValue(undefined);
const mockPostResponse: TokenResponse = {
body: {
access_token: 'new-token',
expires_in: 3600,
},
};
const mockSend = jest.fn<() => Promise<TokenResponse>>().mockResolvedValue(mockPostResponse);
const mockType = jest.fn().mockReturnValue({ send: mockSend });
const mockPost = jest.fn().mockReturnValue({ type: mockType });
(superagent.post as jest.Mock).mockImplementation(mockPost);
const token = await (service as any)['retrieveAccessToken']();
expect(token).toBe('new-token');
expect(superagent.post).toHaveBeenCalledWith('https://oauth.provider/token');
expect(cache.set).toHaveBeenCalledWith('access_token', 'new-token', { ttl: 3540 });
});
it('should forward GraphQL request with access token', async () => {
cache.get.mockResolvedValue('cached-token');
const mockGraphQLResponse: GraphQLResponse = {
body: { data: { hello: 'world' } },
};
const mockSend = jest.fn<() => Promise<GraphQLResponse>>().mockResolvedValue(mockGraphQLResponse);
const mockSet = jest.fn().mockReturnValue({ send: mockSend });
const mockPost = jest.fn().mockReturnValue({ set: mockSet });
(superagent.post as jest.Mock).mockImplementation(mockPost);
const response = await service.forwardGraphqlRequest('{ hello }', {});
expect(response).toEqual({ data: { hello: 'world' } });
expect(superagent.post).toHaveBeenCalledWith('https://graphql.api/endpoint');
});
});
// src/services/GraphQLRelayService.ts
import superagent from 'superagent';
import { CacheClient } from '@backstage/backend-common';
export class GraphQLRelayService {
private readonly clientId: string;
private readonly clientSecret: string;
private readonly cache: CacheClient;
private readonly tokenCacheKey = 'access_token';
constructor(clientId: string, clientSecret: string, cache: CacheClient) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.cache = cache;
}
private async getCachedAccessToken(): Promise<string | undefined> {
const cachedToken = await this.cache.get<string>(this.tokenCacheKey);
return cachedToken;
}
private async fetchAccessToken(): Promise<{ token: string; expiresIn: number }> {
const response = await superagent
.post('https://oauth.provider/token')
.type('form')
.send({
grant_type: 'client_credentials',
client_id: this.clientId,
client_secret: this.clientSecret,
});
const { access_token, expires_in } = response.body;
return { token: access_token, expiresIn: expires_in };
}
private async retrieveAccessToken(): Promise<string> {
let accessToken = await this.getCachedAccessToken();
if (!accessToken) {
const { token, expiresIn } = await this.fetchAccessToken();
accessToken = token;
const cacheExpiry = expiresIn - 60;
await this.cache.set(this.tokenCacheKey, accessToken, { ttl: cacheExpiry });
}
return accessToken;
}
public async forwardGraphqlRequest(query: string, variables: any): Promise<any> {
const accessToken = await this.retrieveAccessToken();
const response = await superagent
.post('https://graphql.api/endpoint')
.set('Authorization', `Bearer ${accessToken}`)
.send({ query, variables });
return response.body;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment