1
0
Fork 0

more tests

This commit is contained in:
Sean Sube 2022-12-19 22:15:15 -06:00
parent 09fa094c32
commit 61b32a5981
33 changed files with 486 additions and 45 deletions

View File

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [conan-discord](./conan-discord.md) &gt; [Bot](./conan-discord.bot.md) &gt; [commands](./conan-discord.bot.commands.md)
## Bot.commands property
<b>Signature:</b>
```typescript
commands: ReadonlyArray<Command>;
```

View File

@ -0,0 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [conan-discord](./conan-discord.md) &gt; [Bot](./conan-discord.bot.md) &gt; [connect](./conan-discord.bot.connect.md)
## Bot.connect() method
<b>Signature:</b>
```typescript
connect(): Promise<boolean>;
```
<b>Returns:</b>
Promise&lt;boolean&gt;

View File

@ -0,0 +1,22 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [conan-discord](./conan-discord.md) &gt; [Bot](./conan-discord.bot.md) &gt; [getUser](./conan-discord.bot.getuser.md)
## Bot.getUser() method
<b>Signature:</b>
```typescript
getUser(ident: string): Promise<Author | undefined>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| ident | string | |
<b>Returns:</b>
Promise&lt;[Author](./conan-discord.author.md) \| undefined&gt;

View File

@ -12,9 +12,17 @@ Necessary methods for a bot.
export interface Bot
```
## Properties
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [commands](./conan-discord.bot.commands.md) | | ReadonlyArray&lt;[Command](./conan-discord.command.md)<!-- -->&gt; | |
## Methods
| Method | Description |
| --- | --- |
| [connect()](./conan-discord.bot.connect.md) | |
| [destroy()](./conan-discord.bot.destroy.md) | |
| [getUser(ident)](./conan-discord.bot.getuser.md) | |

View File

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [conan-discord](./conan-discord.md) &gt; [BotContext](./conan-discord.botcontext.md) &gt; [args](./conan-discord.botcontext.args.md)
## BotContext.args property
<b>Signature:</b>
```typescript
args: ParsedArgs;
```

View File

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [conan-discord](./conan-discord.md) &gt; [BotContext](./conan-discord.botcontext.md) &gt; [data](./conan-discord.botcontext.data.md)
## BotContext.data property
<b>Signature:</b>
```typescript
data: DataLayer;
```

View File

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [conan-discord](./conan-discord.md) &gt; [BotContext](./conan-discord.botcontext.md) &gt; [logger](./conan-discord.botcontext.logger.md)
## BotContext.logger property
<b>Signature:</b>
```typescript
logger: Logger;
```

View File

@ -0,0 +1,23 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [conan-discord](./conan-discord.md) &gt; [BotContext](./conan-discord.botcontext.md)
## BotContext interface
Context passed to a [Bot](./conan-discord.bot.md) when it is created.
<b>Signature:</b>
```typescript
export interface BotContext
```
## Properties
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [args](./conan-discord.botcontext.args.md) | | [ParsedArgs](./conan-discord.parsedargs.md) | |
| [data](./conan-discord.botcontext.data.md) | | DataLayer | |
| [logger](./conan-discord.botcontext.logger.md) | | Logger | |
| [rcon](./conan-discord.botcontext.rcon.md) | | [RconClient](./conan-discord.rconclient.md) | |

View File

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [conan-discord](./conan-discord.md) &gt; [BotContext](./conan-discord.botcontext.md) &gt; [rcon](./conan-discord.botcontext.rcon.md)
## BotContext.rcon property
<b>Signature:</b>
```typescript
rcon: RconClient;
```

View File

@ -11,5 +11,5 @@ Constructor for a Bot.
```typescript
export type BotCtor = (context: BotContext, commands: ReadonlyArray<Command>) => Promise<Bot>;
```
<b>References:</b> [Command](./conan-discord.command.md)<!-- -->, [Bot](./conan-discord.bot.md)
<b>References:</b> [BotContext](./conan-discord.botcontext.md)<!-- -->, [Command](./conan-discord.command.md)<!-- -->, [Bot](./conan-discord.bot.md)

