Repository: neet/masto.js
Pull request #197 (2020-01-30 14:39:55)

Merge pull request #197 from neet/custom-errors


Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a83e2d39..0e07afd1 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -27,6 +27,6 @@ There are some useful scripts in `package.json` which you can with `yarn run`.
 - `docs:build`     - Generate documentation by TypeDoc
 
 ## Testing
-We write tests to check if the program works fine. Specifications live in `__tests__` directory. We enforce **100% codecov** on this library.
+We write tests to check if the program works fine. Specifications live in `__tests__` directory.
 
 Husky, a hooker of git, always validates your code satisfies `test:eslint` so you don't need to run them.
diff --git a/cspell.json b/cspell.json
index 4ffab944..3e1c5c94 100644
--- a/cspell.json
+++ b/cspell.json
@@ -28,6 +28,7 @@
     "unreblog",
     "unreviewed",
     "unsilence",
-    "unbookmark"
+    "unbookmark",
+    "unprocessable"
   ]
 }
diff --git a/src/clients/masto-admin/index.ts b/src/clients/masto-admin/index.ts
new file mode 100644
index 00000000..a00c5109
--- /dev/null
+++ b/src/clients/masto-admin/index.ts
@@ -0,0 +1,2 @@
+export * from './masto-admin';
+export * from './params';
diff --git a/src/clients/masto-admin/masto-admin.ts b/src/clients/masto-admin/masto-admin.ts
index 6a03bdee..d2e115e8 100644
--- a/src/clients/masto-admin/masto-admin.ts
+++ b/src/clients/masto-admin/masto-admin.ts
@@ -1,7 +1,6 @@
-import { AdminAccount } from '../../entities/admin-account';
-import { AdminReport } from '../../entities/admin-report';
 import { Gateway } from '../../gateway/gateway';
 import { available } from '../../gateway/decorators';
