Skip to content

Commit

Permalink
Upgrade test suite jsr (#63)
Browse files Browse the repository at this point in the history
* Add docs

* Move tests to jsr

* Remove dependency on std/async
  • Loading branch information
tomas-zijdemans-vipps authored Sep 22, 2024
1 parent 55eb070 commit f336100
Show file tree
Hide file tree
Showing 20 changed files with 289 additions and 102 deletions.
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

0 comments on commit f336100

Please sign in to comment.