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

Update user agent #83

Merged
merged 13 commits into from
Sep 1, 2023
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) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests where moves to base-client-helpers_test.ts

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 {
filmaj marked this conversation as resolved.
Show resolved Hide resolved
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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function was moved to base-client-helpers.ts

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";