From ded1e3c0791c20229dfa341938f3375904a2eefe Mon Sep 17 00:00:00 2001 From: null8626 Date: Thu, 27 Feb 2025 20:37:30 +0700 Subject: [PATCH 01/16] feat: deprecate features that are not available in v0 --- src/structs/Api.ts | 48 +++++++++++------------ src/typings.ts | 96 ++++++++++++++++++++++++++++++++++------------ 2 files changed, 94 insertions(+), 50 deletions(-) diff --git a/src/structs/Api.ts b/src/structs/Api.ts index 90f7ba5..3a77457 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -30,6 +30,7 @@ import { */ export class Api extends EventEmitter { private options: APIOptions; + private id: Snowflake; /** * Create Top.gg API instance * @@ -39,9 +40,10 @@ export class Api extends EventEmitter { constructor(token: string, options: APIOptions = {}) { super(); this.options = { - token: token, + token, ...options, }; + this.id = JSON.parse(atob(token.split('.')[1])).id; } private async _request( @@ -91,25 +93,20 @@ export class Api extends EventEmitter { * @example * ```js * await api.postStats({ - * serverCount: 28199, - * shardCount: 1, + * serverCount: 28199 * }); * ``` * * @param {object} stats Stats object * @param {number} stats.serverCount Server count - * @param {number} [stats.shardCount] Shard count - * @param {number} [stats.shardId] Posting shard (useful for process sharding) * @returns {BotStats} Passed object */ public async postStats(stats: BotStats): Promise { if (!stats?.serverCount) throw new Error("Missing Server Count"); /* eslint-disable camelcase */ - await this._request("POST", "/bots/stats", { - server_count: stats.serverCount, - shard_id: stats.shardId, - shard_count: stats.shardCount, + await this._request("POST", `/bots/${this.id}/stats`, { + server_count: stats.serverCount }); /* eslint-enable camelcase */ @@ -117,29 +114,28 @@ export class Api extends EventEmitter { } /** - * Get a bots stats + * Get your bot's stats * * @example * ```js - * await api.getStats("461521980492087297"); + * await api.getStats(); * // => * { * serverCount: 28199, - * shardCount 1, + * shardCount: null, * shards: [] * } * ``` * - * @param {Snowflake} id Bot ID - * @returns {BotStats} Stats of bot requested + * @returns {BotStats} Your bot's stats */ - public async getStats(id: Snowflake): Promise { - if (!id) throw new Error("ID missing"); - const result = await this._request("GET", `/bots/${id}/stats`); + public async getStats(_id: Snowflake): Promise { + if (_id) console.warn('[DeprecationWaring] getStats() no longer needs an ID argument') + const result = await this._request("GET", `/bots/${this.id}/stats`); return { serverCount: result.server_count, - shardCount: result.shard_count, - shards: result.shards, + shardCount: null, + shards: [] }; } @@ -160,6 +156,8 @@ export class Api extends EventEmitter { } /** + * @deprecated No longer supported by Top.gg API v0. + * * Get user info * * @example @@ -173,6 +171,8 @@ export class Api extends EventEmitter { * @returns {UserInfo} Info for user */ public async getUser(id: Snowflake): Promise { + console.warn('[DeprecationWarning] getUser is no longer supported by Top.gg API v0.') + if (!id) throw new Error("ID Missing"); return this._request("GET", `/users/${id}`); } @@ -185,8 +185,7 @@ export class Api extends EventEmitter { * // Finding by properties * await api.getBots({ * search: { - * username: "shiro", - * certifiedBot: true, + * username: "shiro" * }, * }); * // => @@ -195,8 +194,6 @@ export class Api extends EventEmitter { * { * id: '461521980492087297', * username: 'Shiro', - * discriminator: '8764', - * lib: 'discord.js', * ...rest of bot object * } * ...other shiro knockoffs B) @@ -267,8 +264,7 @@ export class Api extends EventEmitter { * @returns {ShortUser[]} Array of users who've voted */ public async getVotes(): Promise { - if (!this.options.token) throw new Error("Missing token"); - return this._request("GET", "/bots/votes"); + return this._request("GET", `/bots/${this.id}/votes`); } /** @@ -285,7 +281,7 @@ export class Api extends EventEmitter { */ public async hasVoted(id: Snowflake): Promise { if (!id) throw new Error("Missing ID"); - return this._request("GET", "/bots/check", { userId: id }).then( + return this._request("GET", `/bots/${this.id}/check`, { userId: id }).then( (x) => !!x.voted ); } diff --git a/src/typings.ts b/src/typings.ts index d0c8eca..0e8b715 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -7,22 +7,36 @@ export interface APIOptions { export type Snowflake = string; export interface BotInfo { - /** The id of the bot */ + /** The Top.gg ID of the bot */ id: Snowflake; + /** The Discord ID of the bot */ + clientid: Snowflake; /** The username of the bot */ username: string; - /** The discriminator of the bot */ + /** + * The discriminator of the bot + * + * @deprecated No longer supported by Top.gg API v0. + */ discriminator: string; - /** The avatar hash of the bot's avatar */ - avatar?: string; - /** The cdn hash of the bot's avatar if the bot has none */ + /** The bot's avatar */ + avatar: string; + /** + * The cdn hash of the bot's avatar if the bot has none + * + * @deprecated No longer supported by Top.gg API v0. + */ defAvatar: string; - /** The URL for the banner image */ + /** + * The URL for the banner image + * + * @deprecated No longer supported by Top.gg API v0. + */ bannerUrl?: string; /** * The library of the bot * - * @deprecated + * @deprecated No longer supported by Top.gg API v0. */ lib: string; /** The prefix of the bot */ @@ -30,33 +44,54 @@ export interface BotInfo { /** The short description of the bot */ shortdesc: string; /** The long description of the bot. Can contain HTML and/or Markdown */ - longdesc: string; + longdesc?: string; /** The tags of the bot */ tags: string[]; /** The website url of the bot */ website?: string; - /** The support server invite code of the bot */ + /** The support url of the bot */ support?: string; /** The link to the github repo of the bot */ github?: string; /** The owners of the bot. First one in the array is the main owner */ owners: Snowflake[]; - /** The guilds featured on the bot page */ + /** + * The guilds featured on the bot page + * + * @deprecated No longer supported by Top.gg API v0. + */ guilds: Snowflake[]; /** The custom bot invite url of the bot */ invite?: string; - /** The date when the bot was approved (in ISO 8601) */ + /** The date when the bot was submitted (in ISO 8601) */ date: string; - /** The certified status of the bot */ + /** + * The certified status of the bot + * + * @deprecated No longer supported by Top.gg API v0. + */ certifiedBot: boolean; /** The vanity url of the bot */ vanity?: string; - /** The amount of upvotes the bot has */ + /** The amount of votes the bot has */ points: number; - /** The amount of upvotes the bot has this month */ + /** The amount of votes the bot has this month */ monthlyPoints: number; - /** The guild id for the donatebot setup */ + /** + * The guild id for the donatebot setup + * + * @deprecated No longer supported by Top.gg API v0. + */ donatebotguildid: Snowflake; + /** The amount of servers the bot is in based on posted stats */ + server_count?: number; + /** The bot's reviews on Top.gg */ + reviews: { + /** This bot's average review score out of 5 */ + averageScore: number; + /** This bot's review count */ + count: number; + } } export interface BotStats { @@ -65,14 +100,27 @@ export interface BotStats { /** * The amount of servers the bot is in per shard. Always present but can be * empty. (Only when receiving stats) + * + * @deprecated No longer supported by Top.gg API v0. */ shards?: number[]; - /** The shard ID to post as (only when posting) */ + /** + * The shard ID to post as (only when posting) + * + * @deprecated No longer supported by Top.gg API v0. + */ shardId?: number; - /** The amount of shards a bot has */ - shardCount?: number; + /** + * The amount of shards a bot has + * + * @deprecated No longer supported by Top.gg API v0. + */ + shardCount?: number | null; } +/** + * @deprecated No longer supported by Top.gg API v0. + */ export interface UserInfo { /** The id of the user */ id: Snowflake; @@ -80,8 +128,8 @@ export interface UserInfo { username: string; /** The discriminator of the user */ discriminator: string; - /** The avatar hash of the user's avatar */ - avatar?: string; + /** The user's avatar url */ + avatar: string; /** The cdn hash of the user's avatar if the user has none */ defAvatar: string; /** The bio of the user */ @@ -126,8 +174,8 @@ export interface BotsQuery { [key in keyof BotInfo]: string; } | string; - /** The field to sort by. Prefix with "-"" to reverse the order */ - sort?: string; + /** Sorts results from a specific criteria. Results will always be descending. */ + sort?: 'monthlyPoints' | 'id' | 'date'; /** A list of fields to show. */ fields?: string[] | string; } @@ -153,10 +201,10 @@ export interface ShortUser { /** * User's discriminator * - * @deprecated + * @deprecated No longer supported by Top.gg API v0. */ discriminator: string; - /** User's avatar hash */ + /** User's avatar url */ avatar: string; } From bbb62a100df8eda4f2d6bcf3caf43bc784478642 Mon Sep 17 00:00:00 2001 From: null8626 Date: Thu, 6 Mar 2025 15:26:03 +0700 Subject: [PATCH 02/16] doc: improve documentation for getVotes --- src/structs/Api.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/structs/Api.ts b/src/structs/Api.ts index 3a77457..342404f 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -240,7 +240,7 @@ export class Api extends EventEmitter { } /** - * Get users who've voted + * Get recent unique users who've voted * * @example * ```js @@ -250,18 +250,18 @@ export class Api extends EventEmitter { * { * username: 'Xignotic', * id: '205680187394752512', - * avatar: '3b9335670c7213b3a2d4e990081900c7' + * avatar: 'https://cdn.discordapp.com/avatars/1026525568344264724/cd70e62e41f691f1c05c8455d8c31e23.png' * }, * { * username: 'iara', * id: '395526710101278721', - * avatar: '3d1477390b8d7c3cec717ac5c778f5f4' + * avatar: 'https://cdn.discordapp.com/avatars/1026525568344264724/cd70e62e41f691f1c05c8455d8c31e23.png' * } * ...more * ] * ``` * - * @returns {ShortUser[]} Array of users who've voted + * @returns {ShortUser[]} Array of unique users who've voted */ public async getVotes(): Promise { return this._request("GET", `/bots/${this.id}/votes`); From ce04186c5f73005ac8d82c7a47606c61294bb989 Mon Sep 17 00:00:00 2001 From: null8626 Date: Thu, 6 Mar 2025 15:33:10 +0700 Subject: [PATCH 03/16] feat: just straight up throw an error --- src/structs/Api.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/structs/Api.ts b/src/structs/Api.ts index 342404f..e454441 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -171,10 +171,7 @@ export class Api extends EventEmitter { * @returns {UserInfo} Info for user */ public async getUser(id: Snowflake): Promise { - console.warn('[DeprecationWarning] getUser is no longer supported by Top.gg API v0.') - - if (!id) throw new Error("ID Missing"); - return this._request("GET", `/users/${id}`); + throw new Error('[Deprecated] getUser is no longer supported by Top.gg API v0.'); } /** From 2e65b7ff73c61a7c32ff5673cfcd65015951ef03 Mon Sep 17 00:00:00 2001 From: null8626 Date: Fri, 7 Mar 2025 22:53:15 +0700 Subject: [PATCH 04/16] feat: add page to getVotes --- src/structs/Api.ts | 13 ++++++++----- src/typings.ts | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/structs/Api.ts b/src/structs/Api.ts index e454441..345866d 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -43,7 +43,7 @@ export class Api extends EventEmitter { token, ...options, }; - this.id = JSON.parse(atob(token.split('.')[1])).id; + this.id = JSON.parse(atob(token.split(".")[1])).id; } private async _request( @@ -130,7 +130,7 @@ export class Api extends EventEmitter { * @returns {BotStats} Your bot's stats */ public async getStats(_id: Snowflake): Promise { - if (_id) console.warn('[DeprecationWaring] getStats() no longer needs an ID argument') + if (_id) console.warn("[DeprecationWaring] getStats() no longer needs an ID argument"); const result = await this._request("GET", `/bots/${this.id}/stats`); return { serverCount: result.server_count, @@ -171,7 +171,9 @@ export class Api extends EventEmitter { * @returns {UserInfo} Info for user */ public async getUser(id: Snowflake): Promise { - throw new Error('[Deprecated] getUser is no longer supported by Top.gg API v0.'); + console.warn("[Deprecated] getUser is no longer supported by Top.gg API v0."); + + return this._request("GET", `/users/${id}`); } /** @@ -258,10 +260,11 @@ export class Api extends EventEmitter { * ] * ``` * + * @param {number} [page] The page number. Each page can only have at most 100 voters. * @returns {ShortUser[]} Array of unique users who've voted */ - public async getVotes(): Promise { - return this._request("GET", `/bots/${this.id}/votes`); + public async getVotes(page?: number): Promise { + return this._request("GET", `/bots/${this.id}/votes`, { page: page ?? 1 }); } /** diff --git a/src/typings.ts b/src/typings.ts index 0e8b715..b67aa9b 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -175,7 +175,7 @@ export interface BotsQuery { } | string; /** Sorts results from a specific criteria. Results will always be descending. */ - sort?: 'monthlyPoints' | 'id' | 'date'; + sort?: "monthlyPoints" | "id" | "date"; /** A list of fields to show. */ fields?: string[] | string; } From da94b32e36b984fc795438bef835365fced9299c Mon Sep 17 00:00:00 2001 From: null8626 Date: Sat, 8 Mar 2025 06:09:48 +0700 Subject: [PATCH 05/16] refactor: remove the need to destruct API token --- src/structs/Api.ts | 62 ++++++++++++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/src/structs/Api.ts b/src/structs/Api.ts index 345866d..43ad1da 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -30,7 +30,7 @@ import { */ export class Api extends EventEmitter { private options: APIOptions; - private id: Snowflake; + /** * Create Top.gg API instance * @@ -39,11 +39,25 @@ export class Api extends EventEmitter { */ constructor(token: string, options: APIOptions = {}) { super(); + + const tokenSegments = token.split("."); + + if (tokenSegments.length !== 3) { + throw new Error("Got a malformed API token."); + } + + const tokenData = atob(token[1]); + + try { + JSON.parse(tokenData).id; + } catch { + throw new Error("Invalid API token state, this should not happen! Please report!"); + } + this.options = { token, ...options, }; - this.id = JSON.parse(atob(token.split(".")[1])).id; } private async _request( @@ -105,7 +119,7 @@ export class Api extends EventEmitter { if (!stats?.serverCount) throw new Error("Missing Server Count"); /* eslint-disable camelcase */ - await this._request("POST", `/bots/${this.id}/stats`, { + await this._request("POST", "/bots/stats", { server_count: stats.serverCount }); /* eslint-enable camelcase */ @@ -114,7 +128,7 @@ export class Api extends EventEmitter { } /** - * Get your bot's stats + * Get your bot"s stats * * @example * ```js @@ -127,11 +141,11 @@ export class Api extends EventEmitter { * } * ``` * - * @returns {BotStats} Your bot's stats + * @returns {BotStats} Your bot"s stats */ public async getStats(_id: Snowflake): Promise { - if (_id) console.warn("[DeprecationWaring] getStats() no longer needs an ID argument"); - const result = await this._request("GET", `/bots/${this.id}/stats`); + if (_id) console.warn("[DeprecationWarning] getStats() no longer needs an ID argument"); + const result = await this._request("GET", "/bots/stats"); return { serverCount: result.server_count, shardCount: null, @@ -171,7 +185,7 @@ export class Api extends EventEmitter { * @returns {UserInfo} Info for user */ public async getUser(id: Snowflake): Promise { - console.warn("[Deprecated] getUser is no longer supported by Top.gg API v0."); + console.warn("[DeprecationWarning] getUser is no longer supported by Top.gg API v0."); return this._request("GET", `/users/${id}`); } @@ -191,8 +205,8 @@ export class Api extends EventEmitter { * { * results: [ * { - * id: '461521980492087297', - * username: 'Shiro', + * id: "461521980492087297", + * username: "Shiro", * ...rest of bot object * } * ...other shiro knockoffs B) @@ -210,12 +224,12 @@ export class Api extends EventEmitter { * { * results: [ * { - * id: '461521980492087297', - * username: 'Shiro' + * id: "461521980492087297", + * username: "Shiro" * }, * { - * id: '493716749342998541', - * username: 'Mimu' + * id: "493716749342998541", + * username: "Mimu" * }, * ... * ], @@ -239,7 +253,7 @@ export class Api extends EventEmitter { } /** - * Get recent unique users who've voted + * Get recent unique users who"ve voted * * @example * ```js @@ -247,24 +261,24 @@ export class Api extends EventEmitter { * // => * [ * { - * username: 'Xignotic', - * id: '205680187394752512', - * avatar: 'https://cdn.discordapp.com/avatars/1026525568344264724/cd70e62e41f691f1c05c8455d8c31e23.png' + * username: "Xignotic", + * id: "205680187394752512", + * avatar: "https://cdn.discordapp.com/avatars/1026525568344264724/cd70e62e41f691f1c05c8455d8c31e23.png" * }, * { - * username: 'iara', - * id: '395526710101278721', - * avatar: 'https://cdn.discordapp.com/avatars/1026525568344264724/cd70e62e41f691f1c05c8455d8c31e23.png' + * username: "iara", + * id: "395526710101278721", + * avatar: "https://cdn.discordapp.com/avatars/1026525568344264724/cd70e62e41f691f1c05c8455d8c31e23.png" * } * ...more * ] * ``` * * @param {number} [page] The page number. Each page can only have at most 100 voters. - * @returns {ShortUser[]} Array of unique users who've voted + * @returns {ShortUser[]} Array of unique users who"ve voted */ public async getVotes(page?: number): Promise { - return this._request("GET", `/bots/${this.id}/votes`, { page: page ?? 1 }); + return this._request("GET", "/bots/votes", { page: page ?? 1 }); } /** @@ -281,7 +295,7 @@ export class Api extends EventEmitter { */ public async hasVoted(id: Snowflake): Promise { if (!id) throw new Error("Missing ID"); - return this._request("GET", `/bots/${this.id}/check`, { userId: id }).then( + return this._request("GET", "/bots/check", { userId: id }).then( (x) => !!x.voted ); } From 40d6c1ec67118baaf36fdd1548fb54507568c119 Mon Sep 17 00:00:00 2001 From: null8626 Date: Sat, 8 Mar 2025 06:12:24 +0700 Subject: [PATCH 06/16] doc: i hate CTRL+R --- src/structs/Api.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/structs/Api.ts b/src/structs/Api.ts index 43ad1da..b5ee0c8 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -128,7 +128,7 @@ export class Api extends EventEmitter { } /** - * Get your bot"s stats + * Get your bot's stats * * @example * ```js @@ -141,7 +141,7 @@ export class Api extends EventEmitter { * } * ``` * - * @returns {BotStats} Your bot"s stats + * @returns {BotStats} Your bot's stats */ public async getStats(_id: Snowflake): Promise { if (_id) console.warn("[DeprecationWarning] getStats() no longer needs an ID argument"); @@ -253,7 +253,7 @@ export class Api extends EventEmitter { } /** - * Get recent unique users who"ve voted + * Get recent unique users who've voted * * @example * ```js @@ -275,7 +275,7 @@ export class Api extends EventEmitter { * ``` * * @param {number} [page] The page number. Each page can only have at most 100 voters. - * @returns {ShortUser[]} Array of unique users who"ve voted + * @returns {ShortUser[]} Array of unique users who've voted */ public async getVotes(page?: number): Promise { return this._request("GET", "/bots/votes", { page: page ?? 1 }); From d4a926a57c436d5f007bd849755605cc71d11168 Mon Sep 17 00:00:00 2001 From: null8626 Date: Sat, 8 Mar 2025 07:46:16 +0700 Subject: [PATCH 07/16] ci: remove no_token tests --- src/structs/Api.ts | 2 +- tests/Api.test.ts | 50 +++------------------------------------------- 2 files changed, 4 insertions(+), 48 deletions(-) diff --git a/src/structs/Api.ts b/src/structs/Api.ts index b5ee0c8..18a798a 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -46,7 +46,7 @@ export class Api extends EventEmitter { throw new Error("Got a malformed API token."); } - const tokenData = atob(token[1]); + const tokenData = atob(tokenSegments[1]); try { JSON.parse(tokenData).id; diff --git a/tests/Api.test.ts b/tests/Api.test.ts index 04000db..e32bee0 100644 --- a/tests/Api.test.ts +++ b/tests/Api.test.ts @@ -1,21 +1,14 @@ import { Api } from '../src/index'; import ApiError from '../src/utils/ApiError'; -import { BOT, BOT_STATS, USER, VOTES } from './mocks/data'; +import { BOT, BOT_STATS, VOTES } from './mocks/data'; -const is_owner = new Api('owner_token'); -const not_owner = new Api('not_owner_token'); -const no_token = new Api(''); +/* mock token */ +const is_owner = new Api('.eyJpZCI6IjEwMjY1MjU1NjgzNDQyNjQ3MjQiLCJib3QiOnRydWV9.'); describe('API postStats test', () => { - it('postStats should return 401 when no token is provided', async() => { - await expect(no_token.postStats({serverCount: 1, shardCount: 0})).rejects.toThrowError(ApiError); - }) it('postStats without server count should throw error', async () => { await expect(is_owner.postStats({shardCount: 0})).rejects.toThrowError(Error); }) - it('postStats should return 401 when no token is provided', async () => { - await expect(no_token.postStats({serverCount: 1, shardCount: 0})).rejects.toThrowError(ApiError); - }) /* TODO: Check for this is not added in code and api returns 400 @@ -43,10 +36,6 @@ describe('API postStats test', () => { expect(is_owner.postStats({serverCount: 1, shardCount: 10001})).rejects.toThrowError(Error); }) */ - - it('postStats should return 403 when token is not owner of bot', async () => { - await expect(not_owner.postStats({serverCount: 1, shardCount: 0})).rejects.toThrowError(ApiError); - }) it('postStats should return 200 when token is owner of bot', async () => { await expect(is_owner.postStats({serverCount: 1, shardCount: 1})).resolves.toBeInstanceOf(Object); @@ -54,10 +43,6 @@ describe('API postStats test', () => { }) describe('API getStats test', () => { - it('getStats should return 401 when no token is provided', () => { - expect(no_token.getStats('1')).rejects.toThrowError(ApiError); - }) - it('getStats should return 404 when bot is not found', async() => { await expect(is_owner.getStats('0')).rejects.toThrowError(ApiError); }) @@ -77,9 +62,6 @@ describe('API getStats test', () => { describe('API getBot test', () => { - it('getBot should return 401 when no token is provided', () => { - expect(no_token.getBot('1')).rejects.toThrowError(ApiError); - }) it('getBot should return 404 when bot is not found', () => { expect(is_owner.getBot('0')).rejects.toThrowError(ApiError); }) @@ -93,40 +75,14 @@ describe('API getBot test', () => { }) }) -describe('API getUser test', () => { - it('getUser should return 401 when no token is provided', () => { - expect(no_token.getUser('1')).rejects.toThrowError(ApiError); - }) - - it('getUser should return 404 when user is not found', () => { - expect(is_owner.getUser('0')).rejects.toThrowError(ApiError); - }) - - it('getUser should return 200 when user is found', async () => { - expect(is_owner.getUser('1')).resolves.toStrictEqual(USER); - }) - - it('getUser should throw when no id is provided', () => { - expect(is_owner.getUser('')).rejects.toThrowError(Error); - }) -}) - describe('API getVotes test', () => { - it('getVotes should return throw error when no token is provided', () => { - expect(no_token.getVotes()).rejects.toThrowError(Error); - }) - it('getVotes should return 200 when token is provided', () => { expect(is_owner.getVotes()).resolves.toEqual(VOTES); }) }); describe('API hasVoted test', () => { - it('hasVoted should throw error when no token is provided', () => { - expect(no_token.hasVoted('1')).rejects.toThrowError(Error); - }) - it('hasVoted should return 200 when token is provided', () => { expect(is_owner.hasVoted('1')).resolves.toBe(true); }) From 5f5c5ac00c52d10b7fcee0e7a8f858eceb313556 Mon Sep 17 00:00:00 2001 From: null8626 Date: Sat, 8 Mar 2025 07:51:36 +0700 Subject: [PATCH 08/16] ci: update mock types --- tests/mocks/data.ts | 62 +++++++++++++++------------------------------ 1 file changed, 21 insertions(+), 41 deletions(-) diff --git a/tests/mocks/data.ts b/tests/mocks/data.ts index fc3bd6b..b79d946 100644 --- a/tests/mocks/data.ts +++ b/tests/mocks/data.ts @@ -1,29 +1,24 @@ // https://docs.top.gg/api/bot/#find-one-bot export const BOT = { - "defAvatar": "6debd47ed13483642cf09e832ed0bc1b", - "invite": "", - "website": "https://discordbots.org", - "support": "KYZsaFb", - "github": "https://github.com/DiscordBotList/Luca", - "longdesc": "Luca only works in the **Discord Bot List** server. \r\nPrepend commands with the prefix `-` or `@Luca#1375`. \r\n**Please refrain from using these commands in non testing channels.**\r\n- `botinfo @bot` Shows bot info, title redirects to site listing.\r\n- `bots @user`* Shows all bots of that user, includes bots in the queue.\r\n- `owner / -owners @bot`* Shows all owners of that bot.\r\n- `prefix @bot`* Shows the prefix of that bot.\r\n* Mobile friendly version exists. Just add `noembed` to the end of the command.\r\n", - "shortdesc": "Luca is a bot for managing and informing members of the server", - "prefix": "- or @Luca#1375", - "lib": "discord.js", - "clientid": "264811613708746752", - "avatar": "7edcc4c6fbb0b23762455ca139f0e1c9", - "id": "264811613708746752", - "discriminator": "1375", - "username": "Luca", - "date": "2017-04-26T18:08:17.125Z", - "server_count": 2, - "guilds": ["417723229721853963", "264445053596991498"], - "shards": [], - "monthlyPoints": 19, - "points": 397, - "certifiedBot": false, - "owners": ["129908908096487424"], - "tags": ["Moderation", "Role Management", "Logging"], - "donatebotguildid": "" + invite: "https://top.gg/discord", + support: "https://discord.gg/dbl", + github: "https://github.com/top-gg", + longdesc: "A bot to grant API access to our Library Developers on the Top.gg site without them needing to submit a bot to pass verification just to be able to access the API. \n" + + "\n" + + "Access to this bot's team can be requested by contacting a Community Manager in [our Discord server](https://top.gg/discord).", + shortdesc: "API access for Top.gg Library Developers", + prefix: "/", + clientid: "1026525568344264724", + avatar: "https://cdn.discordapp.com/avatars/1026525568344264724/cd70e62e41f691f1c05c8455d8c31e23.png", + id: "1026525568344264724", + username: "Top.gg Lib Dev API Access", + date: "2022-10-03T16:08:55.000Z", + server_count: 2, + monthlyPoints: 4, + points: 18, + owners: [ "491002268401926145" ], + tags: [ "api", "library", "topgg" ], + reviews: { averageScore: 5, count: 2 } } // https://docs.top.gg/api/bot/#search-bots @@ -40,14 +35,14 @@ export const VOTES = [ { "username": "Xetera", "id": "140862798832861184", - "avatar": "a_1241439d430def25c100dd28add2d42f" + "avatar": "https://cdn.discordapp.com/avatars/1026525568344264724/cd70e62e41f691f1c05c8455d8c31e23.png" } ] // https://docs.top.gg/api/bot/#bot-stats export const BOT_STATS = { server_count: 0, - shards: ['200'], + shards: ["200"], shard_count: 1 } @@ -56,21 +51,6 @@ export const USER_VOTE = { "voted": 1 } -// https://docs.top.gg/api/user/#structure -export const USER = { - "discriminator": "0001", - "avatar": "a_1241439d430def25c100dd28add2d42f", - "id": "140862798832861184", - "username": "Xetera", - "defAvatar": "322c936a8c8be1b803cd94861bdfa868", - "admin": true, - "webMod": true, - "mod": true, - "certifiedDev": false, - "supporter": false, - "social": {} -} - export const USER_VOTE_CHECK = { voted: 1 } From 915106715baff799ac2b90fdb4e7bd10bbadb02d Mon Sep 17 00:00:00 2001 From: null8626 Date: Sat, 8 Mar 2025 08:28:06 +0700 Subject: [PATCH 09/16] style: prettier --- src/structs/Api.ts | 37 ++++++---- src/structs/Webhook.ts | 17 +++-- src/typings.ts | 18 ++--- tests/Api.test.ts | 128 ++++++++++++++++---------------- tests/jest.setup.ts | 153 +++++++++++++++++++++++---------------- tests/mocks/data.ts | 48 ++++++------ tests/mocks/endpoints.ts | 152 ++++++++++++++++++-------------------- 7 files changed, 291 insertions(+), 262 deletions(-) diff --git a/src/structs/Api.ts b/src/structs/Api.ts index 18a798a..f77ab8e 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -30,7 +30,7 @@ import { */ export class Api extends EventEmitter { private options: APIOptions; - + /** * Create Top.gg API instance * @@ -41,19 +41,21 @@ export class Api extends EventEmitter { super(); const tokenSegments = token.split("."); - + if (tokenSegments.length !== 3) { throw new Error("Got a malformed API token."); } - + const tokenData = atob(tokenSegments[1]); - + try { JSON.parse(tokenData).id; } catch { - throw new Error("Invalid API token state, this should not happen! Please report!"); + throw new Error( + "Invalid API token state, this should not happen! Please report!", + ); } - + this.options = { token, ...options, @@ -63,7 +65,7 @@ export class Api extends EventEmitter { private async _request( method: Dispatcher.HttpMethod, path: string, - body?: Record + body?: Record, ): Promise { const headers: IncomingHttpHeaders = {}; if (this.options.token) headers["authorization"] = this.options.token; @@ -82,7 +84,7 @@ export class Api extends EventEmitter { let responseBody; if ( (response.headers["content-type"] as string)?.startsWith( - "application/json" + "application/json", ) ) { responseBody = await response.body.json(); @@ -94,7 +96,7 @@ export class Api extends EventEmitter { throw new ApiError( response.statusCode, STATUS_CODES[response.statusCode] ?? "", - response + response, ); } @@ -120,7 +122,7 @@ export class Api extends EventEmitter { /* eslint-disable camelcase */ await this._request("POST", "/bots/stats", { - server_count: stats.serverCount + server_count: stats.serverCount, }); /* eslint-enable camelcase */ @@ -144,12 +146,15 @@ export class Api extends EventEmitter { * @returns {BotStats} Your bot's stats */ public async getStats(_id: Snowflake): Promise { - if (_id) console.warn("[DeprecationWarning] getStats() no longer needs an ID argument"); + if (_id) + console.warn( + "[DeprecationWarning] getStats() no longer needs an ID argument", + ); const result = await this._request("GET", "/bots/stats"); return { serverCount: result.server_count, shardCount: null, - shards: [] + shards: [], }; } @@ -171,7 +176,7 @@ export class Api extends EventEmitter { /** * @deprecated No longer supported by Top.gg API v0. - * + * * Get user info * * @example @@ -185,7 +190,9 @@ export class Api extends EventEmitter { * @returns {UserInfo} Info for user */ public async getUser(id: Snowflake): Promise { - console.warn("[DeprecationWarning] getUser is no longer supported by Top.gg API v0."); + console.warn( + "[DeprecationWarning] getUser is no longer supported by Top.gg API v0.", + ); return this._request("GET", `/users/${id}`); } @@ -296,7 +303,7 @@ export class Api extends EventEmitter { public async hasVoted(id: Snowflake): Promise { if (!id) throw new Error("Missing ID"); return this._request("GET", "/bots/check", { userId: id }).then( - (x) => !!x.voted + (x) => !!x.voted, ); } diff --git a/src/structs/Webhook.ts b/src/structs/Webhook.ts index d257ce8..199fbc9 100644 --- a/src/structs/Webhook.ts +++ b/src/structs/Webhook.ts @@ -45,14 +45,17 @@ export class Webhook { * * @param authorization Webhook authorization to verify requests */ - constructor(private authorization?: string, options: WebhookOptions = {}) { + constructor( + private authorization?: string, + options: WebhookOptions = {}, + ) { this.options = { error: options.error ?? console.error, }; } private _formatIncoming( - body: WebhookPayload & { query: string } + body: WebhookPayload & { query: string }, ): WebhookPayload { const out: WebhookPayload = { ...body }; if (body?.query?.length > 0) @@ -62,7 +65,7 @@ export class Webhook { private _parseRequest( req: Request, - res: Response + res: Response, ): Promise { return new Promise((resolve) => { if ( @@ -116,13 +119,13 @@ export class Webhook { payload: WebhookPayload, req?: Request, res?: Response, - next?: NextFunction - ) => void | Promise + next?: NextFunction, + ) => void | Promise, ) { return async ( req: Request, res: Response, - next: NextFunction + next: NextFunction, ): Promise => { const response = await this._parseRequest(req, res); if (!response) return; @@ -157,7 +160,7 @@ export class Webhook { return async ( req: Request, res: Response, - next: NextFunction + next: NextFunction, ): Promise => { const response = await this._parseRequest(req, res); if (!response) return; diff --git a/src/typings.ts b/src/typings.ts index b67aa9b..f891c54 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -23,13 +23,13 @@ export interface BotInfo { avatar: string; /** * The cdn hash of the bot's avatar if the bot has none - * + * * @deprecated No longer supported by Top.gg API v0. */ defAvatar: string; /** * The URL for the banner image - * + * * @deprecated No longer supported by Top.gg API v0. */ bannerUrl?: string; @@ -57,7 +57,7 @@ export interface BotInfo { owners: Snowflake[]; /** * The guilds featured on the bot page - * + * * @deprecated No longer supported by Top.gg API v0. */ guilds: Snowflake[]; @@ -67,7 +67,7 @@ export interface BotInfo { date: string; /** * The certified status of the bot - * + * * @deprecated No longer supported by Top.gg API v0. */ certifiedBot: boolean; @@ -79,7 +79,7 @@ export interface BotInfo { monthlyPoints: number; /** * The guild id for the donatebot setup - * + * * @deprecated No longer supported by Top.gg API v0. */ donatebotguildid: Snowflake; @@ -91,7 +91,7 @@ export interface BotInfo { averageScore: number; /** This bot's review count */ count: number; - } + }; } export interface BotStats { @@ -100,19 +100,19 @@ export interface BotStats { /** * The amount of servers the bot is in per shard. Always present but can be * empty. (Only when receiving stats) - * + * * @deprecated No longer supported by Top.gg API v0. */ shards?: number[]; /** * The shard ID to post as (only when posting) - * + * * @deprecated No longer supported by Top.gg API v0. */ shardId?: number; /** * The amount of shards a bot has - * + * * @deprecated No longer supported by Top.gg API v0. */ shardCount?: number | null; diff --git a/tests/Api.test.ts b/tests/Api.test.ts index e32bee0..c682117 100644 --- a/tests/Api.test.ts +++ b/tests/Api.test.ts @@ -1,16 +1,20 @@ -import { Api } from '../src/index'; -import ApiError from '../src/utils/ApiError'; -import { BOT, BOT_STATS, VOTES } from './mocks/data'; +import { Api } from "../src/index"; +import ApiError from "../src/utils/ApiError"; +import { BOT, BOT_STATS, VOTES } from "./mocks/data"; /* mock token */ -const is_owner = new Api('.eyJpZCI6IjEwMjY1MjU1NjgzNDQyNjQ3MjQiLCJib3QiOnRydWV9.'); - -describe('API postStats test', () => { - it('postStats without server count should throw error', async () => { - await expect(is_owner.postStats({shardCount: 0})).rejects.toThrowError(Error); - }) - - /* +const is_owner = new Api( + ".eyJpZCI6IjEwMjY1MjU1NjgzNDQyNjQ3MjQiLCJib3QiOnRydWV9.", +); + +describe("API postStats test", () => { + it("postStats without server count should throw error", async () => { + await expect(is_owner.postStats({ shardCount: 0 })).rejects.toThrowError( + Error, + ); + }); + + /* TODO: Check for this is not added in code and api returns 400 it('postStats with invalid negative server count should throw error', () => { expect(is_owner.postStats({serverCount: -1, shardCount: 0})).rejects.toThrowError(Error); @@ -37,63 +41,63 @@ describe('API postStats test', () => { }) */ - it('postStats should return 200 when token is owner of bot', async () => { - await expect(is_owner.postStats({serverCount: 1, shardCount: 1})).resolves.toBeInstanceOf(Object); - }) -}) - -describe('API getStats test', () => { - it('getStats should return 404 when bot is not found', async() => { - await expect(is_owner.getStats('0')).rejects.toThrowError(ApiError); - }) + it("postStats should return 200 when token is owner of bot", async () => { + await expect( + is_owner.postStats({ serverCount: 1, shardCount: 1 }), + ).resolves.toBeInstanceOf(Object); + }); +}); - it('getStats should return 200 when bot is found', async () => { - expect(is_owner.getStats('1')).resolves.toStrictEqual({ - serverCount: BOT_STATS.server_count, - shardCount: BOT_STATS.shard_count, - shards: BOT_STATS.shards - }); - }) +describe("API getStats test", () => { + it("getStats should return 404 when bot is not found", async () => { + await expect(is_owner.getStats("0")).rejects.toThrowError(ApiError); + }); + + it("getStats should return 200 when bot is found", async () => { + expect(is_owner.getStats("1")).resolves.toStrictEqual({ + serverCount: BOT_STATS.server_count, + shardCount: BOT_STATS.shard_count, + shards: BOT_STATS.shards, + }); + }); + + it("getStats should throw when no id is provided", () => { + expect(is_owner.getStats("")).rejects.toThrowError(Error); + }); +}); - it('getStats should throw when no id is provided', () => { - expect(is_owner.getStats('')).rejects.toThrowError(Error); - }) -}) +describe("API getBot test", () => { + it("getBot should return 404 when bot is not found", () => { + expect(is_owner.getBot("0")).rejects.toThrowError(ApiError); + }); + it("getBot should return 200 when bot is found", async () => { + expect(is_owner.getBot("1")).resolves.toStrictEqual(BOT); + }); -describe('API getBot test', () => { - it('getBot should return 404 when bot is not found', () => { - expect(is_owner.getBot('0')).rejects.toThrowError(ApiError); - }) - - it('getBot should return 200 when bot is found', async () => { - expect(is_owner.getBot('1')).resolves.toStrictEqual(BOT); - }) - - it('getBot should throw when no id is provided', () => { - expect(is_owner.getBot('')).rejects.toThrowError(Error); - }) - -}) + it("getBot should throw when no id is provided", () => { + expect(is_owner.getBot("")).rejects.toThrowError(Error); + }); +}); -describe('API getVotes test', () => { - it('getVotes should return 200 when token is provided', () => { - expect(is_owner.getVotes()).resolves.toEqual(VOTES); - }) +describe("API getVotes test", () => { + it("getVotes should return 200 when token is provided", () => { + expect(is_owner.getVotes()).resolves.toEqual(VOTES); + }); }); -describe('API hasVoted test', () => { - it('hasVoted should return 200 when token is provided', () => { - expect(is_owner.hasVoted('1')).resolves.toBe(true); - }) +describe("API hasVoted test", () => { + it("hasVoted should return 200 when token is provided", () => { + expect(is_owner.hasVoted("1")).resolves.toBe(true); + }); - it('hasVoted should throw error when no id is provided', () => { - expect(is_owner.hasVoted('')).rejects.toThrowError(Error); - }) -}) + it("hasVoted should throw error when no id is provided", () => { + expect(is_owner.hasVoted("")).rejects.toThrowError(Error); + }); +}); -describe('API isWeekend tests', () => { - it('isWeekend should return true', async () => { - expect(is_owner.isWeekend()).resolves.toBe(true) - }) -}); \ No newline at end of file +describe("API isWeekend tests", () => { + it("isWeekend should return true", async () => { + expect(is_owner.isWeekend()).resolves.toBe(true); + }); +}); diff --git a/tests/jest.setup.ts b/tests/jest.setup.ts index 7145c22..c03b0e1 100644 --- a/tests/jest.setup.ts +++ b/tests/jest.setup.ts @@ -1,76 +1,103 @@ -import { MockAgent, setGlobalDispatcher } from 'undici'; -import { MockInterceptor } from 'undici/types/mock-interceptor'; -import { endpoints } from './mocks/endpoints'; +import { MockAgent, setGlobalDispatcher } from "undici"; +import { MockInterceptor } from "undici/types/mock-interceptor"; +import { endpoints } from "./mocks/endpoints"; interface IOptions { - pattern: string; - requireAuth?: boolean; - validate?: (request: MockInterceptor.MockResponseCallbackOptions) => void; + pattern: string; + requireAuth?: boolean; + validate?: (request: MockInterceptor.MockResponseCallbackOptions) => void; } export const getIdInPath = (pattern: string, url: string) => { - const regex = new RegExp(`^${pattern.replace(/:[^/]+/g, '([^/]+)')}$`); - const match = url.match(regex); - - return match ? match[1] : null; -} + const regex = new RegExp(`^${pattern.replace(/:[^/]+/g, "([^/]+)")}$`); + const match = url.match(regex); + + return match ? match[1] : null; +}; export const isMatchingPath = (pattern: string, url: string) => { - // Remove query params - url = url.split("?")[0] + // Remove query params + url = url.split("?")[0]; - if (pattern === url) { - return true; - } + if (pattern === url) { + return true; + } - // Check if there is an exact match - if(endpoints.some(({ pattern }) => pattern === url)) { - return false - }; + // Check if there is an exact match + if (endpoints.some(({ pattern }) => pattern === url)) { + return false; + } - return getIdInPath(pattern, url) !== null; -} + return getIdInPath(pattern, url) !== null; +}; beforeEach(() => { - const mockAgent = new MockAgent() - mockAgent.disableNetConnect(); - const client = mockAgent.get('https://top.gg'); - - - const generateResponse = (request: MockInterceptor.MockResponseCallbackOptions, statusCode: number, data: any, headers = {}, options: IOptions) => { - const requestHeaders = request.headers as any; - - // Check if token is avaliable - if (options.requireAuth && (!requestHeaders['authorization'] || requestHeaders['authorization'] == '')) return { statusCode: 401 }; - - // Check that user is owner of bot - if (options.requireAuth && requestHeaders['authorization'] !== 'owner_token') return { statusCode: 403 } - - const error = options.validate?.(request); - if (error) return error; - - return { - statusCode, - data: JSON.stringify(data), - responseOptions: { - headers: { 'content-type': 'application/json', ...headers }, - } - } - } - - endpoints.forEach(({ pattern, method, data, requireAuth, validate }) => { - client.intercept({ - path: (path) => isMatchingPath(pattern, path), - method, - }).reply((request) => {return generateResponse(request, 200, data, {}, { pattern, requireAuth, validate: validate })}); - }) - - client.intercept({ - path: (path) => !endpoints.some(({ pattern }) => isMatchingPath(pattern, path)), - method: (_) => true, - }).reply((request) => { - throw Error(`No endpoint found for ${request.method} ${request.path}`) + const mockAgent = new MockAgent(); + mockAgent.disableNetConnect(); + const client = mockAgent.get("https://top.gg"); + + const generateResponse = ( + request: MockInterceptor.MockResponseCallbackOptions, + statusCode: number, + data: any, + headers = {}, + options: IOptions, + ) => { + const requestHeaders = request.headers as any; + + // Check if token is avaliable + if ( + options.requireAuth && + (!requestHeaders["authorization"] || + requestHeaders["authorization"] == "") + ) + return { statusCode: 401 }; + + // Check that user is owner of bot + if ( + options.requireAuth && + requestHeaders["authorization"] !== "owner_token" + ) + return { statusCode: 403 }; + + const error = options.validate?.(request); + if (error) return error; + + return { + statusCode, + data: JSON.stringify(data), + responseOptions: { + headers: { "content-type": "application/json", ...headers }, + }, + }; + }; + + endpoints.forEach(({ pattern, method, data, requireAuth, validate }) => { + client + .intercept({ + path: (path) => isMatchingPath(pattern, path), + method, + }) + .reply((request) => { + return generateResponse( + request, + 200, + data, + {}, + { pattern, requireAuth, validate: validate }, + ); + }); + }); + + client + .intercept({ + path: (path) => + !endpoints.some(({ pattern }) => isMatchingPath(pattern, path)), + method: (_) => true, }) - - setGlobalDispatcher(mockAgent); -}) \ No newline at end of file + .reply((request) => { + throw Error(`No endpoint found for ${request.method} ${request.path}`); + }); + + setGlobalDispatcher(mockAgent); +}); diff --git a/tests/mocks/data.ts b/tests/mocks/data.ts index b79d946..f114eff 100644 --- a/tests/mocks/data.ts +++ b/tests/mocks/data.ts @@ -3,23 +3,25 @@ export const BOT = { invite: "https://top.gg/discord", support: "https://discord.gg/dbl", github: "https://github.com/top-gg", - longdesc: "A bot to grant API access to our Library Developers on the Top.gg site without them needing to submit a bot to pass verification just to be able to access the API. \n" + + longdesc: + "A bot to grant API access to our Library Developers on the Top.gg site without them needing to submit a bot to pass verification just to be able to access the API. \n" + "\n" + "Access to this bot's team can be requested by contacting a Community Manager in [our Discord server](https://top.gg/discord).", shortdesc: "API access for Top.gg Library Developers", prefix: "/", clientid: "1026525568344264724", - avatar: "https://cdn.discordapp.com/avatars/1026525568344264724/cd70e62e41f691f1c05c8455d8c31e23.png", + avatar: + "https://cdn.discordapp.com/avatars/1026525568344264724/cd70e62e41f691f1c05c8455d8c31e23.png", id: "1026525568344264724", username: "Top.gg Lib Dev API Access", date: "2022-10-03T16:08:55.000Z", server_count: 2, monthlyPoints: 4, points: 18, - owners: [ "491002268401926145" ], - tags: [ "api", "library", "topgg" ], - reviews: { averageScore: 5, count: 2 } -} + owners: ["491002268401926145"], + tags: ["api", "library", "topgg"], + reviews: { averageScore: 5, count: 2 }, +}; // https://docs.top.gg/api/bot/#search-bots export const BOTS = { @@ -28,35 +30,35 @@ export const BOTS = { count: 1, total: 1, results: [BOT], -} +}; // https://docs.top.gg/api/bot/#last-1000-votes export const VOTES = [ { - "username": "Xetera", - "id": "140862798832861184", - "avatar": "https://cdn.discordapp.com/avatars/1026525568344264724/cd70e62e41f691f1c05c8455d8c31e23.png" - } -] + username: "Xetera", + id: "140862798832861184", + avatar: + "https://cdn.discordapp.com/avatars/1026525568344264724/cd70e62e41f691f1c05c8455d8c31e23.png", + }, +]; // https://docs.top.gg/api/bot/#bot-stats export const BOT_STATS = { server_count: 0, - shards: ["200"], - shard_count: 1 -} + shards: [], + shard_count: null, +}; // https://docs.top.gg/api/bot/#individual-user-vote export const USER_VOTE = { - "voted": 1 -} + voted: 1, +}; -export const USER_VOTE_CHECK = { - voted: 1 -} +export const USER_VOTE_CHECK = { + voted: 1, +}; // Undocumented 😢 export const WEEKEND = { - is_weekend: true -} - + is_weekend: true, +}; diff --git a/tests/mocks/endpoints.ts b/tests/mocks/endpoints.ts index 1919c0a..f257a37 100644 --- a/tests/mocks/endpoints.ts +++ b/tests/mocks/endpoints.ts @@ -1,86 +1,72 @@ -import { MockInterceptor } from 'undici/types/mock-interceptor'; -import { BOT, BOTS, BOT_STATS, USER, USER_VOTE, USER_VOTE_CHECK, VOTES, WEEKEND } from './data'; -import { getIdInPath } from '../jest.setup'; +import { MockInterceptor } from "undici/types/mock-interceptor"; +import { + BOT, + BOTS, + BOT_STATS, + USER_VOTE, + USER_VOTE_CHECK, + VOTES, + WEEKEND, +} from "./data"; +import { getIdInPath } from "../jest.setup"; export const endpoints = [ - { - pattern: '/api/bots', - method: 'GET', - data: BOTS, - requireAuth: true + { + pattern: "/api/bots", + method: "GET", + data: BOTS, + requireAuth: true, + }, + { + pattern: "/api/bots/:bot_id", + method: "GET", + data: BOT, + requireAuth: true, + validate: (request: MockInterceptor.MockResponseCallbackOptions) => { + const bot_id = getIdInPath("/api/bots/:bot_id", request.path); + if (Number(bot_id) === 0) return { statusCode: 404 }; + return null; }, - { - pattern: '/api/bots/:bot_id', - method: 'GET', - data: BOT, - requireAuth: true, - validate: (request: MockInterceptor.MockResponseCallbackOptions) => { - const bot_id = getIdInPath('/api/bots/:bot_id', request.path); - if (Number(bot_id) === 0) return { statusCode: 404 }; - return null - }, - }, - { - pattern: '/api/bots/:bot_id/votes', - method: 'GET', - data: VOTES, - requireAuth: true - }, - { - pattern: '/api/bots/votes', - method: 'GET', - data: VOTES, - requireAuth: true - }, // Undocumented - { - pattern: '/api/bots/:bot_id/stats', - method: 'GET', - data: BOT_STATS, - requireAuth: true, - validate: (request: MockInterceptor.MockResponseCallbackOptions) => { - const bot_id = getIdInPath('/api/bots/:bot_id/stats', request.path); - if (Number(bot_id) === 0) return { statusCode: 404 }; - return null - }, - }, - { - pattern: '/api/bots/:bot_id/check', - method: 'GET', - data: USER_VOTE, - requireAuth: true - }, - { - pattern: '/api/bots/:bot_id/stats', - method: 'POST', - requireAuth: true - }, - { - pattern: '/api/bots/stats', - method: 'POST', - data: {}, - requireAuth: true - }, // Undocumented - { - pattern: '/api/users/:user_id', - method: 'GET', - data: USER, - requireAuth: true, - validate: (request: MockInterceptor.MockResponseCallbackOptions) => { - const bot_id = getIdInPath('/api/users/:user_id', request.path); - if (Number(bot_id) === 0) return { statusCode: 404 }; - return null - }, - }, - { - pattern: '/api/bots/check', - method: 'GET', - data: USER_VOTE_CHECK, - requireAuth: true - }, - { - pattern: '/api/weekend', - method: 'GET', - data: WEEKEND, - requireAuth: true - }, -]; \ No newline at end of file + }, + { + pattern: "/api/bots/votes", + method: "GET", + data: VOTES, + requireAuth: true, + }, + { + pattern: "/api/bots/check", + method: "GET", + data: USER_VOTE, + requireAuth: true, + }, + { + pattern: "/api/bots/stats", + method: "GET", + data: BOT_STATS, + requireAuth: true, + }, + { + pattern: "/api/bots/stats", + method: "POST", + requireAuth: true, + }, + { + pattern: "/api/bots/stats", + method: "POST", + data: {}, + requireAuth: true, + }, + { + pattern: "/api/bots/check", + method: "GET", + data: USER_VOTE_CHECK, + requireAuth: true, + }, + { + pattern: "/api/weekend", + method: "GET", + data: WEEKEND, + requireAuth: true, + }, +]; From 3357c1ad75b4697fcba0af2113ad347911fc7d83 Mon Sep 17 00:00:00 2001 From: null8626 Date: Sun, 9 Mar 2025 10:30:40 +0700 Subject: [PATCH 10/16] fix: make mock tests work --- src/structs/Api.ts | 5 ++-- tests/Api.test.ts | 65 ++++++++++------------------------------ tests/jest.setup.ts | 19 +----------- tests/mocks/endpoints.ts | 5 ---- 4 files changed, 20 insertions(+), 74 deletions(-) diff --git a/src/structs/Api.ts b/src/structs/Api.ts index f77ab8e..3c6c875 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -82,6 +82,7 @@ export class Api extends EventEmitter { }); let responseBody; + if ( (response.headers["content-type"] as string)?.startsWith( "application/json", @@ -118,7 +119,7 @@ export class Api extends EventEmitter { * @returns {BotStats} Passed object */ public async postStats(stats: BotStats): Promise { - if (!stats?.serverCount) throw new Error("Missing Server Count"); + if ((stats?.serverCount ?? 0) <= 0) throw new Error("Missing server count"); /* eslint-disable camelcase */ await this._request("POST", "/bots/stats", { @@ -145,7 +146,7 @@ export class Api extends EventEmitter { * * @returns {BotStats} Your bot's stats */ - public async getStats(_id: Snowflake): Promise { + public async getStats(_id?: Snowflake): Promise { if (_id) console.warn( "[DeprecationWarning] getStats() no longer needs an ID argument", diff --git a/tests/Api.test.ts b/tests/Api.test.ts index c682117..708d05c 100644 --- a/tests/Api.test.ts +++ b/tests/Api.test.ts @@ -3,101 +3,68 @@ import ApiError from "../src/utils/ApiError"; import { BOT, BOT_STATS, VOTES } from "./mocks/data"; /* mock token */ -const is_owner = new Api( +const client = new Api( ".eyJpZCI6IjEwMjY1MjU1NjgzNDQyNjQ3MjQiLCJib3QiOnRydWV9.", ); describe("API postStats test", () => { it("postStats without server count should throw error", async () => { - await expect(is_owner.postStats({ shardCount: 0 })).rejects.toThrowError( - Error, - ); + await expect(client.postStats({ shardCount: 0 })).rejects.toThrow(Error); }); - /* - TODO: Check for this is not added in code and api returns 400 - it('postStats with invalid negative server count should throw error', () => { - expect(is_owner.postStats({serverCount: -1, shardCount: 0})).rejects.toThrowError(Error); - }) - - TODO: Check for negative shardId is not added - it('postStats with invalid negative shardId should throw error', () => { - expect(is_owner.postStats({serverCount: 1, shardCount: 0, shardId: -1})).rejects.toThrowError(Error); - }) - - TODO: ShardId cannot be greater or equal to shardCount - it('postStats with shardId greater than shardCount should throw error', () => { - expect(is_owner.postStats({serverCount: 1, shardCount: 0, shardId: 1})).rejects.toThrowError(Error); - }) - - TODO: Check for negative shardCount is not added - it('postStats with invalid negative shardCount should throw error', () => { - expect(is_owner.postStats({serverCount: 1, shardCount: -1})).rejects.toThrowError(Error); - }) - - TODO: Check if shardCount is greater than 10000 - it('postStats with invalid shardCount should throw error', () => { - expect(is_owner.postStats({serverCount: 1, shardCount: 10001})).rejects.toThrowError(Error); - }) - */ + it("postStats with invalid negative server count should throw error", () => { + expect(client.postStats({ serverCount: -1 })).rejects.toThrow(Error); + }); it("postStats should return 200 when token is owner of bot", async () => { - await expect( - is_owner.postStats({ serverCount: 1, shardCount: 1 }), - ).resolves.toBeInstanceOf(Object); + await expect(client.postStats({ serverCount: 1 })).resolves.toBeInstanceOf( + Object, + ); }); }); describe("API getStats test", () => { - it("getStats should return 404 when bot is not found", async () => { - await expect(is_owner.getStats("0")).rejects.toThrowError(ApiError); - }); - it("getStats should return 200 when bot is found", async () => { - expect(is_owner.getStats("1")).resolves.toStrictEqual({ + expect(client.getStats("1")).resolves.toStrictEqual({ serverCount: BOT_STATS.server_count, shardCount: BOT_STATS.shard_count, shards: BOT_STATS.shards, }); }); - - it("getStats should throw when no id is provided", () => { - expect(is_owner.getStats("")).rejects.toThrowError(Error); - }); }); describe("API getBot test", () => { it("getBot should return 404 when bot is not found", () => { - expect(is_owner.getBot("0")).rejects.toThrowError(ApiError); + expect(client.getBot("0")).rejects.toThrow(ApiError); }); it("getBot should return 200 when bot is found", async () => { - expect(is_owner.getBot("1")).resolves.toStrictEqual(BOT); + expect(client.getBot("1")).resolves.toStrictEqual(BOT); }); it("getBot should throw when no id is provided", () => { - expect(is_owner.getBot("")).rejects.toThrowError(Error); + expect(client.getBot("")).rejects.toThrow(Error); }); }); describe("API getVotes test", () => { it("getVotes should return 200 when token is provided", () => { - expect(is_owner.getVotes()).resolves.toEqual(VOTES); + expect(client.getVotes()).resolves.toEqual(VOTES); }); }); describe("API hasVoted test", () => { it("hasVoted should return 200 when token is provided", () => { - expect(is_owner.hasVoted("1")).resolves.toBe(true); + expect(client.hasVoted("1")).resolves.toBe(true); }); it("hasVoted should throw error when no id is provided", () => { - expect(is_owner.hasVoted("")).rejects.toThrowError(Error); + expect(client.hasVoted("")).rejects.toThrow(Error); }); }); describe("API isWeekend tests", () => { it("isWeekend should return true", async () => { - expect(is_owner.isWeekend()).resolves.toBe(true); + expect(client.isWeekend()).resolves.toBe(true); }); }); diff --git a/tests/jest.setup.ts b/tests/jest.setup.ts index c03b0e1..a7f93b4 100644 --- a/tests/jest.setup.ts +++ b/tests/jest.setup.ts @@ -43,23 +43,6 @@ beforeEach(() => { headers = {}, options: IOptions, ) => { - const requestHeaders = request.headers as any; - - // Check if token is avaliable - if ( - options.requireAuth && - (!requestHeaders["authorization"] || - requestHeaders["authorization"] == "") - ) - return { statusCode: 401 }; - - // Check that user is owner of bot - if ( - options.requireAuth && - requestHeaders["authorization"] !== "owner_token" - ) - return { statusCode: 403 }; - const error = options.validate?.(request); if (error) return error; @@ -84,7 +67,7 @@ beforeEach(() => { 200, data, {}, - { pattern, requireAuth, validate: validate }, + { pattern, requireAuth, validate }, ); }); }); diff --git a/tests/mocks/endpoints.ts b/tests/mocks/endpoints.ts index f257a37..97e8f7a 100644 --- a/tests/mocks/endpoints.ts +++ b/tests/mocks/endpoints.ts @@ -46,11 +46,6 @@ export const endpoints = [ data: BOT_STATS, requireAuth: true, }, - { - pattern: "/api/bots/stats", - method: "POST", - requireAuth: true, - }, { pattern: "/api/bots/stats", method: "POST", From 95082bdaf78bbbc7121db5f1bcc051a07cf925d1 Mon Sep 17 00:00:00 2001 From: null8626 Date: Sun, 9 Mar 2025 10:31:44 +0700 Subject: [PATCH 11/16] test: rename test --- tests/Api.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Api.test.ts b/tests/Api.test.ts index 708d05c..3570f24 100644 --- a/tests/Api.test.ts +++ b/tests/Api.test.ts @@ -16,7 +16,7 @@ describe("API postStats test", () => { expect(client.postStats({ serverCount: -1 })).rejects.toThrow(Error); }); - it("postStats should return 200 when token is owner of bot", async () => { + it("postStats should return 200", async () => { await expect(client.postStats({ serverCount: 1 })).resolves.toBeInstanceOf( Object, ); From 1042573edfc8b3cc31c1ad55222d5a20b19d78af Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Sun, 9 Mar 2025 11:04:17 +0700 Subject: [PATCH 12/16] doc: replace outdated screenshot --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8fdef71..125807d 100644 --- a/README.md +++ b/README.md @@ -58,5 +58,5 @@ app.post( app.listen(3000); // your port ``` -With this example, your webhook dashboard should look like this: -![](https://i.imgur.com/wFlp4Hg.png) +With this example, your webhook dashboard (`https://top.gg/bot/{your bot's id}/webhooks`) should look like this: +![](https://i.imgur.com/cZfZgK5.png) From 28d431f6b14192a42d922bf10fef119c621c91d9 Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Sun, 9 Mar 2025 14:31:01 +0700 Subject: [PATCH 13/16] doc: use a tutorial link --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 125807d..156cae7 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ An official module for interacting with the Top.gg API # Introduction -The base client is Topgg.Api, and it takes your Top.gg token and provides you with plenty of methods to interact with the API. +The base client is Topgg.Api, and it takes your Top.gg token and provides you with plenty of methods to interact with the API. -Your Top.gg token can be found at `top.gg/bot/(BOT_ID)/webhooks` and copying the token. +See [this tutorial](https://github.com/top-gg/rust-sdk/assets/60427892/d2df5bd3-bc48-464c-b878-a04121727bff) on how to retrieve your API token. You can also setup webhooks via Topgg.Webhook, look down below at the examples for how to do so! From 50fb0761c24fa6ccec9ef31bbc931b6761f9daef Mon Sep 17 00:00:00 2001 From: null8626 Date: Mon, 10 Mar 2025 01:09:40 +0700 Subject: [PATCH 14/16] style: use double quotes and remove trailing comma --- src/structs/Api.ts | 24 ++++++++++++------------ src/structs/Webhook.ts | 16 ++++++++-------- src/utils/ApiError.ts | 2 +- tests/Api.test.ts | 6 +++--- tests/jest.setup.ts | 12 ++++++------ tests/mocks/data.ts | 16 ++++++++-------- tests/mocks/endpoints.ts | 20 ++++++++++---------- 7 files changed, 48 insertions(+), 48 deletions(-) diff --git a/src/structs/Api.ts b/src/structs/Api.ts index 3c6c875..c52b4ef 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -12,7 +12,7 @@ import { UserInfo, BotsResponse, ShortUser, - BotsQuery, + BotsQuery } from "../typings"; /** @@ -52,20 +52,20 @@ export class Api extends EventEmitter { JSON.parse(tokenData).id; } catch { throw new Error( - "Invalid API token state, this should not happen! Please report!", + "Invalid API token state, this should not happen! Please report!" ); } this.options = { token, - ...options, + ...options }; } private async _request( method: Dispatcher.HttpMethod, path: string, - body?: Record, + body?: Record ): Promise { const headers: IncomingHttpHeaders = {}; if (this.options.token) headers["authorization"] = this.options.token; @@ -78,14 +78,14 @@ export class Api extends EventEmitter { const response = await request(url, { method, headers, - body: body && method !== "GET" ? JSON.stringify(body) : undefined, + body: body && method !== "GET" ? JSON.stringify(body) : undefined }); let responseBody; if ( (response.headers["content-type"] as string)?.startsWith( - "application/json", + "application/json" ) ) { responseBody = await response.body.json(); @@ -97,7 +97,7 @@ export class Api extends EventEmitter { throw new ApiError( response.statusCode, STATUS_CODES[response.statusCode] ?? "", - response, + response ); } @@ -123,7 +123,7 @@ export class Api extends EventEmitter { /* eslint-disable camelcase */ await this._request("POST", "/bots/stats", { - server_count: stats.serverCount, + server_count: stats.serverCount }); /* eslint-enable camelcase */ @@ -149,13 +149,13 @@ export class Api extends EventEmitter { public async getStats(_id?: Snowflake): Promise { if (_id) console.warn( - "[DeprecationWarning] getStats() no longer needs an ID argument", + "[DeprecationWarning] getStats() no longer needs an ID argument" ); const result = await this._request("GET", "/bots/stats"); return { serverCount: result.server_count, shardCount: null, - shards: [], + shards: [] }; } @@ -192,7 +192,7 @@ export class Api extends EventEmitter { */ public async getUser(id: Snowflake): Promise { console.warn( - "[DeprecationWarning] getUser is no longer supported by Top.gg API v0.", + "[DeprecationWarning] getUser is no longer supported by Top.gg API v0." ); return this._request("GET", `/users/${id}`); @@ -304,7 +304,7 @@ export class Api extends EventEmitter { public async hasVoted(id: Snowflake): Promise { if (!id) throw new Error("Missing ID"); return this._request("GET", "/bots/check", { userId: id }).then( - (x) => !!x.voted, + (x) => !!x.voted ); } diff --git a/src/structs/Webhook.ts b/src/structs/Webhook.ts index 199fbc9..eacc0dd 100644 --- a/src/structs/Webhook.ts +++ b/src/structs/Webhook.ts @@ -47,15 +47,15 @@ export class Webhook { */ constructor( private authorization?: string, - options: WebhookOptions = {}, + options: WebhookOptions = {} ) { this.options = { - error: options.error ?? console.error, + error: options.error ?? console.error }; } private _formatIncoming( - body: WebhookPayload & { query: string }, + body: WebhookPayload & { query: string } ): WebhookPayload { const out: WebhookPayload = { ...body }; if (body?.query?.length > 0) @@ -65,7 +65,7 @@ export class Webhook { private _parseRequest( req: Request, - res: Response, + res: Response ): Promise { return new Promise((resolve) => { if ( @@ -119,13 +119,13 @@ export class Webhook { payload: WebhookPayload, req?: Request, res?: Response, - next?: NextFunction, - ) => void | Promise, + next?: NextFunction + ) => void | Promise ) { return async ( req: Request, res: Response, - next: NextFunction, + next: NextFunction ): Promise => { const response = await this._parseRequest(req, res); if (!response) return; @@ -160,7 +160,7 @@ export class Webhook { return async ( req: Request, res: Response, - next: NextFunction, + next: NextFunction ): Promise => { const response = await this._parseRequest(req, res); if (!response) return; diff --git a/src/utils/ApiError.ts b/src/utils/ApiError.ts index b4df924..6940ee0 100644 --- a/src/utils/ApiError.ts +++ b/src/utils/ApiError.ts @@ -2,7 +2,7 @@ import type { Dispatcher } from "undici"; const tips = { 401: "You need a token for this endpoint", - 403: "You don't have access to this endpoint", + 403: "You don't have access to this endpoint" }; /** API Error */ diff --git a/tests/Api.test.ts b/tests/Api.test.ts index 3570f24..fbdcdb8 100644 --- a/tests/Api.test.ts +++ b/tests/Api.test.ts @@ -4,7 +4,7 @@ import { BOT, BOT_STATS, VOTES } from "./mocks/data"; /* mock token */ const client = new Api( - ".eyJpZCI6IjEwMjY1MjU1NjgzNDQyNjQ3MjQiLCJib3QiOnRydWV9.", + ".eyJpZCI6IjEwMjY1MjU1NjgzNDQyNjQ3MjQiLCJib3QiOnRydWV9." ); describe("API postStats test", () => { @@ -18,7 +18,7 @@ describe("API postStats test", () => { it("postStats should return 200", async () => { await expect(client.postStats({ serverCount: 1 })).resolves.toBeInstanceOf( - Object, + Object ); }); }); @@ -28,7 +28,7 @@ describe("API getStats test", () => { expect(client.getStats("1")).resolves.toStrictEqual({ serverCount: BOT_STATS.server_count, shardCount: BOT_STATS.shard_count, - shards: BOT_STATS.shards, + shards: BOT_STATS.shards }); }); }); diff --git a/tests/jest.setup.ts b/tests/jest.setup.ts index a7f93b4..c934abd 100644 --- a/tests/jest.setup.ts +++ b/tests/jest.setup.ts @@ -41,7 +41,7 @@ beforeEach(() => { statusCode: number, data: any, headers = {}, - options: IOptions, + options: IOptions ) => { const error = options.validate?.(request); if (error) return error; @@ -50,8 +50,8 @@ beforeEach(() => { statusCode, data: JSON.stringify(data), responseOptions: { - headers: { "content-type": "application/json", ...headers }, - }, + headers: { "content-type": "application/json", ...headers } + } }; }; @@ -59,7 +59,7 @@ beforeEach(() => { client .intercept({ path: (path) => isMatchingPath(pattern, path), - method, + method }) .reply((request) => { return generateResponse( @@ -67,7 +67,7 @@ beforeEach(() => { 200, data, {}, - { pattern, requireAuth, validate }, + { pattern, requireAuth, validate } ); }); }); @@ -76,7 +76,7 @@ beforeEach(() => { .intercept({ path: (path) => !endpoints.some(({ pattern }) => isMatchingPath(pattern, path)), - method: (_) => true, + method: (_) => true }) .reply((request) => { throw Error(`No endpoint found for ${request.method} ${request.path}`); diff --git a/tests/mocks/data.ts b/tests/mocks/data.ts index f114eff..504eec7 100644 --- a/tests/mocks/data.ts +++ b/tests/mocks/data.ts @@ -20,7 +20,7 @@ export const BOT = { points: 18, owners: ["491002268401926145"], tags: ["api", "library", "topgg"], - reviews: { averageScore: 5, count: 2 }, + reviews: { averageScore: 5, count: 2 } }; // https://docs.top.gg/api/bot/#search-bots @@ -29,7 +29,7 @@ export const BOTS = { offset: 0, count: 1, total: 1, - results: [BOT], + results: [BOT] }; // https://docs.top.gg/api/bot/#last-1000-votes @@ -38,27 +38,27 @@ export const VOTES = [ username: "Xetera", id: "140862798832861184", avatar: - "https://cdn.discordapp.com/avatars/1026525568344264724/cd70e62e41f691f1c05c8455d8c31e23.png", - }, + "https://cdn.discordapp.com/avatars/1026525568344264724/cd70e62e41f691f1c05c8455d8c31e23.png" + } ]; // https://docs.top.gg/api/bot/#bot-stats export const BOT_STATS = { server_count: 0, shards: [], - shard_count: null, + shard_count: null }; // https://docs.top.gg/api/bot/#individual-user-vote export const USER_VOTE = { - voted: 1, + voted: 1 }; export const USER_VOTE_CHECK = { - voted: 1, + voted: 1 }; // Undocumented 😢 export const WEEKEND = { - is_weekend: true, + is_weekend: true }; diff --git a/tests/mocks/endpoints.ts b/tests/mocks/endpoints.ts index 97e8f7a..9f0bf2c 100644 --- a/tests/mocks/endpoints.ts +++ b/tests/mocks/endpoints.ts @@ -6,7 +6,7 @@ import { USER_VOTE, USER_VOTE_CHECK, VOTES, - WEEKEND, + WEEKEND } from "./data"; import { getIdInPath } from "../jest.setup"; @@ -15,7 +15,7 @@ export const endpoints = [ pattern: "/api/bots", method: "GET", data: BOTS, - requireAuth: true, + requireAuth: true }, { pattern: "/api/bots/:bot_id", @@ -26,42 +26,42 @@ export const endpoints = [ const bot_id = getIdInPath("/api/bots/:bot_id", request.path); if (Number(bot_id) === 0) return { statusCode: 404 }; return null; - }, + } }, { pattern: "/api/bots/votes", method: "GET", data: VOTES, - requireAuth: true, + requireAuth: true }, { pattern: "/api/bots/check", method: "GET", data: USER_VOTE, - requireAuth: true, + requireAuth: true }, { pattern: "/api/bots/stats", method: "GET", data: BOT_STATS, - requireAuth: true, + requireAuth: true }, { pattern: "/api/bots/stats", method: "POST", data: {}, - requireAuth: true, + requireAuth: true }, { pattern: "/api/bots/check", method: "GET", data: USER_VOTE_CHECK, - requireAuth: true, + requireAuth: true }, { pattern: "/api/weekend", method: "GET", data: WEEKEND, - requireAuth: true, - }, + requireAuth: true + } ]; From 9a4778154ac7dd6609e2c4197e481cf7f647b1b4 Mon Sep 17 00:00:00 2001 From: null8626 Date: Mon, 10 Mar 2025 10:39:56 +0700 Subject: [PATCH 15/16] style: apply to js files too --- .eslintrc.js | 2 +- jest.config.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index fbe2908..f160664 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,6 +2,6 @@ module.exports = { ignorePatterns: ["node_modules/*", "docs/*", "dist/*"], extends: "@top-gg/eslint-config", parserOptions: { - project: "./tsconfig.json", + project: "./tsconfig.json" } }; diff --git a/jest.config.js b/jest.config.js index fa04662..14eb669 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,6 +1,6 @@ /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - setupFilesAfterEnv: ['./tests/jest.setup.ts'] -}; \ No newline at end of file + preset: "ts-jest", + testEnvironment: "node", + setupFilesAfterEnv: ["./tests/jest.setup.ts"] +}; From 5fa294773ddd9790809e1fabcf0289d9f575f9c6 Mon Sep 17 00:00:00 2001 From: null8626 Date: Wed, 12 Mar 2025 09:19:20 +0700 Subject: [PATCH 16/16] style: revert many stylistic changes --- .eslintrc.js | 2 +- jest.config.js | 8 +-- src/structs/Api.ts | 32 +++++------ src/structs/Webhook.ts | 7 +-- src/utils/ApiError.ts | 2 +- tests/Api.test.ts | 98 +++++++++++++++++---------------- tests/jest.setup.ts | 113 ++++++++++++++++---------------------- tests/mocks/data.ts | 24 ++++----- tests/mocks/endpoints.ts | 114 +++++++++++++++++---------------------- 9 files changed, 178 insertions(+), 222 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index f160664..fbe2908 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,6 +2,6 @@ module.exports = { ignorePatterns: ["node_modules/*", "docs/*", "dist/*"], extends: "@top-gg/eslint-config", parserOptions: { - project: "./tsconfig.json" + project: "./tsconfig.json", } }; diff --git a/jest.config.js b/jest.config.js index 14eb669..fa04662 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,6 +1,6 @@ /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { - preset: "ts-jest", - testEnvironment: "node", - setupFilesAfterEnv: ["./tests/jest.setup.ts"] -}; + preset: 'ts-jest', + testEnvironment: 'node', + setupFilesAfterEnv: ['./tests/jest.setup.ts'] +}; \ No newline at end of file diff --git a/src/structs/Api.ts b/src/structs/Api.ts index c52b4ef..74aaf73 100644 --- a/src/structs/Api.ts +++ b/src/structs/Api.ts @@ -12,7 +12,7 @@ import { UserInfo, BotsResponse, ShortUser, - BotsQuery + BotsQuery, } from "../typings"; /** @@ -58,7 +58,7 @@ export class Api extends EventEmitter { this.options = { token, - ...options + ...options, }; } @@ -78,7 +78,7 @@ export class Api extends EventEmitter { const response = await request(url, { method, headers, - body: body && method !== "GET" ? JSON.stringify(body) : undefined + body: body && method !== "GET" ? JSON.stringify(body) : undefined, }); let responseBody; @@ -110,7 +110,7 @@ export class Api extends EventEmitter { * @example * ```js * await api.postStats({ - * serverCount: 28199 + * serverCount: 28199, * }); * ``` * @@ -123,7 +123,7 @@ export class Api extends EventEmitter { /* eslint-disable camelcase */ await this._request("POST", "/bots/stats", { - server_count: stats.serverCount + server_count: stats.serverCount, }); /* eslint-enable camelcase */ @@ -155,7 +155,7 @@ export class Api extends EventEmitter { return { serverCount: result.server_count, shardCount: null, - shards: [] + shards: [], }; } @@ -232,12 +232,12 @@ export class Api extends EventEmitter { * { * results: [ * { - * id: "461521980492087297", - * username: "Shiro" + * id: '461521980492087297', + * username: 'Shiro' * }, * { - * id: "493716749342998541", - * username: "Mimu" + * id: '493716749342998541', + * username: 'Mimu' * }, * ... * ], @@ -269,14 +269,14 @@ export class Api extends EventEmitter { * // => * [ * { - * username: "Xignotic", - * id: "205680187394752512", - * avatar: "https://cdn.discordapp.com/avatars/1026525568344264724/cd70e62e41f691f1c05c8455d8c31e23.png" + * username: 'Xignotic', + * id: '205680187394752512', + * avatar: 'https://cdn.discordapp.com/avatars/1026525568344264724/cd70e62e41f691f1c05c8455d8c31e23.png' * }, * { - * username: "iara", - * id: "395526710101278721", - * avatar: "https://cdn.discordapp.com/avatars/1026525568344264724/cd70e62e41f691f1c05c8455d8c31e23.png" + * username: 'iara', + * id: '395526710101278721', + * avatar: 'https://cdn.discordapp.com/avatars/1026525568344264724/cd70e62e41f691f1c05c8455d8c31e23.png' * } * ...more * ] diff --git a/src/structs/Webhook.ts b/src/structs/Webhook.ts index eacc0dd..d257ce8 100644 --- a/src/structs/Webhook.ts +++ b/src/structs/Webhook.ts @@ -45,12 +45,9 @@ export class Webhook { * * @param authorization Webhook authorization to verify requests */ - constructor( - private authorization?: string, - options: WebhookOptions = {} - ) { + constructor(private authorization?: string, options: WebhookOptions = {}) { this.options = { - error: options.error ?? console.error + error: options.error ?? console.error, }; } diff --git a/src/utils/ApiError.ts b/src/utils/ApiError.ts index 6940ee0..b4df924 100644 --- a/src/utils/ApiError.ts +++ b/src/utils/ApiError.ts @@ -2,7 +2,7 @@ import type { Dispatcher } from "undici"; const tips = { 401: "You need a token for this endpoint", - 403: "You don't have access to this endpoint" + 403: "You don't have access to this endpoint", }; /** API Error */ diff --git a/tests/Api.test.ts b/tests/Api.test.ts index fbdcdb8..f163b42 100644 --- a/tests/Api.test.ts +++ b/tests/Api.test.ts @@ -1,70 +1,68 @@ -import { Api } from "../src/index"; -import ApiError from "../src/utils/ApiError"; -import { BOT, BOT_STATS, VOTES } from "./mocks/data"; +import { Api } from '../src/index'; +import ApiError from '../src/utils/ApiError'; +import { BOT, BOT_STATS, VOTES } from './mocks/data'; /* mock token */ -const client = new Api( - ".eyJpZCI6IjEwMjY1MjU1NjgzNDQyNjQ3MjQiLCJib3QiOnRydWV9." -); +const client = new Api('.eyJpZCI6IjEwMjY1MjU1NjgzNDQyNjQ3MjQiLCJib3QiOnRydWV9.'); -describe("API postStats test", () => { - it("postStats without server count should throw error", async () => { - await expect(client.postStats({ shardCount: 0 })).rejects.toThrow(Error); - }); +describe('API postStats test', () => { + it('postStats without server count should throw error', async () => { + await expect(client.postStats({ shardCount: 0 })).rejects.toThrow(Error); + }); - it("postStats with invalid negative server count should throw error", () => { - expect(client.postStats({ serverCount: -1 })).rejects.toThrow(Error); - }); + it('postStats with invalid negative server count should throw error', () => { + expect(client.postStats({ serverCount: -1 })).rejects.toThrow(Error); + }); - it("postStats should return 200", async () => { - await expect(client.postStats({ serverCount: 1 })).resolves.toBeInstanceOf( - Object - ); - }); + it('postStats should return 200', async () => { + await expect(client.postStats({ serverCount: 1 })).resolves.toBeInstanceOf( + Object + ); + }); }); -describe("API getStats test", () => { - it("getStats should return 200 when bot is found", async () => { - expect(client.getStats("1")).resolves.toStrictEqual({ - serverCount: BOT_STATS.server_count, - shardCount: BOT_STATS.shard_count, - shards: BOT_STATS.shards +describe('API getStats test', () => { + it('getStats should return 200 when bot is found', async () => { + expect(client.getStats('1')).resolves.toStrictEqual({ + serverCount: BOT_STATS.server_count, + shardCount: BOT_STATS.shard_count, + shards: BOT_STATS.shards + }); }); - }); }); -describe("API getBot test", () => { - it("getBot should return 404 when bot is not found", () => { - expect(client.getBot("0")).rejects.toThrow(ApiError); - }); +describe('API getBot test', () => { + it('getBot should return 404 when bot is not found', () => { + expect(client.getBot('0')).rejects.toThrow(ApiError); + }); - it("getBot should return 200 when bot is found", async () => { - expect(client.getBot("1")).resolves.toStrictEqual(BOT); - }); + it('getBot should return 200 when bot is found', async () => { + expect(client.getBot('1')).resolves.toStrictEqual(BOT); + }); - it("getBot should throw when no id is provided", () => { - expect(client.getBot("")).rejects.toThrow(Error); - }); + it('getBot should throw when no id is provided', () => { + expect(client.getBot('')).rejects.toThrow(Error); + }); }); -describe("API getVotes test", () => { - it("getVotes should return 200 when token is provided", () => { - expect(client.getVotes()).resolves.toEqual(VOTES); - }); +describe('API getVotes test', () => { + it('getVotes should return 200 when token is provided', () => { + expect(client.getVotes()).resolves.toEqual(VOTES); + }); }); -describe("API hasVoted test", () => { - it("hasVoted should return 200 when token is provided", () => { - expect(client.hasVoted("1")).resolves.toBe(true); - }); +describe('API hasVoted test', () => { + it('hasVoted should return 200 when token is provided', () => { + expect(client.hasVoted('1')).resolves.toBe(true); + }); - it("hasVoted should throw error when no id is provided", () => { - expect(client.hasVoted("")).rejects.toThrow(Error); - }); + it('hasVoted should throw error when no id is provided', () => { + expect(client.hasVoted('')).rejects.toThrow(Error); + }); }); -describe("API isWeekend tests", () => { - it("isWeekend should return true", async () => { - expect(client.isWeekend()).resolves.toBe(true); - }); +describe('API isWeekend tests', () => { + it('isWeekend should return true', async () => { + expect(client.isWeekend()).resolves.toBe(true); + }); }); diff --git a/tests/jest.setup.ts b/tests/jest.setup.ts index c934abd..7d4db81 100644 --- a/tests/jest.setup.ts +++ b/tests/jest.setup.ts @@ -1,86 +1,67 @@ -import { MockAgent, setGlobalDispatcher } from "undici"; -import { MockInterceptor } from "undici/types/mock-interceptor"; -import { endpoints } from "./mocks/endpoints"; +import { MockAgent, setGlobalDispatcher } from 'undici'; +import { MockInterceptor } from 'undici/types/mock-interceptor'; +import { endpoints } from './mocks/endpoints'; interface IOptions { - pattern: string; - requireAuth?: boolean; - validate?: (request: MockInterceptor.MockResponseCallbackOptions) => void; + pattern: string; + requireAuth?: boolean; + validate?: (request: MockInterceptor.MockResponseCallbackOptions) => void; } export const getIdInPath = (pattern: string, url: string) => { - const regex = new RegExp(`^${pattern.replace(/:[^/]+/g, "([^/]+)")}$`); - const match = url.match(regex); + const regex = new RegExp(`^${pattern.replace(/:[^/]+/g, '([^/]+)')}$`); + const match = url.match(regex); - return match ? match[1] : null; + return match ? match[1] : null; }; export const isMatchingPath = (pattern: string, url: string) => { - // Remove query params - url = url.split("?")[0]; + // Remove query params + url = url.split("?")[0]; - if (pattern === url) { - return true; - } + if (pattern === url) { + return true; + } - // Check if there is an exact match - if (endpoints.some(({ pattern }) => pattern === url)) { - return false; - } + // Check if there is an exact match + if (endpoints.some(({ pattern }) => pattern === url)) { + return false; + } - return getIdInPath(pattern, url) !== null; + return getIdInPath(pattern, url) !== null; }; beforeEach(() => { - const mockAgent = new MockAgent(); - mockAgent.disableNetConnect(); - const client = mockAgent.get("https://top.gg"); + const mockAgent = new MockAgent(); + mockAgent.disableNetConnect(); + const client = mockAgent.get('https://top.gg'); - const generateResponse = ( - request: MockInterceptor.MockResponseCallbackOptions, - statusCode: number, - data: any, - headers = {}, - options: IOptions - ) => { - const error = options.validate?.(request); - if (error) return error; + const generateResponse = (request: MockInterceptor.MockResponseCallbackOptions, statusCode: number, data: any, headers = {}, options: IOptions) => { + const error = options.validate?.(request); + if (error) return error; - return { - statusCode, - data: JSON.stringify(data), - responseOptions: { - headers: { "content-type": "application/json", ...headers } - } - }; - }; + return { + statusCode, + data: JSON.stringify(data), + responseOptions: { + headers: { 'content-type': 'application/json', ...headers }, + } + } + } - endpoints.forEach(({ pattern, method, data, requireAuth, validate }) => { - client - .intercept({ - path: (path) => isMatchingPath(pattern, path), - method - }) - .reply((request) => { - return generateResponse( - request, - 200, - data, - {}, - { pattern, requireAuth, validate } - ); - }); - }); - - client - .intercept({ - path: (path) => - !endpoints.some(({ pattern }) => isMatchingPath(pattern, path)), - method: (_) => true + endpoints.forEach(({ pattern, method, data, requireAuth, validate }) => { + client.intercept({ + path: (path) => isMatchingPath(pattern, path), + method, + }).reply((request) => {return generateResponse(request, 200, data, {}, { pattern, requireAuth, validate })}); + }) + + client.intercept({ + path: (path) => !endpoints.some(({ pattern }) => isMatchingPath(pattern, path)), + method: (_) => true, + }).reply((request) => { + throw Error(`No endpoint found for ${request.method} ${request.path}`) }) - .reply((request) => { - throw Error(`No endpoint found for ${request.method} ${request.path}`); - }); - setGlobalDispatcher(mockAgent); -}); + setGlobalDispatcher(mockAgent); +}); \ No newline at end of file diff --git a/tests/mocks/data.ts b/tests/mocks/data.ts index 504eec7..65ec229 100644 --- a/tests/mocks/data.ts +++ b/tests/mocks/data.ts @@ -10,8 +10,7 @@ export const BOT = { shortdesc: "API access for Top.gg Library Developers", prefix: "/", clientid: "1026525568344264724", - avatar: - "https://cdn.discordapp.com/avatars/1026525568344264724/cd70e62e41f691f1c05c8455d8c31e23.png", + avatar: "https://cdn.discordapp.com/avatars/1026525568344264724/cd70e62e41f691f1c05c8455d8c31e23.png", id: "1026525568344264724", username: "Top.gg Lib Dev API Access", date: "2022-10-03T16:08:55.000Z", @@ -21,7 +20,7 @@ export const BOT = { owners: ["491002268401926145"], tags: ["api", "library", "topgg"], reviews: { averageScore: 5, count: 2 } -}; +} // https://docs.top.gg/api/bot/#search-bots export const BOTS = { @@ -29,36 +28,31 @@ export const BOTS = { offset: 0, count: 1, total: 1, - results: [BOT] -}; + results: [BOT], +} // https://docs.top.gg/api/bot/#last-1000-votes export const VOTES = [ { username: "Xetera", id: "140862798832861184", - avatar: - "https://cdn.discordapp.com/avatars/1026525568344264724/cd70e62e41f691f1c05c8455d8c31e23.png" + avatar: "https://cdn.discordapp.com/avatars/1026525568344264724/cd70e62e41f691f1c05c8455d8c31e23.png" } -]; +] // https://docs.top.gg/api/bot/#bot-stats export const BOT_STATS = { server_count: 0, shards: [], shard_count: null -}; +} // https://docs.top.gg/api/bot/#individual-user-vote export const USER_VOTE = { voted: 1 -}; - -export const USER_VOTE_CHECK = { - voted: 1 -}; +} // Undocumented 😢 export const WEEKEND = { is_weekend: true -}; +} diff --git a/tests/mocks/endpoints.ts b/tests/mocks/endpoints.ts index 9f0bf2c..ce1fca9 100644 --- a/tests/mocks/endpoints.ts +++ b/tests/mocks/endpoints.ts @@ -1,67 +1,53 @@ -import { MockInterceptor } from "undici/types/mock-interceptor"; -import { - BOT, - BOTS, - BOT_STATS, - USER_VOTE, - USER_VOTE_CHECK, - VOTES, - WEEKEND -} from "./data"; -import { getIdInPath } from "../jest.setup"; +import { MockInterceptor } from 'undici/types/mock-interceptor'; +import { BOT, BOTS, BOT_STATS, USER_VOTE, VOTES, WEEKEND } from './data'; +import { getIdInPath } from '../jest.setup'; export const endpoints = [ - { - pattern: "/api/bots", - method: "GET", - data: BOTS, - requireAuth: true - }, - { - pattern: "/api/bots/:bot_id", - method: "GET", - data: BOT, - requireAuth: true, - validate: (request: MockInterceptor.MockResponseCallbackOptions) => { - const bot_id = getIdInPath("/api/bots/:bot_id", request.path); - if (Number(bot_id) === 0) return { statusCode: 404 }; - return null; + { + pattern: '/api/bots', + method: 'GET', + data: BOTS, + requireAuth: true + }, + { + pattern: '/api/bots/:bot_id', + method: 'GET', + data: BOT, + requireAuth: true, + validate: (request: MockInterceptor.MockResponseCallbackOptions) => { + const bot_id = getIdInPath('/api/bots/:bot_id', request.path); + if (Number(bot_id) === 0) return { statusCode: 404 }; + return null; + } + }, + { + pattern: '/api/bots/votes', + method: 'GET', + data: VOTES, + requireAuth: true + }, + { + pattern: '/api/bots/check', + method: 'GET', + data: USER_VOTE, + requireAuth: true + }, + { + pattern: '/api/bots/stats', + method: 'GET', + data: BOT_STATS, + requireAuth: true + }, + { + pattern: '/api/bots/stats', + method: 'POST', + data: {}, + requireAuth: true + }, + { + pattern: '/api/weekend', + method: 'GET', + data: WEEKEND, + requireAuth: true } - }, - { - pattern: "/api/bots/votes", - method: "GET", - data: VOTES, - requireAuth: true - }, - { - pattern: "/api/bots/check", - method: "GET", - data: USER_VOTE, - requireAuth: true - }, - { - pattern: "/api/bots/stats", - method: "GET", - data: BOT_STATS, - requireAuth: true - }, - { - pattern: "/api/bots/stats", - method: "POST", - data: {}, - requireAuth: true - }, - { - pattern: "/api/bots/check", - method: "GET", - data: USER_VOTE_CHECK, - requireAuth: true - }, - { - pattern: "/api/weekend", - method: "GET", - data: WEEKEND, - requireAuth: true - } -]; +] \ No newline at end of file