Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade test suite jsr #63

Merged
merged 9 commits into from
Sep 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
"imports": {
"@deno/dnt": "jsr:@deno/dnt@^0.41.3",
"@hey-api/openapi-ts": "npm:@hey-api/openapi-ts",
"@hongminhee/deno-mock-fetch": "jsr:@hongminhee/deno-mock-fetch@^0.3.2",
"@lambdalisue/systemopen": "jsr:@lambdalisue/systemopen@^1.0.0",
"@std/assert": "jsr:@std/assert@^1.0.5",
"@std/cli": "jsr:@std/cli@^1.0.6",
"@std/dotenv": "jsr:@std/dotenv@^0.225.2",
"@std/fmt": "jsr:@std/fmt@^1.0.2",
Expand Down
18 changes: 6 additions & 12 deletions src/base_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import type {
} from "./types_internal.ts";
import type { ClientConfig } from "./types_external.ts";
import { buildRequest } from "./base_client_helper.ts";
import { parseError } from "./errors.ts";
import { validateRequestData } from "./validate.ts";
import { fetchRetry } from "./fetch.ts";

Expand Down Expand Up @@ -37,16 +36,11 @@ export const baseClient = (cfg: ClientConfig): BaseClient =>
// Build the request
const request = buildRequest(cfg, requestData);

try {
// Make the request with retry logic
const response = await fetchRetry<TOk, TErr>(
request,
cfg.retryRequests,
);
return response;
} catch (error: unknown) {
// Parse and return the error
return parseError<TErr>(error);
}
// Make the request with retry logic
const response = await fetchRetry<TOk, TErr>(
request,
cfg.retryRequests,
);
return response;
},
}) as const;
4 changes: 0 additions & 4 deletions src/deps.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
export {
retry,
RetryError,
} from "https://deno.land/std@0.224.0/async/retry.ts";
export { filterKeys } from "https://deno.land/std@0.224.0/collections/mod.ts";
/**
* This is a workaround for `crypto.randomUUID` not being available in
Expand Down
12 changes: 0 additions & 12 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { RetryError } from "./deps.ts";
import type { SDKError } from "./types_external.ts";

/**
Expand All @@ -20,17 +19,6 @@ export const parseError = <TErr>(
error: unknown,
status?: number,
): SDKError<TErr> => {
// Handle RetryError
if (error instanceof RetryError) {
return {
ok: false,
error: {
message:
"Retry limit reached. Could not get a response from the server",
},
};
}

// Handle connection errors
if (
error instanceof TypeError &&
Expand Down
37 changes: 22 additions & 15 deletions src/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { retry } from "./deps.ts";
import { parseError } from "./errors.ts";
import {
isServerErrorStatus,
Expand All @@ -18,21 +17,28 @@ export const fetchRetry = async <TOk, TErr>(
request: Request,
retryRequest: boolean = true,
): Promise<ClientResponse<TOk, TErr>> => {
// Execute request without retry
if (!retryRequest) {
return await fetchJSON<TOk, TErr>(request);
// Delays between retries in milliseconds, if retryRequest is true.
// If retryRequest is false, the array will be empty.
const delays = retryRequest ? [1000, 3000] : [];

let attempt = 0;
while (true) {
try {
return await fetchJSON<TOk, TErr>(request);
} catch (_error) {
if (attempt === delays.length) {
return {
ok: false,
error: {
message:
`Retry limit reached. Could not get a response from the server after ${attempt} attempts`,
},
};
}
await new Promise((r) => setTimeout(r, delays[attempt]));
attempt++;
}
}
// Execute request using retry
const req = retry(async () => {
return await fetchJSON<TOk, TErr>(request);
}, {
multiplier: 2,
maxTimeout: 3000,
maxAttempts: 3,
minTimeout: 1000,
jitter: 0,
});
return req;
};

/**
Expand All @@ -42,6 +48,7 @@ export const fetchRetry = async <TOk, TErr>(
* @template TErr - The type of the error response data.
* @param {Request} request - The request to fetch JSON data from.
* @returns {Promise<ClientResponse<TOk, TErr>>} A ClientResponse object containing the fetched data.
* @throws {Error} Throws an error if the response status is a server error.
*/
export const fetchJSON = async <TOk, TErr>(
request: Request,
Expand Down
17 changes: 5 additions & 12 deletions src/types_external.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
/**
* Utility Types
*/
type PrettifyType<T> = { [K in keyof T]: T[K] } & unknown;

type MakePropertyOptional<T, K extends keyof T> =
& Omit<T, K>
& { [P in K]?: T[P] };

type MakeNestedPropertyOptional<T, K extends keyof T, N extends keyof T[K]> = {
[P in keyof T]: P extends K ? Omit<T[K], N> & Partial<Pick<T[K], N>> : T[P];
};
import {
MakeNestedPropertyOptional,
MakePropertyOptional,
PrettifyType,
} from "./types_internal.ts";

/**
* Access Token API
Expand Down
67 changes: 67 additions & 0 deletions src/types_internal.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,43 @@
import { SDKError } from "./types_external.ts";

/**
* Represents a response from the client.
*
* @template TOk - The type of the successful response data.
* @template TErr - The type of the error details.
*/
export type ClientResponse<TOk, TErr> =
| {
ok: true;
data: TOk;
}
| SDKError<TErr>;

/**
* Represents the base client with a method to make requests.
*/
export type BaseClient = {
readonly makeRequest: (
requestData: RequestData<unknown, unknown>,
) => Promise<ClientResponse<unknown, unknown>>;
};

/**
* Represents a factory for creating request data.
*/
export type RequestFactory = {
// deno-lint-ignore no-explicit-any
[key: string]: (...args: any[]) => RequestData<unknown, unknown>;
};

/**
* Represents a proxy for API requests.
*
* This type transforms a `RequestFactory` type into a type where each method
* returns a `Promise` that resolves to a `ClientResponse`.
*
* @template TFac - The type of the request factory.
*/
export type ApiProxy<TFac extends RequestFactory> = {
[key in keyof TFac]: TFac[key] extends (
...args: infer TArgs
Expand All @@ -26,6 +46,12 @@ export type ApiProxy<TFac extends RequestFactory> = {
: never;
};

/**
* Represents the data required to make a request.
*
* @template TOk - The type of the successful response data.
* @template TErr - The type of the error details.
*/
export type RequestData<TOk, TErr> = {
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
url: string;
Expand All @@ -35,6 +61,9 @@ export type RequestData<TOk, TErr> = {
token?: string;
};

/**
* Represents the default headers used in requests.
*/
export type DefaultHeaders = {
"Content-Type": "application/json";
"Authorization": string;
Expand All @@ -48,4 +77,42 @@ export type DefaultHeaders = {
"Idempotency-Key": string;
};

/**
* Represents an array of keys from the DefaultHeaders type that should be omitted.
*
* @example
* const headersToOmit: OmitHeaders = ["Authorization", "Content-Type"];
*/
export type OmitHeaders = (keyof DefaultHeaders)[];

/**
* A utility type that makes the type `T` more readable by flattening its structure.
*
* @template T - The type to prettify.
*/
export type PrettifyType<T> = { [K in keyof T]: T[K] } & unknown;

/**
* A utility type that makes the specified property `K` of type `T` optional.
*
* @template T - The type containing the property to make optional.
* @template K - The key of the property to make optional.
*/
export type MakePropertyOptional<T, K extends keyof T> =
& Omit<T, K>
& { [P in K]?: T[P] };

/**
* A utility type that makes the specified nested property `N` of type `T[K]` optional.
*
* @template T - The type containing the nested property to make optional.
* @template K - The key of the property containing the nested property.
* @template N - The key of the nested property to make optional.
*/
export type MakeNestedPropertyOptional<
T,
K extends keyof T,
N extends keyof T[K],
> = {
[P in keyof T]: P extends K ? Omit<T[K], N> & Partial<Pick<T[K], N>> : T[P];
};
2 changes: 1 addition & 1 deletion tests/api_proxy_test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { proxifyFactory } from "../src/api_proxy.ts";
import { baseClient } from "../src/base_client.ts";
import { assertEquals } from "./test_deps.ts";
import { assertEquals } from "@std/assert";
import type { RequestData } from "../src/types_internal.ts";

Deno.test("proxifyFactory - Should return a Proxy object with method", () => {
Expand Down
2 changes: 1 addition & 1 deletion tests/auth_test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { assertEquals } from "./test_deps.ts";
import { assertEquals } from "@std/assert";
import { authRequestFactory } from "../src/apis/auth.ts";

Deno.test("getToken - Should have correct url and header", () => {
Expand Down
2 changes: 1 addition & 1 deletion tests/base_client_helper_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
import { uuid } from "../src/deps.ts";
import type { ClientConfig } from "../src/types_external.ts";
import type { RequestData } from "../src/types_internal.ts";
import { assert, assertEquals } from "./test_deps.ts";
import { assert, assertEquals } from "@std/assert";

Deno.test("buildRequest - Should return a Request object with the correct properties", () => {
const cfg: ClientConfig = {
Expand Down
58 changes: 31 additions & 27 deletions tests/base_client_test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { baseClient } from "../src/base_client.ts";
import { assertEquals, mf } from "./test_deps.ts";
import { assertEquals } from "@std/assert";
import * as mf from "@hongminhee/deno-mock-fetch";
import type { RequestData } from "../src/types_internal.ts";
import { RetryError } from "../src/deps.ts";

Deno.test("makeRequest - Should return ok", async () => {
mf.install(); // mock out calls to `fetch`
Expand All @@ -26,6 +26,33 @@ Deno.test("makeRequest - Should return ok", async () => {
assertEquals(response.ok, true);
});

Deno.test("makeRequest - Should return ok with retrires", async () => {
mf.install(); // mock out calls to `fetch`

mf.mock("GET@/foo", (req: Request) => {
assertEquals(req.url, "https://api.vipps.no/foo");
assertEquals(req.method, "GET");
return new Response(JSON.stringify({}), {
status: 200,
});
});

const cfg = {
merchantSerialNumber: "",
subscriptionKey: "",
retryRequests: true,
};
const requestData: RequestData<unknown, unknown> = {
method: "GET",
url: "/foo",
};

const client = baseClient(cfg);
const response = await client.makeRequest(requestData);

assertEquals(response.ok, true);
});

Deno.test("makeRequest - Should error", async () => {
mf.install(); // mock out calls to `fetch`

Expand Down Expand Up @@ -109,12 +136,12 @@ Deno.test("makeRequest - Should return ok after 2 retries", async () => {
mf.reset();
});

Deno.test("makeRequest - Should not return ok after 4 retries", async () => {
Deno.test("makeRequest - Should not return ok after 3 retries", async () => {
mf.install(); // mock out calls to `fetch`
let count = 0;
mf.mock("GET@/foo", () => {
count++;
if (count < 5) {
if (count < 4) {
return new Response(
JSON.stringify({ ok: false, error: "Internal Server Error" }),
{
Expand Down Expand Up @@ -144,26 +171,3 @@ Deno.test("makeRequest - Should not return ok after 4 retries", async () => {

mf.reset();
});

Deno.test("makeRequest - Should catch Retry Errors", async () => {
mf.install(); // mock out calls to `fetch`
mf.mock("GET@/foo", () => {
throw new RetryError({ foo: "bar" }, 3);
});

const cfg = {
merchantSerialNumber: "",
subscriptionKey: "",
retryRequests: true,
};
const requestData: RequestData<unknown, unknown> = {
method: "GET",
url: "/foo",
};

const client = baseClient(cfg);

const response = await client.makeRequest(requestData);
assertEquals(response.ok, false);
mf.reset();
});
2 changes: 1 addition & 1 deletion tests/checkout_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
assertEquals,
assertExists,
assertNotEquals,
} from "./test_deps.ts";
} from "@std/assert";

Deno.test("create - should return the correct request data", () => {
const client_id = "your_client_id";
Expand Down
2 changes: 1 addition & 1 deletion tests/epayment_test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { assert, assertEquals, assertExists } from "./test_deps.ts";
import { assert, assertEquals, assertExists } from "@std/assert";
import { ePaymentRequestFactory } from "../src/apis/epayment.ts";
import { uuid } from "../src/deps.ts";
import { CreatePaymentRequest } from "../src/types_external.ts";
Expand Down
5 changes: 3 additions & 2 deletions tests/error_test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { AccessTokenError } from "../src/types_external.ts";
import { parseError } from "../src/errors.ts";
import { Client } from "../src/mod.ts";
import { assert, assertExists } from "./test_deps.ts";
import { assertEquals, mf } from "./test_deps.ts";
import { assert, assertExists } from "@std/assert";
import { assertEquals } from "@std/assert";
import * as mf from "@hongminhee/deno-mock-fetch";

Deno.test("parseError - Should return correct error message for connection error", () => {
const error = new TypeError("error trying to connect");
Expand Down
Loading