+import { AdminAccount, AdminReport } from '../../entities';
 import {
   AdminActionAccountParams,
   AdminFetchAccountParams,
diff --git a/src/clients/masto/index.ts b/src/clients/masto/index.ts
new file mode 100644
index 00000000..33d2a1d7
--- /dev/null
+++ b/src/clients/masto/index.ts
@@ -0,0 +1,2 @@
+export * from './masto';
+export * from './params';
diff --git a/src/clients/masto/masto.ts b/src/clients/masto/masto.ts
index 1148b40f..e68cca56 100644
--- a/src/clients/masto/masto.ts
+++ b/src/clients/masto/masto.ts
@@ -1,33 +1,36 @@
-import { Announcement } from '../../entities/announcement';
-import { Reaction } from '../../entities/reaction';
+import { Gateway } from '../../gateway/gateway';
+import { available } from '../../gateway/decorators';
 import {
+  Announcement,
+  Reaction,
   Account,
   AccountCredentials,
   AccountIdentityProof,
-} from '../../entities/account';
-import { Application } from '../../entities/application';
-import { Attachment } from '../../entities/attachment';
-import { Card } from '../../entities/card';
-import { Context } from '../../entities/context';
-import { Conversation } from '../../entities/conversation';
-import { Emoji } from '../../entities/emoji';
-import { Filter } from '../../entities/filter';
-import { Instance, InstanceActivity } from '../../entities/instance';
-import { List } from '../../entities/list';
-import { Notification } from '../../entities/notification';
-import { OAuthClient, OAuthToken } from '../../entities/oauth';
-import { Poll } from '../../entities/poll';
-import { Preference } from '../../entities/preference';
-import { PushSubscription } from '../../entities/push-subscription';
-import { Relationship } from '../../entities/relationship';
-import { Results, ResultsV1 } from '../../entities/results';
-import { ScheduledStatus } from '../../entities/scheduled-status';
-import { Status } from '../../entities/status';
-import { Trend } from '../../entities/trend';
-import { FeaturedTag } from '../../entities/featured-tags';
-import { MarkerMap } from '../../entities/marker';
-import { Gateway } from '../../gateway/gateway';
-import { available } from '../../gateway/decorators';
+  Application,
+  Attachment,
+  Card,
+  Context,
+  Conversation,
+  Emoji,
+  Filter,
+  Instance,
+  InstanceActivity,
+  List,
+  Notification,
+  OAuthClient,
+  OAuthToken,
+  Poll,
+  Preference,
+  PushSubscription,
+  Relationship,
+  Results,
+  ResultsV1,
+  ScheduledStatus,
+  Status,
+  Trend,
+  FeaturedTag,
+  MarkerMap,
+} from '../../entities';
 import {
   AddPushSubscriptionParams,
   CreateAccountParams,
diff --git a/src/clients/masto/params.ts b/src/clients/masto/params.ts
index 91c1ed2c..ced44e17 100644
--- a/src/clients/masto/params.ts
+++ b/src/clients/masto/params.ts
@@ -1,9 +1,13 @@
-import { AccountField, AccountSource } from '../../entities/account';
-import { FilterContext } from '../../entities/filter';
-import { NotificationType } from '../../entities/notification';
-import { PushSubscriptionAlerts } from '../../entities/push-subscription';
-import { StatusVisibility } from '../../entities/status';
-import { MarkerTimeline, Marker } from '../../entities/marker';
+import {
+  AccountField,
+  AccountSource,
+  FilterContext,
+  NotificationType,
+  PushSubscriptionAlerts,
+  StatusVisibility,
+  MarkerTimeline,
+  Marker,
+} from '../../entities';
 
 export interface PaginationParams {
   /** Get a list of items with ID less than this value */
diff --git a/src/entities/index.ts b/src/entities/index.ts
new file mode 100644
index 00000000..0b68e6db
--- /dev/null
+++ b/src/entities/index.ts
@@ -0,0 +1,28 @@
+export * from './account';
+export * from './admin-account';
+export * from './admin-report';
+export * from './announcement';
+export * from './application';
+export * from './attachment';
+export * from './card';
+export * from './context';
+export * from './conversation';
+export * from './emoji';
+export * from './featured-tags';
+export * from './filter';
+export * from './instance';
+export * from './list';
+export * from './marker';
+export * from './mention';
+export * from './notification';
+export * from './oauth';
+export * from './poll';
+export * from './preference';
+export * from './push-subscription';
+export * from './reaction';
+export * from './relationship';
+export * from './results';
+export * from './scheduled-status';
+export * from './status';
+export * from './tag';
+export * from './trend';
diff --git a/src/errors/index.ts b/src/errors/index.ts
new file mode 100644
index 00000000..9db619b9
--- /dev/null
+++ b/src/errors/index.ts
@@ -0,0 +1,5 @@
+export * from './masto-forbidden-error';
+export * from './masto-not-found-error';
+export * from './masto-rate-limit-error';
+export * from './masto-unauthorized-error';
+export * from './masto-unprocessable-entity-error';
diff --git a/src/errors/masto-forbidden-error.ts b/src/errors/masto-forbidden-error.ts
new file mode 100644
index 00000000..119774f0
--- /dev/null
+++ b/src/errors/masto-forbidden-error.ts
@@ -0,0 +1,10 @@
+/**
+ * Mastodon forbidden error
+ * @param message Message for users
+ */
+export class MastoForbiddenError extends Error {
+  constructor(...args: string[]) {
+    super(...args);
+    this.name = 'MastoForbiddenError';
+  }
+}
diff --git a/src/errors/masto-unprocessable-entity-error.ts b/src/errors/masto-unprocessable-entity-error.ts
new file mode 100644
index 00000000..36773ae2
--- /dev/null
+++ b/src/errors/masto-unprocessable-entity-error.ts
@@ -0,0 +1,10 @@
+/**
+ * Mastodon unprocessable entity
+ * @param message Message for users
+ */
+export class MastoUnprocessableEntityError extends Error {
+  constructor(...args: string[]) {
+    super(...args);
+    this.name = 'MastoUnprocessableEntityError';
+  }
+}
diff --git a/src/gateway/decorators.ts b/src/gateway/decorators.ts
index 385d869a..37984b86 100644
--- a/src/gateway/decorators.ts
+++ b/src/gateway/decorators.ts
@@ -1,6 +1,6 @@
 import semver from 'semver';
-import { MastoNotFoundError } from '../errors/masto-not-found-error';
-import { Gateway } from '../gateway/gateway';
+import { MastoNotFoundError } from '../errors';
+import { Gateway } from './gateway';
 
 export interface AvailableParams {
   since?: string;
diff --git a/src/gateway/gateway.ts b/src/gateway/gateway.ts
index 2bd0a463..bae58c4c 100644
--- a/src/gateway/gateway.ts
+++ b/src/gateway/gateway.ts
@@ -2,10 +2,14 @@ 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/instance';
-import { MastoNotFoundError } from '../errors/masto-not-found-error';
-import { MastoRateLimitError } from '../errors/masto-rate-limit-error';
-import { MastoUnauthorizedError } from '../errors/masto-unauthorized-error';
+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';
@@ -183,20 +187,19 @@ export class Gateway {
         throw error;
       }
 
-      // Why??
-      // eslint-disable-next-line @typescript-eslint/no-use-before-define
       const status = error?.response?.status;
-
-      // Error response from REST API might contain error key
-      // https://docs.joinmastodon.org/api/entities/#error
       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:
diff --git a/src/gateway/index.ts b/src/gateway/index.ts
new file mode 100644
index 00000000..1242a9da
--- /dev/null
+++ b/src/gateway/index.ts
@@ -0,0 +1,5 @@
+export * from './create-form-data';
+export * from './decorators';
+export * from './gateway';
+export * from './is-axios-error';
+export * from './websocket';
diff --git a/src/gateway/websocket.ts b/src/gateway/websocket.ts
index 1bf12445..206482d2 100644
--- a/src/gateway/websocket.ts
+++ b/src/gateway/websocket.ts
@@ -1,8 +1,6 @@
 import EventEmitter from 'eventemitter3';
 import WebSocket from 'isomorphic-ws';
-import { Conversation } from '../entities/conversation';
-import { Notification } from '../entities/notification';
-import { Status } from '../entities/status';
+import { Conversation, Notification, Status } from '../entities';
 
 /** Map of event name and callback argument */
 export interface EventTypeMap {
diff --git a/src/index.ts b/src/index.ts
index e21e3d35..f0e39f23 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,39 +1,7 @@
-export * from './gateway/gateway';
-export * from './gateway/websocket';
-export * from './gateway/is-axios-error';
-export * from './gateway/decorators';
+export * from './gateway';
+export * from './entities';
+export * from './errors';
+export * from './clients/masto';
+export * from './clients/masto-admin';
 
-export * from './clients/masto/masto';
-export * from './clients/masto/params';
-export * from './clients/masto-admin/masto-admin';
-export * from './clients/masto-admin/params';
-
-export * from './entities/account';
-export * from './entities/admin-account';
-export * from './entities/admin-report';
-export * from './entities/application';
-export * from './entities/attachment';
-export * from './entities/card';
-export * from './entities/context';
-export * from './entities/conversation';
-export * from './entities/emoji';
-export * from './entities/filter';
-export * from './entities/instance';
-export * from './entities/list';
-export * from './entities/mention';
-export * from './entities/notification';
-export * from './entities/oauth';
-export * from './entities/poll';
-export * from './entities/push-subscription';
-export * from './entities/preference';
-export * from './entities/relationship';
-export * from './entities/results';
-export * from './entities/status';
-export * from './entities/scheduled-status';
-export * from './entities/tag';
-
-export * from './errors/masto-not-found-error';
-export * from './errors/masto-rate-limit-error';
-export * from './errors/masto-unauthorized-error';
-
-export { Masto as default } from './clients/masto/masto';
+export { Masto as default } from './clients/masto';


Test patch:
diff --git a/src/gateway/__tests__/decorators.spec.ts b/src/gateway/__tests__/decorators.spec.ts
index 4867932e..9f20c07d 100644
--- a/src/gateway/__tests__/decorators.spec.ts
+++ b/src/gateway/__tests__/decorators.spec.ts
@@ -1,4 +1,4 @@
-import { Gateway } from '../../gateway/gateway';
+import { Gateway } from '../gateway';
 import { available } from '../decorators';
 
 describe('available', () => {
diff --git a/src/gateway/__tests__/gateway.spec.ts b/src/gateway/__tests__/gateway.spec.ts
index 405d6214..f8fa31b7 100644
--- a/src/gateway/__tests__/gateway.spec.ts
+++ b/src/gateway/__tests__/gateway.spec.ts
@@ -3,9 +3,13 @@ import axios from 'axios';
 import { Gateway } from '../gateway';
 // @ts-ignore
 import { WebSocketEvents, mockConnect } from '../websocket';
-import { MastoUnauthorizedError } from '../../errors/masto-unauthorized-error';
-import { MastoNotFoundError } from '../../errors/masto-not-found-error';
-import { MastoRateLimitError } from '../../errors/masto-rate-limit-error';
+import {
+  MastoNotFoundError,
+  MastoRateLimitError,
+  MastoUnauthorizedError,
+  MastoForbiddenError,
+  MastoUnprocessableEntityError,
+} from '../../errors';
 import 'isomorphic-form-data';
 
 jest.mock('../websocket');
@@ -209,6 +213,48 @@ describe('Gateway', () => {
     );
   });
 
+  test('throw MastodonForbiddenError when 403 responded', async () => {
+    const options = {
+      method: 'POST',
+      url: '/',
+    };
+
+    ((mockAxios.request as any) as jest.Mock).mockRejectedValue({
+      response: {
+        status: 403,
+        data: {
+          error: 'Forbidden',
+        },
+      },
+    });
+
+    // @ts-ignore
+    expect(gateway.request(options)).rejects.toThrow(
+      new MastoForbiddenError('Forbidden'),
+    );
+  });
+
+  test('throw MastoUnprocessableEntityError when 422 responded', async () => {
+    const options = {
+      method: 'POST',
+      url: '/',
+    };
+
+    ((mockAxios.request as any) as jest.Mock).mockRejectedValue({
+      response: {
+        status: 422,
+        data: {
+          error: 'UnprocessableEntity',
+        },
+      },
+    });
+
+    // @ts-ignore
+    expect(gateway.request(options)).rejects.toThrow(
+      new MastoUnprocessableEntityError('UnprocessableEntity'),
+    );
+  });
+
   test('AxiosError: throw given error directly if non of prepared statuses matched', () => {
     const options = {
       method: 'POST',