From 99cd6d1f626dfc5f7ccc15a6a322e807b15c08b1 Mon Sep 17 00:00:00 2001 From: Ryo Igarashi Date: Thu, 2 Nov 2023 11:09:39 +0900 Subject: [PATCH] fix: Add minimal validation to config --- src/adapters/config/http-config.spec.ts | 24 +++++++++++ src/adapters/config/http-config.ts | 16 +++++++- src/adapters/config/validators.spec.ts | 40 +++++++++++++++++++ src/adapters/config/validators.ts | 12 ++++++ src/adapters/config/web-socket-config.spec.ts | 36 +++++++++++++++++ src/adapters/config/web-socket-config.ts | 14 ++++++- src/adapters/http/http-native-impl.spec.ts | 2 +- 7 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 src/adapters/config/validators.spec.ts create mode 100644 src/adapters/config/validators.ts diff --git a/src/adapters/config/http-config.spec.ts b/src/adapters/config/http-config.spec.ts index 0d57eb4f..13f15330 100644 --- a/src/adapters/config/http-config.spec.ts +++ b/src/adapters/config/http-config.spec.ts @@ -1,7 +1,31 @@ +import { MastoInvalidArgumentError } from "../errors"; import { SerializerNativeImpl } from "../serializers"; import { HttpConfigImpl } from "./http-config"; describe("Config", () => { + it("throws invalid argument error when url is not specified", () => { + expect(() => { + new HttpConfigImpl( + { + url: "", + }, + new SerializerNativeImpl(), + ); + }).toThrowError(MastoInvalidArgumentError); + }); + + it("throws invalid argument error when timeout is less than zero", () => { + expect(() => { + new HttpConfigImpl( + { + url: "https://example.com", + timeout: -1, + }, + new SerializerNativeImpl(), + ); + }).toThrowError(MastoInvalidArgumentError); + }); + it("creates header", () => { const config = new HttpConfigImpl( { diff --git a/src/adapters/config/http-config.ts b/src/adapters/config/http-config.ts index 3a969a4a..ed623549 100644 --- a/src/adapters/config/http-config.ts +++ b/src/adapters/config/http-config.ts @@ -1,6 +1,8 @@ import { type HttpConfig, type Serializer } from "../../interfaces"; +import { MastoInvalidArgumentError } from "../errors"; import { mergeAbortSignals } from "./merge-abort-signals"; import { mergeHeadersInit } from "./merge-headers-init"; +import { isPositiveInteger, isURL } from "./validators"; export interface MastoHttpConfigProps { /** @@ -34,7 +36,19 @@ export class HttpConfigImpl implements HttpConfig { constructor( private readonly props: MastoHttpConfigProps, private readonly serializer: Serializer, - ) {} + ) { + if (!isURL(this.props.url)) { + throw new MastoInvalidArgumentError("url is required"); + } + if ( + this.props.timeout != undefined && + !isPositiveInteger(this.props.timeout) + ) { + throw new MastoInvalidArgumentError( + "timeout must be greater than or equal to zero", + ); + } + } mergeRequestInitWithDefaults(override: RequestInit = {}): RequestInit { const requestInit: RequestInit = { ...this.props.requestInit }; diff --git a/src/adapters/config/validators.spec.ts b/src/adapters/config/validators.spec.ts new file mode 100644 index 00000000..1050f251 --- /dev/null +++ b/src/adapters/config/validators.spec.ts @@ -0,0 +1,40 @@ +import { isPositiveInteger, isURL } from "./validators"; + +describe("isURL", () => { + it("validates if the URL is valid", () => { + const res = isURL("https://example.com"); + expect(res).toBe(true); + }); + + it("returns false if the URL is invalid", () => { + const res = isURL("example.com"); + expect(res).toBe(false); + }); + + it("returns false if the URL is empty", () => { + const res = isURL(""); + expect(res).toBe(false); + }); +}); + +describe("isPositiveInteger", () => { + it("validates if the number is a positive integer", () => { + const res = isPositiveInteger(1); + expect(res).toBe(true); + }); + + it("returns false if the number is not a positive integer", () => { + const res = isPositiveInteger(1.1); + expect(res).toBe(false); + }); + + it("returns false if the number is negative", () => { + const res = isPositiveInteger(-1); + expect(res).toBe(false); + }); + + it("returns false if the number is zero", () => { + const res = isPositiveInteger(0); + expect(res).toBe(false); + }); +}); diff --git a/src/adapters/config/validators.ts b/src/adapters/config/validators.ts new file mode 100644 index 00000000..94e72d55 --- /dev/null +++ b/src/adapters/config/validators.ts @@ -0,0 +1,12 @@ +export const isPositiveInteger = (value: number): boolean => { + return value > 0 && value % 1 === 0; +}; + +export const isURL = (value: string): boolean => { + try { + new URL(value); + return true; + } catch { + return false; + } +}; diff --git a/src/adapters/config/web-socket-config.spec.ts b/src/adapters/config/web-socket-config.spec.ts index 57aa57af..ecd0e182 100644 --- a/src/adapters/config/web-socket-config.spec.ts +++ b/src/adapters/config/web-socket-config.spec.ts @@ -1,7 +1,43 @@ +import { MastoInvalidArgumentError } from "../errors"; import { SerializerNativeImpl } from "../serializers"; import { WebSocketConfigImpl } from "./web-socket-config"; describe("WebSocketConfigImpl", () => { + it("throws invalid argument error when url is not specified", () => { + expect(() => { + new WebSocketConfigImpl( + { + streamingApiUrl: "", + }, + new SerializerNativeImpl(), + ); + }).toThrowError(MastoInvalidArgumentError); + }); + + it("throws invalid argument error when retry is less than zero", () => { + expect(() => { + new WebSocketConfigImpl( + { + streamingApiUrl: "wss://mastodon.social", + retry: -1, + }, + new SerializerNativeImpl(), + ); + }).toThrowError(MastoInvalidArgumentError); + }); + + it("throws invalid argument error when retry is not an integer", () => { + expect(() => { + new WebSocketConfigImpl( + { + streamingApiUrl: "wss://mastodon.social", + retry: 1.5, + }, + new SerializerNativeImpl(), + ); + }).toThrowError(MastoInvalidArgumentError); + }); + it("resolves WS path with path", () => { const config = new WebSocketConfigImpl( { diff --git a/src/adapters/config/web-socket-config.ts b/src/adapters/config/web-socket-config.ts index fe7fc9a9..5eccc838 100644 --- a/src/adapters/config/web-socket-config.ts +++ b/src/adapters/config/web-socket-config.ts @@ -1,4 +1,6 @@ import { type Serializer, type WebSocketConfig } from "../../interfaces"; +import { MastoInvalidArgumentError } from "../errors"; +import { isPositiveInteger, isURL } from "./validators"; export interface WebSocketConfigProps { /** @@ -57,7 +59,17 @@ export class WebSocketConfigImpl implements WebSocketConfig { constructor( private readonly props: WebSocketConfigProps, private readonly serializer: Serializer, - ) {} + ) { + if (!isURL(this.props.streamingApiUrl)) { + throw new MastoInvalidArgumentError(`streamingApiUrl is required`); + } + if ( + typeof this.props.retry === "number" && + !isPositiveInteger(this.props.retry) + ) { + throw new MastoInvalidArgumentError("retry must be a positive integer"); + } + } getProtocols(protocols: readonly string[] = []): string[] { if ( diff --git a/src/adapters/http/http-native-impl.spec.ts b/src/adapters/http/http-native-impl.spec.ts index 52f36c4f..c4342570 100644 --- a/src/adapters/http/http-native-impl.spec.ts +++ b/src/adapters/http/http-native-impl.spec.ts @@ -15,7 +15,7 @@ describe("HttpNativeImpl", () => { new HttpConfigImpl( { url: "https://example.com", - timeout: 0, + timeout: 1, }, serializer, ),