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

Added tests for API.ts #79

Merged
merged 12 commits into from
Jul 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Jest Tests

on:
push:
branches:
- master
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: '18.x'
- name: Install Dependencies
run: npm install
- name: Run Jest tests
run: npm run test
6 changes: 6 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
setupFilesAfterEnv: ['./tests/jest.setup.ts']
};
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"description": "Official Top.gg Node SDK",
"main": "./dist/index.js",
"scripts": {
"test": "jest --verbose",
"test:coverage": "jest --coverage",
"build": "tsc",
"build:ci": "npm i --include=dev && tsc",
"docs": "typedoc",
Expand All @@ -25,6 +27,7 @@
"devDependencies": {
"@top-gg/eslint-config": "^0.0.3",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.1",
"@types/node": "^18.15.5",
"@typescript-eslint/eslint-plugin": "^5.56.0",
"@typescript-eslint/parser": "^5.56.0",
Expand All @@ -33,8 +36,10 @@
"eslint-plugin-jest": "^27.2.1",
"express": "^4.18.2",
"husky": "^8.0.3",
"jest": "^29.5.0",
"lint-staged": "^13.2.0",
"prettier": "^2.8.6",
"ts-jest": "^29.1.0",
"typedoc": "^0.23.28",
"typescript": "^5.0.2"
},
Expand Down
143 changes: 143 additions & 0 deletions tests/Api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { Api } from '../src/index';
import ApiError from '../src/utils/ApiError';
import { BOT, BOT_STATS, USER, VOTES } from './mocks/data';

const is_owner = new Api('owner_token');
const not_owner = new Api('not_owner_token');
const no_token = new Api('');

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
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 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);
})
})

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);
})

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);
})
})


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);
})

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);
})

})
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);
})

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)
})
});
76 changes: 76 additions & 0 deletions tests/jest.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
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;
}

export const getIdInPath = (pattern: string, url: string) => {
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]

if (pattern === url) {
return true;
}

// Check if there is an exact match
if(endpoints.some(({ pattern }) => pattern === url)) {
return false
};

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}`)
})

setGlobalDispatcher(mockAgent);
})
82 changes: 82 additions & 0 deletions tests/mocks/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// 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": ""
}

// https://docs.top.gg/api/bot/#search-bots
export const BOTS = {
limit: 0,
offset: 0,
count: 1,
total: 1,
results: [BOT],
}

// https://docs.top.gg/api/bot/#last-1000-votes
export const VOTES = [
{
"username": "Xetera",
"id": "140862798832861184",
"avatar": "a_1241439d430def25c100dd28add2d42f"
}
]

// https://docs.top.gg/api/bot/#bot-stats
export const BOT_STATS = {
server_count: 0,
shards: ['200'],
shard_count: 1
}

// https://docs.top.gg/api/bot/#individual-user-vote
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
}

// Undocumented 😢
export const WEEKEND = {
is_weekend: true
}

Loading