View File

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [conan-discord](./conan-discord.md) &gt; [CommandContext](./conan-discord.commandcontext.md) &gt; [bot](./conan-discord.commandcontext.bot.md)
## CommandContext.bot property
<b>Signature:</b>
```typescript
bot: Bot;
```

View File

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [conan-discord](./conan-discord.md) &gt; [CommandContext](./conan-discord.commandcontext.md) &gt; [data](./conan-discord.commandcontext.data.md)
## CommandContext.data property
<b>Signature:</b>
```typescript
data: DataLayer;
```

View File

@ -17,7 +17,9 @@ export interface CommandContext
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [args](./conan-discord.commandcontext.args.md) | | [ParsedArgs](./conan-discord.parsedargs.md) | |
| [bot](./conan-discord.commandcontext.bot.md) | | [Bot](./conan-discord.bot.md) | |
| [command](./conan-discord.commandcontext.command.md) | | [Command](./conan-discord.command.md) | |
| [data](./conan-discord.commandcontext.data.md) | | DataLayer | |
| [logger](./conan-discord.commandcontext.logger.md) | | Logger | |
| [rcon](./conan-discord.commandcontext.rcon.md) | | [RconClient](./conan-discord.rconclient.md) | |

View File

@ -11,15 +11,16 @@ Requires [ParsedArgs.discordToken](./conan-discord.parsedargs.discordtoken.md) t
<b>Signature:</b>
```typescript
export declare function discordConnect(botContext: BotContext, commands: ReadonlyArray<Command>): Promise<DiscordBot>;
export declare function discordConnect(botContext: BotContext, commands: ReadonlyArray<Command>, clientCtor?: typeof Client): Promise<DiscordBot>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| botContext | BotContext | |
| botContext | [BotContext](./conan-discord.botcontext.md) | |
| commands | ReadonlyArray&lt;[Command](./conan-discord.command.md)<!-- -->&gt; | |
| clientCtor | typeof Client | <i>(Optional)</i> |
<b>Returns:</b>

View File

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [conan-discord](./conan-discord.md) &gt; [LOCAL\_DISCRIMINATOR](./conan-discord.local_discriminator.md)
## LOCAL\_DISCRIMINATOR variable
Discriminator for local authors.
<b>Signature:</b>
```typescript
LOCAL_DISCRIMINATOR = "local"
```

View File

@ -11,12 +11,12 @@
| [authorName(author)](./conan-discord.authorname.md) | Friendly name for an Author. |
| [checkAuthor(author, admins, roles)](./conan-discord.checkauthor.md) | Check if an author is allowed to execute a command. |
| [commandName(command, prefix)](./conan-discord.commandname.md) | Wrapper for append with a default prefix. |
| [discordConnect(botContext, commands)](./conan-discord.discordconnect.md) | <p>Connect to a Discord server using a bot token.</p><p>Requires [ParsedArgs.discordToken](./conan-discord.parsedargs.discordtoken.md) to be set in <code>args</code>.</p> |
| [discordConnect(botContext, commands, clientCtor)](./conan-discord.discordconnect.md) | <p>Connect to a Discord server using a bot token.</p><p>Requires [ParsedArgs.discordToken](./conan-discord.parsedargs.discordtoken.md) to be set in <code>args</code>.</p> |
| [matchCommands(msg, commands, prefix)](./conan-discord.matchcommands.md) | Match command names against a message body. |
| [parseArgs(argv)](./conan-discord.parseargs.md) | Parse CLI options and environment variables. |
| [parseBody(msg, command, prefix, options)](./conan-discord.parsebody.md) | Remove the command name from a message, parse any CLI args, and return them with the remainder of the message body. |
| [parseBody(msg, command, prefix, options, positionals)](./conan-discord.parsebody.md) | Remove the command name from a message, parse any CLI args, and return them with the remainder of the message body. |
| [rconConnect(args, logger)](./conan-discord.rconconnect.md) | Create (and monkey-patch) an RCON client. |
| [readlineConnect(botContext, commands)](./conan-discord.readlineconnect.md) | <p>Start the readline bot interface.</p><p>This reads input from stdin and send replies through the logging system, and is meant for interactive debugging.</p> |
| [readlineConnect(botContext, commands, readlineFactory)](./conan-discord.readlineconnect.md) | <p>Start the readline bot interface.</p><p>This reads input from stdin and send replies through the logging system, and is meant for interactive debugging.</p> |
| [removeCommandName(msg, command, prefix)](./conan-discord.removecommandname.md) | Parse and remove the command name from a message body. |
| [sendWithRetry(client, command, logger, retries)](./conan-discord.sendwithretry.md) | Send an RCON command and attempt to reconnect and retry if it fails. |
@ -26,6 +26,7 @@
| --- | --- |
| [Author](./conan-discord.author.md) | Message author. |
| [Bot](./conan-discord.bot.md) | Necessary methods for a bot. |
| [BotContext](./conan-discord.botcontext.md) | Context passed to a [Bot](./conan-discord.bot.md) when it is created. |
| [Command](./conan-discord.command.md) | Bot command with optional authn/authz. |
| [CommandContext](./conan-discord.commandcontext.md) | Context passed to [Command.run](./conan-discord.command.run.md) when invoked. |
| [DiscordBot](./conan-discord.discordbot.md) | Discord-only methods. |
@ -35,6 +36,13 @@
| [ReadlineBot](./conan-discord.readlinebot.md) | Readline-only methods. |
| [Roles](./conan-discord.roles.md) | Roles who should be able to execute a command. |
## Variables
| Variable | Description |
| --- | --- |
| [LOCAL\_DISCRIMINATOR](./conan-discord.local_discriminator.md) | Discriminator for local authors. |
| [ROLE\_ADMIN\_ONLY](./conan-discord.role_admin_only.md) | Role for admin-only commands. |
## Type Aliases
| Type Alias | Description |

View File

@ -9,7 +9,9 @@ Remove the command name from a message, parse any CLI args, and return them with
<b>Signature:</b>
```typescript
export declare function parseBody<ArgT, OptionT extends BodyOptions<ArgT> = BodyOptions<ArgT>>(msg: string, command: Command, prefix: string, options: OptionT): Promise<[string, ArgT]>;
export declare function parseBody<ArgT, OptionT extends BodyOptions<ArgT> = BodyOptions<ArgT>, ArgVT = ArgT & {
_: Array<string>;
}>(msg: string, command: Command, prefix: string, options: OptionT, positionals?: Array<[StringsOnly<keyof ArgT>, PositionalOptions]>): Promise<[string, ArgVT]>;
```
## Parameters
@ -20,8 +22,9 @@ export declare function parseBody<ArgT, OptionT extends BodyOptions<ArgT> = Body
| command | [Command](./conan-discord.command.md) | |
| prefix | string | |
| options | OptionT | |
| positionals | Array&lt;\[[StringsOnly](./conan-discord.stringsonly.md)<!-- -->&lt;keyof ArgT&gt;, PositionalOptions\]&gt; | <i>(Optional)</i> |
<b>Returns:</b>
Promise&lt;\[string, ArgT\]&gt;
Promise&lt;\[string, ArgVT\]&gt;

View File

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [conan-discord](./conan-discord.md) &gt; [ParsedArgs](./conan-discord.parsedargs.md) &gt; [data](./conan-discord.parsedargs.data.md)
## ParsedArgs.data property
<b>Signature:</b>
```typescript
data: string;
```

View File

@ -19,8 +19,10 @@ export interface ParsedArgs
| [bots](./conan-discord.parsedargs.bots.md) | | Array&lt;string&gt; | |
| [commandAdmins](./conan-discord.parsedargs.commandadmins.md) | | Array&lt;string&gt; | |
| [commandPrefix](./conan-discord.parsedargs.commandprefix.md) | | string | |
| [data](./conan-discord.parsedargs.data.md) | | string | |
| [discordToken](./conan-discord.parsedargs.discordtoken.md) | | string | |
| [rconHost](./conan-discord.parsedargs.rconhost.md) | | string | |
| [rconPassword](./conan-discord.parsedargs.rconpassword.md) | | string | |
| [rconPort](./conan-discord.parsedargs.rconport.md) | | number | |
| [sqliteDatabase](./conan-discord.parsedargs.sqlitedatabase.md) | | string | |

View File

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [conan-discord](./conan-discord.md) &gt; [ParsedArgs](./conan-discord.parsedargs.md) &gt; [sqliteDatabase](./conan-discord.parsedargs.sqlitedatabase.md)
## ParsedArgs.sqliteDatabase property
<b>Signature:</b>
```typescript
sqliteDatabase: string;
```

View File

@ -9,8 +9,9 @@ Readline-only methods.
<b>Signature:</b>
```typescript
export interface ReadlineBot
export interface ReadlineBot extends Bot
```
<b>Extends:</b> [Bot](./conan-discord.bot.md)
## Methods

View File

@ -11,15 +11,16 @@ This reads input from stdin and send replies through the logging system, and is
<b>Signature:</b>
```typescript
export declare function readlineConnect(botContext: BotContext, commands: ReadonlyArray<Command>): Promise<ReadlineBot>;
export declare function readlineConnect(botContext: BotContext, commands: ReadonlyArray<Command>, readlineFactory?: typeof createInterface): Promise<ReadlineBot>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| botContext | BotContext | |
| botContext | [BotContext](./conan-discord.botcontext.md) | |
| commands | ReadonlyArray&lt;[Command](./conan-discord.command.md)<!-- -->&gt; | |
| readlineFactory | typeof createInterface | <i>(Optional)</i> |
<b>Returns:</b>

