Skip to content

Commit

Permalink
Merge pull request #1 from ensembleblock/ericcarraway/2024-05-07-vitest
Browse files Browse the repository at this point in the history
initial unit tests
#1
  • Loading branch information
ericcarraway authored May 8, 2024
2 parents 25ffaf0 + 44ec8c7 commit 3e63db5
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 18 deletions.
8 changes: 8 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ module.exports = {
`plugin:typescript-sort-keys/recommended`,
`@percuss.io/eslint-config-ericcarraway`,
],
overrides: [
{
files: [`tests.spec.ts`],
rules: {
'import/no-extraneous-dependencies': `off`,
},
},
],
parser: `@typescript-eslint/parser`,
parserOptions: {
ecmaVersion: 2018,
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
# airtable
# @ensembleblock/airtable

Lightweight Airtable API client powered by fetch.
85 changes: 84 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,12 @@
"prepublishOnly": "npm run test",
"pretest": "npm run build",
"release": "np",
"test": "npm run lint && npm run typecheck",
"test": "npm run lint && npm run typecheck && vitest",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@percuss.io/eslint-config-ericcarraway": "^3.0.0",
"@types/node": "^20.12.11",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint": "^8.57.0",
Expand All @@ -55,6 +56,7 @@
"prettier": "^3.2.5",
"rimraf": "^5.0.5",
"typescript": "~5.3",
"vitest": "^1.6.0"
"vitest": "^1.6.0",
"vitest-fetch-mock": "^0.2.2"
}
}
18 changes: 4 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
/* eslint no-underscore-dangle: ["error", { "allow": ["_fetch"] }] */

export type AirtableClientOpts = {
_fetch?: typeof fetch;

/** A string of at least 10 characters. */
apiKey: string;

Expand Down Expand Up @@ -57,15 +53,13 @@ export type UpdateRecordOpts = {
};

export class AirtableClient {
private _fetch: typeof fetch = fetch;

private baseId: string | null = null;

private baseUrl: string = `https://api.airtable.com/v0`;

private headers: Record<string, string> = {};

constructor({ _fetch, apiKey, baseId, baseUrl }: AirtableClientOpts) {
constructor({ apiKey, baseId, baseUrl }: AirtableClientOpts) {
if (typeof apiKey !== `string` || apiKey.length < 10) {
throw new TypeError(
`AirtableClient expected 'apiKey' to be string of at least 10 characters`,
Expand All @@ -92,10 +86,6 @@ export class AirtableClient {
if (baseUrl && typeof baseUrl === `string`) {
this.baseUrl = baseUrl;
}

if (typeof _fetch === `function`) {
this._fetch = _fetch;
}
}

/**
Expand All @@ -121,7 +111,7 @@ export class AirtableClient {
const createRecordUrl = `${this.baseUrl}/${this.baseId}/${tableIdOrName}`;
const body = JSON.stringify({ fields });

const res: Response = await this._fetch(createRecordUrl, {
const res: Response = await fetch(createRecordUrl, {
body,
headers: this.headers,
method: `POST`,
Expand Down Expand Up @@ -159,7 +149,7 @@ export class AirtableClient {

const getRecordUrl = `${this.baseUrl}/${this.baseId}/${tableIdOrName}/${recordId}`;

const res = await this._fetch(getRecordUrl, {
const res = await fetch(getRecordUrl, {
headers: this.headers,
method: `GET`,
});
Expand Down Expand Up @@ -213,7 +203,7 @@ export class AirtableClient {
const updateRecordUrl = `${this.baseUrl}/${this.baseId}/${tableIdOrName}/${recordId}`;
const body = JSON.stringify({ fields });

const res = await this._fetch(updateRecordUrl, {
const res = await fetch(updateRecordUrl, {
body,
headers: this.headers,
method: method.toUpperCase(),
Expand Down
86 changes: 86 additions & 0 deletions tests.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import createFetchMock from 'vitest-fetch-mock';

import { AirtableClient } from './dist';

const fetchMock = createFetchMock(vi);

// Set `globalThis.fetch` to our mocked version.
fetchMock.enableMocks();

const apiKey = `pat_mock_123456789`;
const baseId = `app_mock_123456789`;
const recordId = `rec_mock_123456789`;
const tableIdOrName = `table_mock`;

const getClient = () => new AirtableClient({ apiKey, baseId });

const parseBody = (body) =>
body ? JSON.parse(Buffer.from(body).toString()) : null;

describe(`AirtableClient`, () => {
beforeEach(() => {
fetchMock.resetMocks();
});

it(`createRecord`, async () => {
fetchMock.mockResponseOnce(JSON.stringify({ createRecordResponse: true }));

const client: AirtableClient = getClient();
const res = await client.createRecord({
fields: { foo: `bar` },
tableIdOrName,
});

expect(fetchMock.requests().length).toEqual(1);

const { body, method, url } = fetchMock.requests()[0];

expect(method).toEqual(`POST`);
expect(url).toEqual(
`https://api.airtable.com/v0/app_mock_123456789/table_mock`,
);
expect(parseBody(body)).toEqual({ fields: { foo: `bar` } });
expect(res.data).toEqual({ createRecordResponse: true });
});

it(`getRecord`, async () => {
fetchMock.mockResponseOnce(JSON.stringify({ getRecordResponse: true }));

const client: AirtableClient = getClient();
const res = await client.getRecord({ recordId, tableIdOrName });

expect(fetchMock.requests().length).toEqual(1);

const { body, method, url } = fetchMock.requests()[0];

expect(method).toEqual(`GET`);
expect(url).toEqual(
`https://api.airtable.com/v0/app_mock_123456789/table_mock/rec_mock_123456789`,
);
expect(parseBody(body)).toEqual(null);
expect(res.data).toEqual({ getRecordResponse: true });
});

it(`updateRecord`, async () => {
fetchMock.mockResponseOnce(JSON.stringify({ updateRecordResponse: true }));

const client: AirtableClient = getClient();
const res = await client.updateRecord({
fields: { foo: `bar` },
recordId,
tableIdOrName,
});

expect(fetchMock.requests().length).toEqual(1);

const { body, method, url } = fetchMock.requests()[0];

expect(method).toEqual(`PATCH`);
expect(url).toEqual(
`https://api.airtable.com/v0/app_mock_123456789/table_mock/rec_mock_123456789`,
);
expect(parseBody(body)).toEqual({ fields: { foo: `bar` } });
expect(res.data).toEqual({ updateRecordResponse: true });
});
});

0 comments on commit 3e63db5

Please sign in to comment.