Repository: neet/masto.js
Pull request #225 (2020-03-07 02:28:20)

Decouple axios from the gateway


Patch:
diff --git a/src/clients/masto-admin/masto-admin.ts b/src/clients/masto-admin/masto-admin.ts
index d2e115e8..3e447fac 100644
--- a/src/clients/masto-admin/masto-admin.ts
+++ b/src/clients/masto-admin/masto-admin.ts
@@ -1,5 +1,4 @@
-import { Gateway } from '../../gateway/gateway';
-import { available } from '../../gateway/decorators';
+import { GatewayImpl, available } from '../../gateway';
 import { AdminAccount, AdminReport } from '../../entities';
 import {
   AdminActionAccountParams,
@@ -10,7 +9,7 @@ import {
 /**
  * Mastodon Moderation API client
  */
-export class MastoAdmin extends Gateway {
+export class MastoAdmin extends GatewayImpl {
   /**
    * Get accounts
    * @param params parameters
diff --git a/src/clients/masto/masto.ts b/src/clients/masto/masto.ts
index 2c8b4baa..19763675 100644
--- a/src/clients/masto/masto.ts
+++ b/src/clients/masto/masto.ts
@@ -1,5 +1,4 @@
-import { Gateway } from '../../gateway/gateway';
-import { available } from '../../gateway/decorators';
+import { GatewayImpl, available } from '../../gateway';
 import {
   Account,
   AccountCredentials,
@@ -66,7 +65,7 @@ import {
 /**
  * Mastodon API client
  */
-export class Masto extends Gateway {
+export class Masto extends GatewayImpl {
   /**
    * Starting home timeline and notification streaming
    * @return Instance of EventEmitter
diff --git a/src/gateway/__mocks__/gateway.ts b/src/gateway/__mocks__/gateway-impl.ts
similarity index 68%
rename from src/gateway/__mocks__/gateway.ts
rename to src/gateway/__mocks__/gateway-impl.ts
index 74302f28..4facbdeb 100644
--- a/src/gateway/__mocks__/gateway.ts
+++ b/src/gateway/__mocks__/gateway-impl.ts
@@ -1,3 +1,5 @@
+import { Gateway } from "../gateway";
+
 export const mockGet = jest.fn();
 export const mockPost = jest.fn();
 export const mockDelete = jest.fn();
@@ -6,7 +8,10 @@ export const mockPut = jest.fn();
 export const mockStream = jest.fn();
 export const mockPaginate = jest.fn();
 
-export class Gateway {
+export class GatewayImpl implements Gateway {
+  uri = 'https://example.com';
+  version = '99.99.9';
+  streamingApiUrl = 'wss://example.com/stream';
   get = mockGet;
   post = mockPost;
   delete = mockDelete;
diff --git a/src/gateway/gateway-impl.ts b/src/gateway/gateway-impl.ts
new file mode 100644
index 00000000..824d38fd
--- /dev/null
+++ b/src/gateway/gateway-impl.ts
@@ -0,0 +1,354 @@
+import querystring, { ParsedUrlQueryInput } from 'querystring';
+import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
+import normalizeUrl from 'normalize-url'; // eslint-disable-line import/default
+import semver from 'semver';
+import { Instance } from '../entities';
+import {
+  MastoNotFoundError,
+  MastoRateLimitError,
+  MastoUnauthorizedError,
+  MastoForbiddenError,
+  MastoUnprocessableEntityError,
+} from '../errors';
+import { createFormData } from './create-form-data';
+import { isAxiosError } from './is-axios-error';
+import { WebSocketEvents } from './websocket';
+import {
+  Gateway,
+  GatewayConstructorParams,
+  LoginParams,
+  PaginateNext,
+} from './gateway';
+
+/**
+ * Mastodon network request wrapper
+ * @param params Optional params
+ */
+export class GatewayImpl implements Gateway<AxiosRequestConfig> {
+  /** Configured axios instance */
+  private axios: AxiosInstance;
+  /** URI of the instance */
+  private _uri = '';
+  /** Streaming API URL of the instance */
+  private _streamingApiUrl = '';
+  /** Version of the current instance */
+  version = '';
+  /** API token of the user */
+  accessToken?: string;
+
+  /**
+   * @param params Parameters
+   */
+  constructor(params: GatewayConstructorParams<AxiosRequestConfig>) {
+    this.uri = params.uri;
+
+    if (params.accessToken) {
+      this.accessToken = params.accessToken;
+    }
+
+    if (params.streamingApiUrl) {
+      this.streamingApiUrl = params.streamingApiUrl;
+    }
+
+    if (params.version) {
+      this.version = params.version;
+    }
+
+    this.axios = axios.create({
+      baseURL: this.uri,
+      transformResponse: this.transformResponse,
+      ...(params.defaultOptions ?? {}),
+    });
+
+    this.axios.interceptors.request.use(this.transformConfig);
+    this.axios.defaults.headers.common['Content-Type'] = 'application/json';
+    this.axios.defaults.headers.common.Authorization = `Bearer ${this.accessToken}`;
+  }
+
+  get uri() {
+    return this._uri;
+  }
+
+  set uri(uri: string) {
+    this._uri = normalizeUrl(uri);
+  }
+
+  get streamingApiUrl() {
+    return this._streamingApiUrl;
+  }
+
+  set streamingApiUrl(streamingApiUrl: string) {
+    this._streamingApiUrl = normalizeUrl(streamingApiUrl);
+  }
+
+  /**
+   * Login to Mastodon
+   * @param params Parameters
+   * @return Instance of Mastodon class
+   */
+  static async login<T extends typeof GatewayImpl>(
+    this: T,
+    params: LoginParams,
+  ) {
+    const gateway = new this(params) as InstanceType<T>;
+    const instance = await gateway.get<Instance>('/api/v1/instance');
+
+    gateway.version = instance.version;
+    gateway.streamingApiUrl = instance.urls.streaming_api;
+
+    return gateway;
+  }
+
+  /**
+   * Transform JSON to JS object
+   * @param response Response object
+   * @return Parsed entity
+   */
+  private transformResponse(data: string, _headers: unknown) {
+    try {
+      return JSON.parse(data);
+    } catch {
+      return data;
+    }
+  }
+
+  /**
+   * Encode data in request options and add authorization / content-type header
+   * @param config Axios config
+   * @return New config
+   */
+  private transformConfig(originalConfig: AxiosRequestConfig) {
+    const config = { ...originalConfig };
+
+    switch (config.headers['Content-Type']) {
+      case 'application/json':
+        config.data = JSON.stringify(config.data);
+
+        return config;
+
+      case 'multipart/form-data':
+        config.data = createFormData(config.data);
+
+        // In Node.js, axios doesn't set boundary data to the header
+        // so set it manually by using getHeaders of form-data node.js package
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        if (typeof (config.data as any).getHeaders === 'function') {
+          config.headers = {
+            ...config.headers,
+            // eslint-disable-next-line @typescript-eslint/no-explicit-any
+            ...(config.data as any).getHeaders(),
+          };
+        }
+
+        return config;
+
+      default:
+        return config;
+    }
+  }
+
+  /**
+   * Wrapper function for Axios
+   * @param options Axios options
+   * @param parse Whether parse response before return
+   * @return Parsed response object
+   */
+  protected async request<T>(options: AxiosRequestConfig) {
+    try {
+      return await this.axios.request<T>(options);
+    } catch (error) {
+      if (!isAxiosError(error)) {
+        throw error;
+      }
+
+      const status = error?.response?.status;
+      const message =
+        error?.response?.data?.error ?? 'Unexpected error occurred';
+
+      switch (status) {
+        case 401:
+          throw new MastoUnauthorizedError(message);
+        case 403:
+          throw new MastoForbiddenError(message);
+        case 404:
+          throw new MastoNotFoundError(message);
+        case 422:
+          throw new MastoUnprocessableEntityError(message);
+        case 429:
+          throw new MastoRateLimitError(message);
+        default:
+          throw error;
+      }
+    }
+  }
+
+  /**
+   * HTTP GET
+   * @param url URL to request
+   * @param params Query strings
+   * @param options Fetch API options
+   * @param parse Whether parse response before return
+   */
+  async get<T>(
+    path: string,
+    params: unknown = {},
+    options?: AxiosRequestConfig,
+  ) {
+    const response = await this.request<T>({
+      method: 'GET',
+      url: path,
+      params,
+      ...options,
+    });
+
+    return response.data;
+  }
+
+  /**
+   * HTTP POST
+   * @param url URL to request
+   * @param data Payload
+   * @param options Fetch API options
+   * @param parse Whether parse response before return
+   */
+  async post<T>(
+    path: string,
+    data: unknown = {},
+    options?: AxiosRequestConfig,
+  ) {
+    const response = await this.request<T>({
+      method: 'POST',
+      url: path,
+      data,
+      ...options,
+    });
+
+    return response.data;
+  }
+
+  /**
+   * HTTP PUT
+   * @param path Path to request
+   * @param data Payload
+   * @param options Fetch API options
+   * @param parse Whether parse response before return
+   */
+  async put<T>(path: string, data: unknown = {}, options?: AxiosRequestConfig) {
+    const response = await this.request<T>({
+      method: 'PUT',
+      url: path,
+      data,
+      ...options,
+    });
+
+    return response.data;
+  }
+
+  /**
+   * HTTP DELETE
+   * @param path Path to request
+   * @param data jPayload
+   * @param options Fetch API options
+   * @param parse Whether parse response before return
+   */
+  async delete<T>(
+    path: string,
+    data: unknown = {},
+    options?: AxiosRequestConfig,
+  ) {
+    const response = await this.request<T>({
+      method: 'DELETE',
+      url: path,
+      data,
+      ...options,
+    });
+
+    return response.data;
+  }
+
+  /**
+   * HTTP PATCH
+   * @param path Path to request
+   * @param data Payload
+   * @param options Fetch API options
+   * @param parse Whether parse response before return
+   */
+  async patch<T>(
+    path: string,
+    data: unknown = {},
+    options?: AxiosRequestConfig,
+  ) {
+    const response = await this.request<T>({
+      method: 'PATCH',
+      url: path,
+      data,
+      ...options,
+    });
+
+    return response.data;
+  }
+
+  /**
+   * Connect to a streaming
+   * @param path Path to stream
+   * @param params Query parameters
+   * @return Instance of EventEmitter
+   */
+  stream(path: string, params: ParsedUrlQueryInput = {}) {
+    const version = semver.coerce(this.version);
+    const protocols = [];
+
+    // Since v2.8.4, it is supported to pass access token with`Sec-Websocket-Protocol`
+    // https://github.com/tootsuite/mastodon/pull/10818
+    if (this.accessToken && version && semver.gte(version, '2.8.4')) {
+      protocols.push(this.accessToken);
+    } else if (this.accessToken) {
+      params.access_token = this.accessToken;
+    }
+
+    const url =
+      this.streamingApiUrl +
+      path +
+      (Object.keys(params).length ? `?${querystring.stringify(params)}` : '');
+
+    return new WebSocketEvents().connect(url, protocols);
+  }
+
+  /**
+   * Generate an iterable of the pagination.
+   * @param initialURL Path for the endpoint
+   * @param initialParams Query parameter
+   * @return Async iterable iterator of the pages.
+   * See also [MDN article about generator/iterator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators)
+   */
+  async *paginate<T, U>(
+    initialUrl: string,
+    initialParams?: U,
+  ): AsyncGenerator<T, void, PaginateNext<U>> {
+    let nextUrl: string | undefined = initialUrl;
+    let nextParams = initialParams;
+
+    while (nextUrl) {
+      const response: AxiosResponse<T> = await this.request<T>({
+        method: 'GET',
+        url: nextUrl,
+        params: nextParams,
+      });
+
+      // Yield will be argument of next()
+      const options = yield response.data;
+      // Get next URL from "next" in the link header
+      const linkHeaderNext = response.headers?.link?.match(
+        /<(.+?)>; rel="next"/,
+      )?.[1];
+
+      if (options?.reset) {
+        nextUrl = initialUrl;
+        nextParams = initialParams;
+      } else {
+        nextUrl = options?.url ?? linkHeaderNext;
+        nextParams = options?.params;
+      }
+    }
+  }
+}
diff --git a/src/gateway/gateway.ts b/src/gateway/gateway.ts
index bae58c4c..de691249 100644
--- a/src/gateway/gateway.ts
+++ b/src/gateway/gateway.ts
@@ -1,37 +1,5 @@
-import querystring, { ParsedUrlQueryInput } from 'querystring';
-import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
-import normalizeUrl from 'normalize-url'; // eslint-disable-line import/default
-import semver from 'semver';
-import { Instance } from '../entities';
-import {
-  MastoNotFoundError,
-  MastoRateLimitError,
-  MastoUnauthorizedError,
-  MastoForbiddenError,
-  MastoUnprocessableEntityError,
-} from '../errors';
-import { createFormData } from './create-form-data';
-import { isAxiosError } from './is-axios-error';
-import { WebSocketEvents } from './websocket';
-
-export interface GatewayConstructorParams {
-  /** URI of the instance */
-  uri: string;
-  /** Streaming API URL */
-  streamingApiUrl?: string;
-  /** Version of the instance */
-  version?: string;
-  /** Access token of the user */
-  accessToken?: string;
-  /** Axios configurations. See [Axios'](https://github.com/axios/axios#request-config) docs */
-  axiosConfig?: AxiosRequestConfig;
-}
-
-/** Params for login. `version` and `streamingApiUrl` will be set automatically so not needed */
-export type LoginParams = Omit<
-  GatewayConstructorParams,
-  'version' | 'streamingApiUrl'
->;
+import { ParsedUrlQueryInput } from 'querystring';
+import EventEmitter from 'eventemitter3';
 
 /**
  * Argument of `Gateway.paginate().next()`.
@@ -49,332 +17,42 @@ export type PaginateNext<Params> =
       params?: Params;
     };
 
-/**
- * Mastodon network request wrapper
- * @param params Optional params
- */
-export class Gateway {
-  /** Configured axios instance */
-  private axios: AxiosInstance;
-  /** URI of the instance */
-  private _uri = '';
-  /** Streaming API URL of the instance */
-  private _streamingApiUrl = '';
-  /** Version of the current instance */
-  version = '';
-  /** API token of the user */
+export interface Gateway<Options = undefined> {
+  uri: string;
+  streamingApiUrl: string;
+  version: string;
   accessToken?: string;
+  get<T>(path: string, params: unknown, options?: Options): Promise<T>;
+  post<T>(path: string, data: unknown, options?: Options): Promise<T>;
+  put<T>(path: string, data: unknown, options?: Options): Promise<T>;
+  patch<T>(path: string, data: unknown, options?: Options): Promise<T>;
+  delete<T>(path: string, data: unknown, options?: Options): Promise<T>;
+  stream(path: string, params: ParsedUrlQueryInput): Promise<EventEmitter>;
+  paginate<T, U>(
+    url: string,
+    params?: U,
+  ): AsyncGenerator<T, void, PaginateNext<U>>;
+}
 
-  /**
-   * @param params Parameters
-   */
-  constructor(params: GatewayConstructorParams) {
-    this.uri = params.uri;
-
-    if (params.accessToken) {
-      this.accessToken = params.accessToken;
-    }
-
-    if (params.streamingApiUrl) {
-      this.streamingApiUrl = params.streamingApiUrl;
-    }
-
-    if (params.version) {
-      this.version = params.version;
-    }
-
-    this.axios = axios.create({
-      baseURL: this.uri,
-      transformResponse: this.transformResponse,
-      ...(params.axiosConfig || {}),
-    });
-
-    this.axios.interceptors.request.use(this.transformConfig);
-    this.axios.defaults.headers.common['Content-Type'] = 'application/json';
-    this.axios.defaults.headers.common.Authorization = `Bearer ${this.accessToken}`;
-  }
-
-  get uri() {
-    return this._uri;
-  }
-
-  set uri(uri: string) {
-    this._uri = normalizeUrl(uri);
-  }
-
-  get streamingApiUrl() {
-    return this._streamingApiUrl;
-  }
-
-  set streamingApiUrl(streamingApiUrl: string) {
-    this._streamingApiUrl = normalizeUrl(streamingApiUrl);
-  }
-
-  /**
-   * Login to Mastodon
-   * @param params Parameters
-   * @return Instance of Mastodon class
-   */
-  static async login<T extends typeof Gateway>(this: T, params: LoginParams) {
-    const gateway = new this(params) as InstanceType<T>;
-    const instance = await gateway.get<Instance>('/api/v1/instance');
-
-    gateway.version = instance.version;
-    gateway.streamingApiUrl = instance.urls.streaming_api;
-
-    return gateway;
-  }
-
-  /**
-   * Transform JSON to JS object
-   * @param response Response object
-   * @return Parsed entity
-   */
-  private transformResponse(data: string, _headers: unknown) {
-    try {
-      return JSON.parse(data);
-    } catch {
-      return data;
-    }
-  }
-
-  /**
-   * Encode data in request options and add authorization / content-type header
-   * @param config Axios config
-   * @return New config
-   */
-  private transformConfig(originalConfig: AxiosRequestConfig) {
-    const config = { ...originalConfig };
-
-    switch (config.headers['Content-Type']) {
-      case 'application/json':
-        config.data = JSON.stringify(config.data);
-
-        return config;
-
-      case 'multipart/form-data':
-        config.data = createFormData(config.data);
-
-        // In Node.js, axios doesn't set boundary data to the header
-        // so set it manually by using getHeaders of form-data node.js package
-        // eslint-disable-next-line @typescript-eslint/no-explicit-any
-        if (typeof (config.data as any).getHeaders === 'function') {
-          config.headers = {
-            ...config.headers,
-            // eslint-disable-next-line @typescript-eslint/no-explicit-any
-            ...(config.data as any).getHeaders(),
-          };
-        }
-
-        return config;
-
-      default:
-        return config;
-    }
-  }
-
-  /**
-   * Wrapper function for Axios
-   * @param options Axios options
-   * @param parse Whether parse response before return
-   * @return Parsed response object
-   */
-  protected async request<T>(options: AxiosRequestConfig) {
-    try {
-      return await this.axios.request<T>(options);
-    } catch (error) {
-      if (!isAxiosError(error)) {
-        throw error;
-      }
-
-      const status = error?.response?.status;
-      const message =
-        error?.response?.data?.error ?? 'Unexpected error occurred';
-
-      switch (status) {
-        case 401:
-          throw new MastoUnauthorizedError(message);
-        case 403:
-          throw new MastoForbiddenError(message);
-        case 404:
-          throw new MastoNotFoundError(message);
-        case 422:
-          throw new MastoUnprocessableEntityError(message);
-        case 429:
-          throw new MastoRateLimitError(message);
-        default:
-          throw error;
-      }
-    }
-  }
-
-  /**
-   * HTTP GET
-   * @param url URL to request
-   * @param params Query strings
-   * @param options Fetch API options
-   * @param parse Whether parse response before return
-   */
-  async get<T>(
-    path: string,
-    params: unknown = {},
-    options?: AxiosRequestConfig,
-  ) {
-    const response = await this.request<T>({
-      method: 'GET',
-      url: path,
-      params,
-      ...options,
-    });
-
-    return response.data;
-  }
-
-  /**
-   * HTTP POST
-   * @param url URL to request
-   * @param data Payload
-   * @param options Fetch API options
-   * @param parse Whether parse response before return
-   */
-  async post<T>(
-    path: string,
-    data: unknown = {},
-    options?: AxiosRequestConfig,
-  ) {
-    const response = await this.request<T>({
-      method: 'POST',
-      url: path,
-      data,
-      ...options,
-    });
-
-    return response.data;
-  }
-
-  /**
-   * HTTP PUT
-   * @param path Path to request
-   * @param data Payload
-   * @param options Fetch API options
-   * @param parse Whether parse response before return
-   */
-  async put<T>(path: string, data: unknown = {}, options?: AxiosRequestConfig) {
-    const response = await this.request<T>({
-      method: 'PUT',
-      url: path,
-      data,
-      ...options,
-    });
-
-    return response.data;
-  }
-
-  /**
-   * HTTP DELETE
-   * @param path Path to request
-   * @param data jPayload
-   * @param options Fetch API options
-   * @param parse Whether parse response before return
-   */
-  async delete<T>(
-    path: string,
-    data: unknown = {},
-    options?: AxiosRequestConfig,
-  ) {
-    const response = await this.request<T>({
-      method: 'DELETE',
-      url: path,
-      data,
-      ...options,
-    });
-
-    return response.data;
-  }
-
-  /**
-   * HTTP PATCH
-   * @param path Path to request
-   * @param data Payload
-   * @param options Fetch API options
-   * @param parse Whether parse response before return
-   */
-  async patch<T>(
-    path: string,
-    data: unknown = {},
-    options?: AxiosRequestConfig,
-  ) {
-    const response = await this.request<T>({
-      method: 'PATCH',
-      url: path,
-      data,
-      ...options,
-    });
-
-    return response.data;
-  }
-
-  /**
-   * Connect to a streaming
-   * @param path Path to stream
-   * @param params Query parameters
-   * @return Instance of EventEmitter
-   */
-  stream(path: string, params: ParsedUrlQueryInput = {}) {
-    const version = semver.coerce(this.version);
-    const protocols = [];
-
-    // Since v2.8.4, it is supported to pass access token with`Sec-Websocket-Protocol`
-    // https://github.com/tootsuite/mastodon/pull/10818
-    if (this.accessToken && version && semver.gte(version, '2.8.4')) {
-      protocols.push(this.accessToken);
-    } else if (this.accessToken) {
-      params.access_token = this.accessToken;
-    }
-
-    const url =
-      this.streamingApiUrl +
-      path +
-      (Object.keys(params).length ? `?${querystring.stringify(params)}` : '');
-
-    return new WebSocketEvents().connect(url, protocols);
-  }
-
-  /**
-   * Generate an iterable of the pagination.
-   * @param initialURL Path for the endpoint
-   * @param initialParams Query parameter
-   * @return Async iterable iterator of the pages.
-   * See also [MDN article about generator/iterator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators)
-   */
-  async *paginate<T, U>(
-    initialUrl: string,
-    initialParams?: U,
-  ): AsyncGenerator<T, void, PaginateNext<U>> {
-    let nextUrl: string | undefined = initialUrl;
-    let nextParams = initialParams;
-
-    while (nextUrl) {
-      const response: AxiosResponse<T> = await this.request<T>({
-        method: 'GET',
-        url: nextUrl,
-        params: nextParams,
-      });
+export interface GatewayConstructorParams<T = undefined> {
+  /** URI of the instance */
+  uri: string;
+  /** Streaming API URL */
+  streamingApiUrl?: string;
+  /** Version of the instance */
+  version?: string;
+  /** Access token of the user */
+  accessToken?: string;
+  /** Axios configurations. See [Axios'](https://github.com/axios/axios#request-config) docs */
+  defaultOptions?: T;
+}
 
-      // Yield will be argument of next()
-      const options = yield response.data;
-      // Get next URL from "next" in the link header
-      const linkHeaderNext = response.headers?.link?.match(
-        /<(.+?)>; rel="next"/,
-      )?.[1];
+export type LoginParams = Omit<
+  GatewayConstructorParams,
+  'version' | 'streamingApiUrl'
+>;
 
-      if (options?.reset) {
-        nextUrl = initialUrl;
-        nextParams = initialParams;
-      } else {
-        nextUrl = options?.url ?? linkHeaderNext;
-        nextParams = options?.params;
-      }
-    }
-  }
+export interface GatewayConstructor<Options = undefined> {
+  new (params: GatewayConstructorParams<Options>): Gateway;
+  login(params: LoginParams): Promise<Gateway>;
 }
diff --git a/src/gateway/index.ts b/src/gateway/index.ts
index 1242a9da..9e31d393 100644
--- a/src/gateway/index.ts
+++ b/src/gateway/index.ts
@@ -1,5 +1,6 @@
 export * from './create-form-data';
 export * from './decorators';
 export * from './gateway';
+export * from './gateway-impl';
 export * from './is-axios-error';
 export * from './websocket';


Test patch:
diff --git a/src/clients/masto-admin/__tests__/masto-admin.spec.ts b/src/clients/masto-admin/__tests__/masto-admin.spec.ts
index 4e7b5ba4..953dab0e 100644
--- a/src/clients/masto-admin/__tests__/masto-admin.spec.ts
+++ b/src/clients/masto-admin/__tests__/masto-admin.spec.ts
@@ -1,9 +1,9 @@
 // @ts-ignore
 // prettier-ignore
-import { Gateway, mockDelete, mockGet, mockPaginate, mockPatch, mockPost, mockPut, mockStream } from '../../../gateway/gateway';
+import { Gateway, mockDelete, mockGet, mockPaginate, mockPatch, mockPost, mockPut, mockStream } from '../../../gateway/gateway-impl';
 import { MastoAdmin } from '../masto-admin';
 
-jest.mock('../../../gateway/gateway');
+jest.mock('../../../gateway/gateway-impl');
 
 describe('MastoAdmin', () => {
   const masto = new MastoAdmin({
diff --git a/src/clients/masto/__tests__/masto.spec.ts b/src/clients/masto/__tests__/masto.spec.ts
index 9f84b6b8..d2a39c72 100644
--- a/src/clients/masto/__tests__/masto.spec.ts
+++ b/src/clients/masto/__tests__/masto.spec.ts
@@ -2,10 +2,10 @@
 import { Masto } from '../masto';
 // @ts-ignore
 // prettier-ignore
-import { Gateway, mockGet, mockPost, mockDelete, mockPut, mockPatch, mockPaginate, mockStream } from '../../../gateway/gateway';
+import { Gateway, mockGet, mockPost, mockDelete, mockPut, mockPatch, mockPaginate, mockStream } from '../../../gateway/gateway-impl';
 
 jest.mock('../../../gateway/websocket');
-jest.mock('../../../gateway/gateway');
+jest.mock('../../../gateway/gateway-impl');
 
 describe('Masto', () => {
   const masto = new Masto({
diff --git a/src/gateway/__tests__/gateway.spec.ts b/src/gateway/__tests__/gateway-impl.spec.ts
similarity index 97%
rename from src/gateway/__tests__/gateway.spec.ts
rename to src/gateway/__tests__/gateway-impl.spec.ts
index f8fa31b7..3c8ad9ae 100644
--- a/src/gateway/__tests__/gateway.spec.ts
+++ b/src/gateway/__tests__/gateway-impl.spec.ts
@@ -1,6 +1,6 @@
 /* eslint-disable */
 import axios from 'axios';
-import { Gateway } from '../gateway';
+import { GatewayImpl } from '../gateway-impl';
 // @ts-ignore
 import { WebSocketEvents, mockConnect } from '../websocket';
 import {
@@ -20,8 +20,8 @@ jest.mock('axios');
 const mockAxios = jest.genMockFromModule<typeof axios>('axios');
 (axios.create as jest.Mock).mockImplementation(() => mockAxios);
 
-describe('Gateway', () => {
-  const gateway = new Gateway({
+describe('GatewayImpl', () => {
+  const gateway = new GatewayImpl({
     uri: 'https://example.com',
     version: '99.9.9',
     streamingApiUrl: 'wss://example.com',
@@ -50,14 +50,14 @@ describe('Gateway', () => {
       uri: 'https://example.com',
       accessToken: 'some token',
     };
-    const gateway = await Gateway.login(params);
+    const gateway = await GatewayImpl.login(params);
 
     expect(gateway.version).toBe('2.8.0');
     expect(gateway.streamingApiUrl).toBe('wss://example.com/stream');
   });
 
   test('streamingApiUrl has been set if construct with streamingApiUrl', () => {
-    const customGateway = new Gateway({
+    const customGateway = new GatewayImpl({
       uri: 'https://example.com',
       streamingApiUrl: 'wss://example.com',
     });
@@ -65,7 +65,7 @@ describe('Gateway', () => {
   });
 
   test('version has been set if construct with version ', () => {
-    const customGateway = new Gateway({
+    const customGateway = new GatewayImpl({
       uri: 'https://example.com',
       version: '1.2.3',
     });
@@ -73,7 +73,7 @@ describe('Gateway', () => {
   });
 
   test('accessToken has been set if construct with accessToken', () => {
-    const customGateway = new Gateway({
+    const customGateway = new GatewayImpl({
       uri: 'https://example.com',
       accessToken: 'token token',
     });
@@ -81,7 +81,7 @@ describe('Gateway', () => {
   });
 
   test('this._uri accessor works', () => {
-    const customGateway = new Gateway({
+    const customGateway = new GatewayImpl({
       uri: 'https://example.com/aaa',
     });
     customGateway.uri = 'https://example.com/bbb';
@@ -89,7 +89,7 @@ describe('Gateway', () => {
   });
 
   test('this._streamingApiUrl accessor works', () => {
-    const customGateway = new Gateway({
+    const customGateway = new GatewayImpl({
       uri: 'wss://example.com/aaa',
     });
     customGateway.uri = 'wss://example.com/bbb';