View File

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [conan-discord](./conan-discord.md) &gt; [ROLE\_ADMIN\_ONLY](./conan-discord.role_admin_only.md)
## ROLE\_ADMIN\_ONLY variable
Role for admin-only commands.
<b>Signature:</b>
```typescript
ROLE_ADMIN_ONLY: Roles
```

View File

@ -4,7 +4,12 @@
```ts
import Logger from 'bunyan';
/// <reference types="node" />
import { Client } from 'discord.js';
import { createInterface } from 'node:readline/promises';
import { Logger } from 'noicejs';
import { PositionalOptions } from 'yargs';
import yargs from 'yargs';
// @public
@ -25,12 +30,30 @@ export type BodyOptions<T> = {
// @public
export interface Bot {
// (undocumented)
commands: ReadonlyArray<Command>;
// (undocumented)
connect(): Promise<boolean>;
// (undocumented)
destroy(): void;
// (undocumented)
getUser(ident: string): Promise<Author | undefined>;
}
// @public
export interface BotContext {
// (undocumented)
args: ParsedArgs;
// Warning: (ae-forgotten-export) The symbol "DataLayer" needs to be exported by the entry point index.d.ts
//
// (undocumented)
data: DataLayer;
// (undocumented)
logger: Logger;
// (undocumented)
rcon: RconClient;
}
// Warning: (ae-forgotten-export) The symbol "BotContext" needs to be exported by the entry point index.d.ts
//
// @public
export type BotCtor = (context: BotContext, commands: ReadonlyArray<Command>) => Promise<Bot>;
@ -54,8 +77,12 @@ export interface CommandContext {
// (undocumented)
args: ParsedArgs;
// (undocumented)
bot: Bot;
// (undocumented)
command: Command;
// (undocumented)
data: DataLayer;
// (undocumented)
logger: Logger;
// (undocumented)
rcon: RconClient;
@ -71,7 +98,10 @@ export interface DiscordBot extends Bot {
}
// @public
export function discordConnect(botContext: BotContext, commands: ReadonlyArray<Command>): Promise<DiscordBot>;
export function discordConnect(botContext: BotContext, commands: ReadonlyArray<Command>, clientCtor?: typeof Client): Promise<DiscordBot>;
// @public
export const LOCAL_DISCRIMINATOR = "local";
// @public
export function matchCommands(msg: string, commands: ReadonlyArray<Command>, prefix: string): ReadonlyArray<Command>;
@ -90,7 +120,9 @@ export interface Message {
export function parseArgs(argv: Array<string>): Promise<ParsedArgs>;
// @public
export function parseBody<ArgT, OptionT extends BodyOptions<ArgT> = BodyOptions<ArgT>>(msg: string, command: Command, prefix: string, options: OptionT): Promise<[string, ArgT]>;
export function parseBody<ArgT, OptionT extends BodyOptions<ArgT> = BodyOptions<ArgT>, ArgVT = ArgT & {
_: Array<string>;
}>(msg: string, command: Command, prefix: string, options: OptionT, positionals?: Array<[StringsOnly<keyof ArgT>, PositionalOptions]>): Promise<[string, ArgVT]>;
// @public
export interface ParsedArgs {
@ -101,6 +133,8 @@ export interface ParsedArgs {
// (undocumented)
commandPrefix: string;
// (undocumented)
data: string;
// (undocumented)
discordToken: string;
// (undocumented)
rconHost: string;
@ -108,6 +142,8 @@ export interface ParsedArgs {
rconPassword: string;
// (undocumented)
rconPort: number;
// (undocumented)
sqliteDatabase: string;
}
// @public
@ -124,17 +160,20 @@ export interface RconClient {
export function rconConnect(args: ParsedArgs, logger: Logger): Promise<RconClient>;
// @public
export interface ReadlineBot {
export interface ReadlineBot extends Bot {
// (undocumented)
destroy(): void;
}
// @public
export function readlineConnect(botContext: BotContext, commands: ReadonlyArray<Command>): Promise<ReadlineBot>;
export function readlineConnect(botContext: BotContext, commands: ReadonlyArray<Command>, readlineFactory?: typeof createInterface): Promise<ReadlineBot>;
// @public
export function removeCommandName(msg: string, command: Command, prefix: string): string;
// @public
export const ROLE_ADMIN_ONLY: Roles;
// @public
export interface Roles {
admin: boolean;

View File

@ -1,9 +1,9 @@
import { doesExist, mustExist } from '@apextoaster/js-utils';
import { Client, Events, GatewayIntentBits, Message, Partials } from 'discord.js';
import { Client, Events, FormattingPatterns, GatewayIntentBits, Message, Partials } from 'discord.js';
import { Author, Command, CommandContext } from '../command/index.js';
import { catchAndLog } from '../utils/async.js';
import { matchCommands } from '../utils/command.js';
import { authorName, matchCommands } from '../utils/command.js';
import { Bot, BotContext } from './index.js';
/**
@ -22,10 +22,14 @@ export interface DiscordBot extends Bot {
*
* @public
*/
export async function discordConnect(botContext: BotContext, commands: ReadonlyArray<Command>): Promise<DiscordBot> {
export async function discordConnect(
botContext: BotContext,
commands: ReadonlyArray<Command>,
clientCtor: typeof Client = Client
): Promise<DiscordBot> {
const { args, logger } = botContext;
const client = new Client({
const client = new clientCtor({
intents: [
GatewayIntentBits.DirectMessages,
GatewayIntentBits.DirectMessageReactions,
@ -45,10 +49,11 @@ export async function discordConnect(botContext: BotContext, commands: ReadonlyA
}, 'logged in and ready');
});
async function onMessage(msg: Message) {
const name = `${msg.author.username}#${msg.author.discriminator}`;
async function onMessage(message: Message) {
const { author, content } = message;
const name = authorName(author);
logger.debug({
text: msg.content,
text: content,
user: name,
}, 'message created');
@ -57,7 +62,7 @@ export async function discordConnect(botContext: BotContext, commands: ReadonlyA
return;
}
const matchingCommands = matchCommands(msg.content, commands, args.commandPrefix);
const matchingCommands = matchCommands(content, commands, args.commandPrefix);
for (const command of matchingCommands) {
logger.debug({
command: command.name,
@ -70,16 +75,16 @@ export async function discordConnect(botContext: BotContext, commands: ReadonlyA
};
if (doesExist(command.auth)) {
const allowed = await command.auth(msg.author, context);
logger.debug({ allowed, author: msg.author, command }, 'command auth check');
const allowed = await command.auth(author, context);
logger.debug({ allowed, author, command }, 'command auth check');
if (allowed === false) {
logger.warn({ author: msg.author, command }, 'author attempted to use unauthorized command');
logger.warn({ author, command }, 'author attempted to use unauthorized command');
continue;
}
}
command.run(msg, context).catch((err) => {
command.run(message, context).catch((err) => {
logger.error(err, 'error executing command');
});
}
@ -99,7 +104,8 @@ export async function discordConnect(botContext: BotContext, commands: ReadonlyA
client.destroy();
},
async getUser(ident: string): Promise<Author> {
const match = /<@(\d+)>/.exec(ident);
// const match = /<@(\d+)>/.exec(ident);
const match = FormattingPatterns.User.exec(ident);
if (match) {
const [_full, snowflake] = Array.from(match);

View File

@ -30,10 +30,10 @@ export const READLINE_AUTHOR: Author = {
*
* @public
*/
export async function readlineConnect(botContext: BotContext, commands: ReadonlyArray<Command>): Promise<ReadlineBot> {
export async function readlineConnect(botContext: BotContext, commands: ReadonlyArray<Command>, readlineFactory: typeof createInterface = createInterface): Promise<ReadlineBot> {
const { args, logger } = botContext;
const rl = createInterface({ input, output });
const rl = readlineFactory({ input, output });
async function onLine(line: string) {
logger.debug({ line }, 'message received');

View File

@ -23,6 +23,11 @@ export interface Roles {
other: boolean;
}
/**
* Discriminator for local authors.
*
* @public
*/
export const LOCAL_DISCRIMINATOR = 'local';
/**

41
test/bot/TestDiscord.ts Normal file
View File

@ -0,0 +1,41 @@
import { expect } from 'chai';
import { Client, ClientOptions } from 'discord.js';
import { NullLogger } from 'noicejs';
import { createStubInstance, stub } from 'sinon';
import { parseArgs } from '../../src/args.js';
import { discordConnect } from '../../src/bot/discord.js';
import { MemoryDataLayer } from '../../src/data/memory.js';
import { mockRcon } from '../helpers.js';
describe('discord bot', () => {
it('should watch for the client ready event', async () => {
const args = await parseArgs(['--discordToken=""', '--rconPassword=""']);
const rcon = mockRcon();
const destroyMock = stub<[], void>();
class MockDiscord<Ready extends boolean> extends Client<Ready> {
constructor(options: ClientOptions) {
super(options);
return createStubInstance<Client<Ready>>(Client, {
destroy: destroyMock,
on: stub(),
once: stub(),
});
}
}
const bot = await discordConnect({
args,
data: new MemoryDataLayer(args),
logger: NullLogger.global,
rcon,
}, [], MockDiscord);
bot.destroy();
expect(destroyMock).to.have.callCount(1);
});
it('should watch for messages and match commands');
});

View File

@ -1,19 +1,28 @@
import { expect } from 'chai';
import { NullLogger } from 'noicejs';
import { match, mock } from 'sinon';
import { parseArgs } from '../../src/args.js';
import { help } from '../../src/command/help.js';
import { CommandContext, Message } from '../../src/command/index.js';
import { MemoryDataLayer } from '../../src/data/memory.js';
import { mockBot, mockRcon } from '../helpers.js';
describe('ping command', () => {
it('should reply with a pong', async () => {
const args = await parseArgs([
'--discordToken=""',
'--rconPassword=""',
'--commandPrefix="!"',
]);
const ctx: CommandContext = {
args: {
commandPrefix: '!',
},
bot: {
commands: [help],
},
} as unknown as CommandContext;
args,
bot: mockBot([help], {}),
command: help,
data: new MemoryDataLayer(args),
logger: NullLogger.global,
rcon: mockRcon(),
};
const replyMock = mock().named('Message.reply').resolves();
const msg: Message = {
author: {
@ -29,4 +38,3 @@ describe('ping command', () => {
expect(replyMock).to.have.callCount(1).and.been.calledWith(match(`!${help.name}: ${help.desc}`));
});
});

10
test/command/TestIndex.ts Normal file
View File

@ -0,0 +1,10 @@
import { expect } from 'chai';
import { COMMANDS } from '../../src/command/index.js';
// so smelly
describe('index file', () => {
it('should have a list of commands', async () => {
expect(COMMANDS.length).to.be.greaterThan(0);
});
});

View File

@ -0,0 +1,60 @@
import { expect } from 'chai';
import { NullLogger } from 'noicejs';
import { mock } from 'sinon';
import { parseArgs } from '../../src/args.js';
import { Author, CommandContext, Message } from '../../src/command/index.js';
import { karma } from '../../src/command/karma.js';
import { MemoryDataLayer } from '../../src/data/memory.js';
import { mockRcon } from '../helpers.js';
describe('karma command', () => {
it('should store karma', async () => {
const author: Author = {
discriminator: '0000',
username: 'test',
};
const args = await parseArgs([
'--discordToken=""',
'--rconPassword=""',
'--commandPrefix="!"',
]);
const ctx: CommandContext = {
args,
bot: {
commands: [],
connect: () => Promise.resolve(true),
destroy: () => { /* noop */ },
getUser: mock().atLeast(1).resolves(author),
},
command: karma,
data: new MemoryDataLayer(args),
logger: NullLogger.global,
rcon: mockRcon(),
};
const replyMock = mock().named('Message.reply').resolves();
const msg: Message = {
author,
content: '',
reply: replyMock,
};
await karma.run(msg, ctx);
// start at 0
expect(replyMock).to.have.been.calledWith('test#0000 has 0 karma');
replyMock.resetHistory();
const msg2: Message = {
author,
content: '!karma ++++',
reply: replyMock,
};
await karma.run(msg2, ctx);
// ++(+++)
expect(replyMock).to.have.been.calledWith('test#0000 has 3 karma');
});
});

28
test/helpers.ts Normal file
View File

@ -0,0 +1,28 @@
import { Bot } from '../src/bot/index.js';
import { Author, Command } from '../src/command/index.js';
import { RconClient } from '../src/utils/rcon.js';
export function mockBot(commands: Array<Command>, users: Record<string, Author>): Bot {
return {
commands,
connect: () => Promise.resolve(true),
destroy: () => { /* noop */ },
getUser: (name: string) => Promise.resolve(users[name]),
};
}
export function mockRcon(commands: Record<string, string> = {}): RconClient {
const client: RconClient = {
async connect() {
return client;
},
end() {
// noop
},
async send(command: string) {
return commands[command];
},
};
return client;
}

View File

@ -1,7 +1,7 @@
import { expect } from 'chai';
import { Command } from '../../src/command/index.js';
import { matchCommands } from '../../src/utils/command.js';
import { matchCommands, parseBody, removeCommandName } from '../../src/utils/command.js';
const TEST_COMMANDS: Array<Command> = [{
name: 'foo',
@ -30,11 +30,44 @@ describe('command utils', () => {
});
describe('remove command name helper', () => {
it('should remove the command name from the message');
it('should remove the command name from the message if it matches', async () => {
expect(removeCommandName('!foo bar', TEST_COMMANDS[0], '!')).to.equal('bar');
expect(removeCommandName('!foo bar', TEST_COMMANDS[1], '!')).to.equal('!foo bar');
});
});
describe('parse body helper', () => {
it('should parse options from the message');
it('should return the remainder of the message');
it('should parse dashed options from the message', async () => {
interface TestArgs {
count: number;
};
const [_body, args] = await parseBody<TestArgs>('!foo --count 3', TEST_COMMANDS[0], '!', {
count: {
default: 0,
type: 'number',
},
});
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
expect(args.count).to.equal(3);
});
it('should parse positional arguments from the message');
it('should return the remainder of the message', async () => {
interface TestArgs {
count: number;
};
const [body, _args] = await parseBody<TestArgs>('!foo --count 3 bar', TEST_COMMANDS[0], '!', {
count: {
default: 0,
type: 'number',
},
});
expect(body).to.equal('bar');
});
});
});