Skip to content

Commit

Permalink
Update user agent (#83)
Browse files Browse the repository at this point in the history
* Update base-client.ts

---------

Co-authored-by: Fil Maj <[email protected]>
  • Loading branch information
WilliamBergamin and Fil Maj authored Sep 1, 2023
1 parent d857639 commit 368e406
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 56 deletions.
39 changes: 3 additions & 36 deletions src/api_test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {
assertEquals,
assertExists,
assertInstanceOf,
assertRejects,
isHttpError,
mf,
} from "./dev_deps.ts";
import { SlackAPI } from "./mod.ts";
import { serializeData } from "./base-client.ts";
import { HttpError } from "./deps.ts";

Deno.test("SlackAPI class", async (t) => {
Expand All @@ -27,6 +27,7 @@ Deno.test("SlackAPI class", async (t) => {
await t.step("should call the default API URL", async () => {
mf.mock("POST@/api/chat.postMessage", (req: Request) => {
assertEquals(req.url, "https://slack.com/api/chat.postMessage");
assertExists(req.headers.has("user-agent"));
return new Response('{"ok":true}');
});

Expand All @@ -40,6 +41,7 @@ Deno.test("SlackAPI class", async (t) => {
async () => {
mf.mock("POST@/api/chat.postMessage", (req: Request) => {
assertEquals(req.headers.get("authorization"), "Bearer override");
assertExists(req.headers.has("user-agent"));
return new Response('{"ok":true}');
});

Expand Down Expand Up @@ -366,41 +368,6 @@ Deno.test("SlackAPI class", async (t) => {
mf.uninstall();
});

Deno.test("serializeData helper function", async (t) => {
await t.step(
"should serialize string values as strings and return a URLSearchParams object",
() => {
assertEquals(
serializeData({ "batman": "robin" }).toString(),
"batman=robin",
);
},
);
await t.step(
"should serialize non-string values as JSON-encoded strings and return a URLSearchParams object",
() => {
assertEquals(
serializeData({ "hockey": { "good": true, "awesome": "yes" } })
.toString(),
"hockey=%7B%22good%22%3Atrue%2C%22awesome%22%3A%22yes%22%7D",
);
},
);
await t.step(
"should not serialize undefined values",
() => {
assertEquals(
serializeData({
"hockey": { "good": true, "awesome": "yes" },
"baseball": undefined,
})
.toString(),
"hockey=%7B%22good%22%3Atrue%2C%22awesome%22%3A%22yes%22%7D",
);
},
);
});

Deno.test("SlackApi.setSlackApiUrl()", async (t) => {
mf.install();
const testClient = SlackAPI("test-token");
Expand Down
46 changes: 46 additions & 0 deletions src/base-client-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const API_VERSION_REGEX = /\/deno_slack_api@(.*)\//;

export function getUserAgent() {
const userAgents = [];
userAgents.push(`Deno/${Deno.version.deno}`);
userAgents.push(`OS/${Deno.build.os}`);
userAgents.push(
`deno-slack-api/${_internals.getModuleVersion()}`,
);
return userAgents.join(" ");
}

function getModuleVersion(): string | undefined {
const url = _internals.getModuleUrl();
// Insure this module is sourced from https://deno.land/x/deno_slack_api
if (url.host === "deno.land") {
return url.pathname.match(API_VERSION_REGEX)?.at(1);
}
return undefined;
}

function getModuleUrl(): URL {
return new URL(import.meta.url);
}

// Serialize an object into a string so as to be compatible with x-www-form-urlencoded payloads
export function serializeData(data: Record<string, unknown>): URLSearchParams {
const encodedData: Record<string, string> = {};
Object.entries(data).forEach(([key, value]) => {
// Objects/arrays, numbers and booleans get stringified
// Slack API accepts JSON-stringified-and-url-encoded payloads for objects/arrays
// Inspired by https://github.com/slackapi/node-slack-sdk/blob/%40slack/web-api%406.7.2/packages/web-api/src/WebClient.ts#L452-L528

// Skip properties with undefined values.
if (value === undefined) return;

const serializedValue: string = typeof value !== "string"
? JSON.stringify(value)
: value;
encodedData[key] = serializedValue;
});

return new URLSearchParams(encodedData);
}

export const _internals = { getModuleVersion, getModuleUrl };
144 changes: 144 additions & 0 deletions src/base-client-helpers_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { assertEquals } from "https://deno.land/[email protected]/testing/asserts.ts";
import {
_internals,
getUserAgent,
serializeData,
} from "./base-client-helpers.ts";
import { assertSpyCalls, stub } from "./dev_deps.ts";

Deno.test(`base-client-helpers.${_internals.getModuleVersion.name}`, async (t) => {
await t.step(
"should return the version if the module is sourced from deno.land",
() => {
const getModuleUrlStub = stub(_internals, "getModuleUrl", () => {
return new URL("https://deno.land/x/[email protected]/mod.ts)");
});

try {
const moduleVersion = _internals.getModuleVersion();

assertSpyCalls(getModuleUrlStub, 1);
assertEquals(moduleVersion, "2.1.0");
} finally {
getModuleUrlStub.restore();
}
},
);

await t.step(
"should return undefined if the module is not sourced from deno.land",
() => {
const getModuleUrlStub = stub(_internals, "getModuleUrl", () => {
return new URL("file:///hello/world.ts)");
});
try {
const moduleVersion = _internals.getModuleVersion();

assertSpyCalls(getModuleUrlStub, 1);
assertEquals(moduleVersion, undefined);
} finally {
getModuleUrlStub.restore();
}
},
);

await t.step(
"should return undefined if the regex used to parse [email protected] fails",
() => {
const getModuleUrlStub = stub(_internals, "getModuleUrl", () => {
return new URL("https://deno.land/x/[email protected]/mod.ts)");
});
try {
const moduleVersion = _internals.getModuleVersion();

assertSpyCalls(getModuleUrlStub, 1);
assertEquals(moduleVersion, undefined);
} finally {
getModuleUrlStub.restore();
}
},
);
});

Deno.test(`base-client-helpers.${getUserAgent.name}`, async (t) => {
await t.step(
"should return the user agent with deno version, OS name and undefined deno-slack-api version",
() => {
const expectedVersion = undefined;
const getModuleUrlStub = stub(_internals, "getModuleVersion", () => {
return expectedVersion;
});

try {
const userAgent = getUserAgent();

assertSpyCalls(getModuleUrlStub, 1);
assertEquals(
userAgent,
`Deno/${Deno.version.deno} OS/${Deno.build.os} deno-slack-api/undefined`,
);
} finally {
getModuleUrlStub.restore();
}
},
);

await t.step(
"should return the user agent with deno version, OS name and deno-slack-api version",
() => {
const expectedVersion = "2.1.0";
const getModuleUrlStub = stub(_internals, "getModuleUrl", () => {
return new URL(
`https://deno.land/x/deno_slack_api@${expectedVersion}/mod.ts)`,
);
});

try {
const userAgent = getUserAgent();

assertSpyCalls(getModuleUrlStub, 1);
assertEquals(
userAgent,
`Deno/${Deno.version.deno} OS/${Deno.build.os} deno-slack-api/${expectedVersion}`,
);
} finally {
getModuleUrlStub.restore();
}
},
);
});

Deno.test(`${serializeData.name} helper function`, async (t) => {
await t.step(
"should serialize string values as strings and return a URLSearchParams object",
() => {
assertEquals(
serializeData({ "batman": "robin" }).toString(),
"batman=robin",
);
},
);
await t.step(
"should serialize non-string values as JSON-encoded strings and return a URLSearchParams object",
() => {
assertEquals(
serializeData({ "hockey": { "good": true, "awesome": "yes" } })
.toString(),
"hockey=%7B%22good%22%3Atrue%2C%22awesome%22%3A%22yes%22%7D",
);
},
);
await t.step(
"should not serialize undefined values",
() => {
assertEquals(
serializeData({
"hockey": { "good": true, "awesome": "yes" },
"baseball": undefined,
})
.toString(),
"hockey=%7B%22good%22%3Atrue%2C%22awesome%22%3A%22yes%22%7D",
);
},
);
});
23 changes: 3 additions & 20 deletions src/base-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
SlackAPIOptions,
} from "./types.ts";
import { createHttpError, HttpError } from "./deps.ts";
import { getUserAgent, serializeData } from "./base-client-helpers.ts";

export class BaseSlackAPIClient implements BaseSlackClient {
#token?: string;
Expand Down Expand Up @@ -42,6 +43,7 @@ export class BaseSlackAPIClient implements BaseSlackClient {
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": getUserAgent(),
},
body,
});
Expand All @@ -60,6 +62,7 @@ export class BaseSlackAPIClient implements BaseSlackClient {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": getUserAgent(),
},
body: JSON.stringify(data),
});
Expand Down Expand Up @@ -87,23 +90,3 @@ export class BaseSlackAPIClient implements BaseSlackClient {
};
}
}

// Serialize an object into a string so as to be compatible with x-www-form-urlencoded payloads
export function serializeData(data: Record<string, unknown>): URLSearchParams {
const encodedData: Record<string, string> = {};
Object.entries(data).forEach(([key, value]) => {
// Objects/arrays, numbers and booleans get stringified
// Slack API accepts JSON-stringified-and-url-encoded payloads for objects/arrays
// Inspired by https://github.com/slackapi/node-slack-sdk/blob/%40slack/web-api%406.7.2/packages/web-api/src/WebClient.ts#L452-L528

// Skip properties with undefined values.
if (value === undefined) return;

const serializedValue: string = typeof value !== "string"
? JSON.stringify(value)
: value;
encodedData[key] = serializedValue;
});

return new URLSearchParams(encodedData);
}
4 changes: 4 additions & 0 deletions src/dev_deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ export {
afterEach,
beforeAll,
} from "https://deno.land/[email protected]/testing/bdd.ts";
export {
assertSpyCalls,
stub,
} from "https://deno.land/[email protected]/testing/mock.ts";

0 comments on commit 368e406

Please sign in to comment.