import type { FunctionSpy, RestorableFunctionSpy } from 'alsatian'; import { AsyncTest, Expect, Setup, SetupFixture, SpyOn, Teardown, Test } from 'alsatian'; import type { ISlashCommandPreviewItem } from '../../../src/definition/slashcommands'; import { SlashCommandContext } from '../../../src/definition/slashcommands'; import type { AppManager } from '../../../src/server/AppManager'; import type { ProxiedApp } from '../../../src/server/ProxiedApp'; import type { AppBridges } from '../../../src/server/bridges'; import { CommandAlreadyExistsError, CommandHasAlreadyBeenTouchedError } from '../../../src/server/errors'; import type { AppApiManager, AppExternalComponentManager, AppSchedulerManager, AppVideoConfProviderManager, } from '../../../src/server/managers'; import { AppAccessorManager, AppSlashCommandManager } from '../../../src/server/managers'; import { AppSlashCommand } from '../../../src/server/managers/AppSlashCommand'; import type { UIActionButtonManager } from '../../../src/server/managers/UIActionButtonManager'; import { Room } from '../../../src/server/rooms/Room'; import type { AppLogStorage, IAppStorageItem } from '../../../src/server/storage'; import { TestsAppBridges } from '../../test-data/bridges/appBridges'; import { TestsAppLogStorage } from '../../test-data/storage/logStorage'; import { TestData } from '../../test-data/utilities'; export class AppSlashCommandManagerTestFixture { public static doThrow = false; private mockBridges: TestsAppBridges; private mockApp: ProxiedApp; private mockAccessors: AppAccessorManager; private mockManager: AppManager; private spies: Array; @SetupFixture public setupFixture() { this.mockBridges = new TestsAppBridges(); this.mockApp = TestData.getMockApp({ info: { id: 'testing', name: 'TestApp' } } as IAppStorageItem, this.mockManager); const bri = this.mockBridges; const app = this.mockApp; this.mockManager = { getBridges(): AppBridges { return bri; }, getCommandManager() { return {} as AppSlashCommandManager; }, getExternalComponentManager(): AppExternalComponentManager { return {} as AppExternalComponentManager; }, getApiManager() { return {} as AppApiManager; }, getOneById(appId: string): ProxiedApp { return appId === 'failMePlease' ? undefined : app; }, getLogStorage(): AppLogStorage { return new TestsAppLogStorage(); }, getSchedulerManager() { return {} as AppSchedulerManager; }, getUIActionButtonManager() { return {} as UIActionButtonManager; }, getVideoConfProviderManager() { return {} as AppVideoConfProviderManager; }, } as AppManager; this.mockAccessors = new AppAccessorManager(this.mockManager); const ac = this.mockAccessors; this.mockManager.getAccessorManager = function _getAccessorManager(): AppAccessorManager { return ac; }; } @Setup public setup() { this.mockBridges = new TestsAppBridges(); const bri = this.mockBridges; this.mockManager.getBridges = function _refreshedGetBridges(): AppBridges { return bri; }; this.spies = []; this.spies.push(SpyOn(this.mockBridges.getCommandBridge(), 'doDoesCommandExist')); this.spies.push(SpyOn(this.mockBridges.getCommandBridge(), 'doRegisterCommand')); this.spies.push(SpyOn(this.mockBridges.getCommandBridge(), 'doUnregisterCommand')); this.spies.push(SpyOn(this.mockBridges.getCommandBridge(), 'doEnableCommand')); this.spies.push(SpyOn(this.mockBridges.getCommandBridge(), 'doDisableCommand')); } @Teardown public teardown() { this.spies.forEach((s) => s.restore()); } @Test() public basicAppSlashCommandManager() { Expect(() => new AppSlashCommandManager({} as AppManager)).toThrow(); Expect(() => new AppSlashCommandManager(this.mockManager)).not.toThrow(); const ascm = new AppSlashCommandManager(this.mockManager); Expect((ascm as any).manager).toBe(this.mockManager); Expect((ascm as any).bridge).toBe(this.mockBridges.getCommandBridge()); Expect((ascm as any).accessors).toBe(this.mockManager.getAccessorManager()); Expect((ascm as any).providedCommands).toBeDefined(); Expect((ascm as any).providedCommands.size).toBe(0); Expect((ascm as any).modifiedCommands).toBeDefined(); Expect((ascm as any).modifiedCommands.size).toBe(0); Expect((ascm as any).touchedCommandsToApps).toBeDefined(); Expect((ascm as any).touchedCommandsToApps.size).toBe(0); Expect((ascm as any).appsTouchedCommands).toBeDefined(); Expect((ascm as any).appsTouchedCommands.size).toBe(0); } @Test() public canCommandBeTouchedBy() { const ascm = new AppSlashCommandManager(this.mockManager); Expect(ascm.canCommandBeTouchedBy('testing', 'command')).toBe(true); (ascm as any).touchedCommandsToApps.set('just-a-test', 'anotherAppId'); Expect(ascm.canCommandBeTouchedBy('testing', 'just-a-test')).toBe(false); } @Test() public isAlreadyDefined() { const ascm = new AppSlashCommandManager(this.mockManager); const reg = new Map(); reg.set('command', new AppSlashCommand(this.mockApp, TestData.getSlashCommand('command'))); Expect(ascm.isAlreadyDefined('command')).toBe(false); (ascm as any).providedCommands.set('testing', reg); Expect(ascm.isAlreadyDefined('command')).toBe(true); Expect(ascm.isAlreadyDefined('cOMMand')).toBe(true); Expect(ascm.isAlreadyDefined(' command ')).toBe(true); Expect(ascm.isAlreadyDefined('c0mmand')).toBe(false); } @Test() public setAsTouched() { const ascm = new AppSlashCommandManager(this.mockManager); Expect(() => (ascm as any).setAsTouched('testing', 'command')).not.toThrow(); Expect((ascm as any).appsTouchedCommands.has('testing')).toBe(true); Expect((ascm as any).appsTouchedCommands.get('testing') as Array).not.toBeEmpty(); Expect((ascm as any).appsTouchedCommands.get('testing').length).toBe(1); Expect((ascm as any).touchedCommandsToApps.has('command')).toBe(true); Expect((ascm as any).touchedCommandsToApps.get('command')).toBe('testing'); Expect(() => (ascm as any).setAsTouched('testing', 'command')).not.toThrow(); Expect((ascm as any).appsTouchedCommands.get('testing').length).toBe(1); } @AsyncTest() public async registerCommand() { const ascm = new AppSlashCommandManager(this.mockManager); const regInfo = new AppSlashCommand(this.mockApp, TestData.getSlashCommand('command')); await Expect(() => (ascm as any).registerCommand('testing', regInfo)).not.toThrowAsync(); Expect(this.mockBridges.getCommandBridge().doRegisterCommand).toHaveBeenCalledWith(regInfo.slashCommand, 'testing'); Expect(regInfo.isRegistered).toBe(true); Expect(regInfo.isDisabled).toBe(false); Expect(regInfo.isEnabled).toBe(true); } @AsyncTest() public async addCommand() { const cmd = TestData.getSlashCommand('my-cmd'); const ascm = new AppSlashCommandManager(this.mockManager); await Expect(async () => ascm.addCommand('testing', cmd)).not.toThrowAsync(); Expect(this.mockBridges.getCommandBridge().commands.size).toBe(1); Expect((ascm as any).providedCommands.size).toBe(1); Expect((ascm as any).touchedCommandsToApps.get('my-cmd')).toBe('testing'); Expect((ascm as any).appsTouchedCommands.get('testing').length).toBe(1); await Expect(() => ascm.addCommand('another-app', cmd)).toThrowErrorAsync( CommandHasAlreadyBeenTouchedError, 'The command "my-cmd" has already been touched by another App.', ); await Expect(() => ascm.addCommand('testing', cmd)).toThrowErrorAsync( CommandAlreadyExistsError, 'The command "my-cmd" already exists in the system.', ); await Expect(() => ascm.addCommand('failMePlease', TestData.getSlashCommand('yet-another'))).toThrowErrorAsync( Error, 'App must exist in order for a command to be added.', ); await Expect(() => ascm.addCommand('testing', TestData.getSlashCommand('another-command'))).not.toThrowAsync(); Expect((ascm as any).providedCommands.size).toBe(1); Expect((ascm as any).providedCommands.get('testing').size).toBe(2); await Expect(() => ascm.addCommand('even-another-app', TestData.getSlashCommand('it-exists'))).toThrowErrorAsync( CommandAlreadyExistsError, 'The command "it-exists" already exists in the system.', ); } @AsyncTest() public async failToModifyAnotherAppsCommand() { const ascm = new AppSlashCommandManager(this.mockManager); await ascm.addCommand('other-app', TestData.getSlashCommand('my-cmd')); await Expect(() => ascm.modifyCommand('testing', TestData.getSlashCommand('my-cmd'))).toThrowErrorAsync( CommandHasAlreadyBeenTouchedError, 'The command "my-cmd" has already been touched by another App.', ); } @AsyncTest() public async failToModifyNonExistantAppCommand() { const ascm = new AppSlashCommandManager(this.mockManager); await Expect(() => ascm.modifyCommand('failMePlease', TestData.getSlashCommand('yet-another'))).toThrowErrorAsync( Error, 'App must exist in order to modify a command.', ); } @AsyncTest() public async modifyMyCommand() { const ascm = new AppSlashCommandManager(this.mockManager); await Expect(() => ascm.modifyCommand('testing', TestData.getSlashCommand())).toThrowErrorAsync( Error, 'You must first register a command before you can modify it.', ); await ascm.addCommand('testing', TestData.getSlashCommand('the-cmd')); await Expect(() => ascm.modifyCommand('testing', TestData.getSlashCommand('the-cmd'))).not.toThrowAsync(); } @AsyncTest() public async modifySystemCommand() { const ascm = new AppSlashCommandManager(this.mockManager); await Expect(() => ascm.modifyCommand('brand-new-id', TestData.getSlashCommand('it-exists'))).not.toThrowAsync(); Expect((ascm as any).modifiedCommands.size).toBe(1); Expect((ascm as any).modifiedCommands.get('it-exists')).toBeDefined(); Expect((ascm as any).touchedCommandsToApps.get('it-exists')).toBe('brand-new-id'); } @AsyncTest() public async enableMyCommand() { const ascm = new AppSlashCommandManager(this.mockManager); await Expect(() => ascm.enableCommand('testing', 'doesnt-exist')).toThrowErrorAsync( Error, 'The command "doesnt-exist" does not exist to enable.', ); await ascm.addCommand('testing', TestData.getSlashCommand('command')); await Expect(() => ascm.enableCommand('testing', 'command')).not.toThrowAsync(); Expect((ascm as any).providedCommands.get('testing').get('command').isDisabled).toBe(false); Expect((ascm as any).providedCommands.get('testing').get('command').isEnabled).toBe(true); await ascm.addCommand('testing', TestData.getSlashCommand('another-command')); (ascm as any).providedCommands.get('testing').get('another-command').isRegistered = true; await Expect(() => ascm.enableCommand('testing', 'another-command')).not.toThrowAsync(); Expect(this.mockBridges.getCommandBridge().doDoesCommandExist).toHaveBeenCalled().exactly(3); } @AsyncTest() public async enableSystemCommand() { const ascm = new AppSlashCommandManager(this.mockManager); await Expect(() => ascm.enableCommand('testing', 'it-exists')).not.toThrowAsync(); Expect(this.mockBridges.getCommandBridge().doEnableCommand).toHaveBeenCalledWith('it-exists', 'testing').exactly(1); Expect(this.mockBridges.getCommandBridge().doDoesCommandExist).toHaveBeenCalled().exactly(1); } @AsyncTest() public async failToEnableAnotherAppsCommand() { const ascm = new AppSlashCommandManager(this.mockManager); await ascm.addCommand('another-app', TestData.getSlashCommand('command')); await Expect(() => ascm.enableCommand('my-app', 'command')).toThrowErrorAsync( CommandHasAlreadyBeenTouchedError, 'The command "command" has already been touched by another App.', ); } @AsyncTest() public async disableMyCommand() { const ascm = new AppSlashCommandManager(this.mockManager); await Expect(() => ascm.disableCommand('testing', 'doesnt-exist')).toThrowErrorAsync( Error, 'The command "doesnt-exist" does not exist to disable.', ); await ascm.addCommand('testing', TestData.getSlashCommand('command')); await Expect(() => ascm.disableCommand('testing', 'command')).not.toThrowAsync(); Expect((ascm as any).providedCommands.get('testing').get('command').isDisabled).toBe(true); Expect((ascm as any).providedCommands.get('testing').get('command').isEnabled).toBe(false); await ascm.addCommand('testing', TestData.getSlashCommand('another-command')); (ascm as any).providedCommands.get('testing').get('another-command').isRegistered = true; await Expect(() => ascm.disableCommand('testing', 'another-command')).not.toThrowAsync(); Expect(this.mockBridges.getCommandBridge().doDoesCommandExist).toHaveBeenCalled().exactly(3); } @AsyncTest() public async disableSystemCommand() { const ascm = new AppSlashCommandManager(this.mockManager); await Expect(() => ascm.disableCommand('testing', 'it-exists')).not.toThrowAsync(); Expect(this.mockBridges.getCommandBridge().doDisableCommand).toHaveBeenCalledWith('it-exists', 'testing').exactly(1); Expect(this.mockBridges.getCommandBridge().doDoesCommandExist).toHaveBeenCalled().exactly(1); } @AsyncTest() public async failToDisableAnotherAppsCommand() { const ascm = new AppSlashCommandManager(this.mockManager); await ascm.addCommand('another-app', TestData.getSlashCommand('command')); await Expect(() => ascm.disableCommand('my-app', 'command')).toThrowErrorAsync( CommandHasAlreadyBeenTouchedError, 'The command "command" has already been touched by another App.', ); } @AsyncTest() public async registerCommands() { const ascm = new AppSlashCommandManager(this.mockManager); SpyOn(ascm, 'registerCommand'); await ascm.addCommand('testing', TestData.getSlashCommand('enabled-command')); const enabledRegInfo = (ascm as any).providedCommands.get('testing').get('enabled-command') as AppSlashCommand; await ascm.addCommand('testing', TestData.getSlashCommand('disabled-command')); await ascm.disableCommand('testing', 'disabled-command'); const disabledRegInfo = (ascm as any).providedCommands.get('testing').get('disabled-command') as AppSlashCommand; await Expect(() => ascm.registerCommands('non-existant')).not.toThrowAsync(); await Expect(() => ascm.registerCommands('testing')).not.toThrowAsync(); Expect(enabledRegInfo.isRegistered).toBe(true); Expect(disabledRegInfo.isRegistered).toBe(false); Expect((ascm as any).registerCommand as FunctionSpy) .toHaveBeenCalledWith('testing', enabledRegInfo) .exactly(1); Expect(this.mockBridges.getCommandBridge().doRegisterCommand).toHaveBeenCalledWith(enabledRegInfo.slashCommand, 'testing').exactly(1); } @AsyncTest() public async unregisterCommands() { const ascm = new AppSlashCommandManager(this.mockManager); await ascm.addCommand('testing', TestData.getSlashCommand('command')); await ascm.modifyCommand('testing', TestData.getSlashCommand('it-exists')); await Expect(() => ascm.unregisterCommands('non-existant')).not.toThrowAsync(); await Expect(() => ascm.unregisterCommands('testing')).not.toThrowAsync(); Expect(this.mockBridges.getCommandBridge().doUnregisterCommand).toHaveBeenCalled().exactly(1); } @AsyncTest() public async executeCommands() { const ascm = new AppSlashCommandManager(this.mockManager); await ascm.addCommand('testing', TestData.getSlashCommand('command')); await ascm.addCommand('testing', TestData.getSlashCommand('not-registered')); await ascm.addCommand('testing', TestData.getSlashCommand('disabled-command')); await ascm.disableCommand('testing', 'not-registered'); await ascm.registerCommands('testing'); (ascm as any).providedCommands.get('testing').get('disabled-command').isDisabled = true; await ascm.modifyCommand('testing', TestData.getSlashCommand('it-exists')); const context = new SlashCommandContext(TestData.getUser(), TestData.getRoom(), []); await Expect(() => ascm.executeCommand('nope', context)).not.toThrowAsync(); await Expect(() => ascm.executeCommand('it-exists', context)).not.toThrowAsync(); await Expect(() => ascm.executeCommand('command', context)).not.toThrowAsync(); await Expect(() => ascm.executeCommand('not-registered', context)).not.toThrowAsync(); await Expect(() => ascm.executeCommand('disabled-command', context)).not.toThrowAsync(); const classContext = new SlashCommandContext(TestData.getUser(), new Room(TestData.getRoom(), this.mockManager), []); await Expect(() => ascm.executeCommand('it-exists', classContext)).not.toThrowAsync(); // set it up for no "no app failure" const failedItems = new Map(); const asm = new AppSlashCommand(this.mockApp, TestData.getSlashCommand('failure')); asm.hasBeenRegistered(); failedItems.set('failure', asm); (ascm as any).providedCommands.set('failMePlease', failedItems); (ascm as any).touchedCommandsToApps.set('failure', 'failMePlease'); await Expect(() => ascm.executeCommand('failure', context)).toThrowAsync(); AppSlashCommandManagerTestFixture.doThrow = true; await Expect(() => ascm.executeCommand('command', context)).not.toThrowAsync(); AppSlashCommandManagerTestFixture.doThrow = false; } @AsyncTest() public async getPreviews() { const ascm = new AppSlashCommandManager(this.mockManager); await ascm.addCommand('testing', TestData.getSlashCommand('command')); await ascm.addCommand('testing', TestData.getSlashCommand('not-registered')); await ascm.addCommand('testing', TestData.getSlashCommand('disabled-command')); await ascm.disableCommand('testing', 'not-registered'); await ascm.registerCommands('testing'); (ascm as any).providedCommands.get('testing').get('disabled-command').isDisabled = true; await ascm.modifyCommand('testing', TestData.getSlashCommand('it-exists')); const context = new SlashCommandContext(TestData.getUser(), TestData.getRoom(), ['testing']); await Expect(() => ascm.getPreviews('nope', context)).not.toThrowAsync(); await Expect(() => ascm.getPreviews('it-exists', context)).not.toThrowAsync(); await Expect(() => ascm.getPreviews('command', context)).not.toThrowAsync(); await Expect(() => ascm.getPreviews('not-registered', context)).not.toThrowAsync(); await Expect(() => ascm.getPreviews('disabled-command', context)).not.toThrowAsync(); const classContext = new SlashCommandContext(TestData.getUser(), new Room(TestData.getRoom(), this.mockManager), []); await Expect(() => ascm.getPreviews('it-exists', classContext)).not.toThrowAsync(); // set it up for no "no app failure" const failedItems = new Map(); const asm = new AppSlashCommand(this.mockApp, TestData.getSlashCommand('failure')); asm.hasBeenRegistered(); failedItems.set('failure', asm); (ascm as any).providedCommands.set('failMePlease', failedItems); (ascm as any).touchedCommandsToApps.set('failure', 'failMePlease'); await Expect(() => ascm.getPreviews('failure', context)).not.toThrowAsync(); // TODO: Figure out how tests can mock/test the result now that we care about it } @AsyncTest() public async executePreview() { const previewItem = {} as ISlashCommandPreviewItem; const ascm = new AppSlashCommandManager(this.mockManager); await ascm.addCommand('testing', TestData.getSlashCommand('command')); await ascm.addCommand('testing', TestData.getSlashCommand('not-registered')); await ascm.addCommand('testing', TestData.getSlashCommand('disabled-command')); await ascm.disableCommand('testing', 'not-registered'); await ascm.registerCommands('testing'); (ascm as any).providedCommands.get('testing').get('disabled-command').isDisabled = true; await ascm.modifyCommand('testing', TestData.getSlashCommand('it-exists')); const context = new SlashCommandContext(TestData.getUser(), TestData.getRoom(), ['testing']); await Expect(() => ascm.executePreview('nope', previewItem, context)).not.toThrowAsync(); await Expect(() => ascm.executePreview('it-exists', previewItem, context)).not.toThrowAsync(); await Expect(() => ascm.executePreview('command', previewItem, context)).not.toThrowAsync(); await Expect(() => ascm.executePreview('not-registered', previewItem, context)).not.toThrowAsync(); await Expect(() => ascm.executePreview('disabled-command', previewItem, context)).not.toThrowAsync(); const classContext = new SlashCommandContext(TestData.getUser(), new Room(TestData.getRoom(), this.mockManager), []); await Expect(() => ascm.executePreview('it-exists', previewItem, classContext)).not.toThrowAsync(); // set it up for no "no app failure" const failedItems = new Map(); const asm = new AppSlashCommand(this.mockApp, TestData.getSlashCommand('failure')); asm.hasBeenRegistered(); failedItems.set('failure', asm); (ascm as any).providedCommands.set('failMePlease', failedItems); (ascm as any).touchedCommandsToApps.set('failure', 'failMePlease'); await ascm.executePreview('nope', previewItem, context).catch(() => console.log(new Error().stack)); await Expect(() => ascm.executePreview('failure', previewItem, context)).not.toThrowAsync(); } }