Skip to content

Commit

Permalink
テスト追加
Browse files Browse the repository at this point in the history
  • Loading branch information
na2na-p committed Nov 20, 2023
1 parent fa96838 commit c1ac5b6
Show file tree
Hide file tree
Showing 7 changed files with 353 additions and 31 deletions.
277 changes: 277 additions & 0 deletions src/features/core/internal/Voice/internal/Voice.class.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
import type { Mock } from 'vitest';

import {
joinVoiceChannel,
getVoiceConnection,
} from '@/features/library/index.js';
import type {
ChatInputCommandInteraction,
GuildMember,
VoiceConnection,
} from '@/features/library/index.js';
import { LogicException } from '@/features/others/Error/LogicException.js';
import { getInteractionMemberId } from '@/features/others/discord/index.js';

import { Voice } from './Voice.class.js';
import {
JOINABLE_TYPE_MAP,
checkJoinable,
} from './funcs/checkJoinable/index.js';

vi.mock('@/features/others/discord/index.js', async () => {
const actual = (await vi.importActual(
'@/features/others/discord/index.js'
)) as object;
return {
...actual,
getInteractionMemberId: vi.fn(),
};
});

vi.mock('@/features/library/index.js', async () => {
const actual = (await vi.importActual(
'@/features/library/index.js'
)) as object;
return {
...actual,
joinVoiceChannel: vi.fn(),
getVoiceConnection: vi.fn(),
};
});

vi.mock('./funcs/checkJoinable/index.js', async () => {
const actual = (await vi.importActual(
'./funcs/checkJoinable/index.js'
)) as object;
return {
...actual,
checkJoinable: vi.fn(),
};
});

describe('Voice', () => {
let voice: Voice;

beforeEach(() => {
voice = new Voice();
});

afterEach(() => {
vi.clearAllMocks();
});

describe('join', () => {
it('should reply with an error message if the channel is not found', async () => {
const mockedInteraction = {
reply: vi.fn(),
} as unknown as Readonly<ChatInputCommandInteraction>;

(getInteractionMemberId as Mock).mockResolvedValueOnce({
voice: { channel: null },
});
(checkJoinable as Mock).mockReturnValueOnce(JOINABLE_TYPE_MAP.NOT_FOUND);

await voice.join({ interaction: mockedInteraction });

expect(checkJoinable).toHaveBeenCalledWith({ channel: null });
expect(mockedInteraction.reply).toHaveBeenCalledWith({
content: '接続先のVCが見つかりません。',
ephemeral: false,
});
});

it('should reply with an error message if the channel is not joinable', async () => {
const mockedInteraction = {
reply: vi.fn(),
} as unknown as Readonly<ChatInputCommandInteraction>;

(getInteractionMemberId as Mock).mockResolvedValueOnce({
voice: { channel: { joinable: true } },
});
(checkJoinable as Mock).mockReturnValueOnce(
JOINABLE_TYPE_MAP.NOT_JOINABLE
);

await voice.join({ interaction: mockedInteraction });

expect(checkJoinable).toHaveBeenCalledWith({
channel: { joinable: true },
});
expect(mockedInteraction.reply).toHaveBeenCalledWith({
content: '接続先のVCに参加できません。権限の見直しをしてください。',
ephemeral: true,
});
});

it('should reply with an error message if the channel is not viewable', async () => {
const mockedInteraction = {
reply: vi.fn(),
} as unknown as Readonly<ChatInputCommandInteraction>;

(getInteractionMemberId as Mock).mockResolvedValueOnce({
voice: { channel: { joinable: true, viewable: false } },
});
(checkJoinable as Mock).mockReturnValueOnce(
JOINABLE_TYPE_MAP.NOT_VIEWABLE
);

await voice.join({ interaction: mockedInteraction });

expect(checkJoinable).toHaveBeenCalledWith({
channel: { joinable: true, viewable: false },
});
expect(mockedInteraction.reply).toHaveBeenCalledWith({
content: '接続先のVCに参加できません。権限の見直しをしてください。',
ephemeral: true,
});
});

it('should throw an error if the channel is null', async () => {
const interaction = {
reply: vi.fn(),
} as unknown as Readonly<ChatInputCommandInteraction>;

(getInteractionMemberId as Mock).mockRejectedValueOnce(
new LogicException('Channel is null. Please check checkJoinable()')
);

await expect(voice.join({ interaction })).rejects.toThrowError(
'Channel is null. Please check checkJoinable()'
);
});

it('should throw an error if the channel is null', async () => {
const mockedInteraction = {
reply: vi.fn(),
} as unknown as Readonly<ChatInputCommandInteraction>;

(getInteractionMemberId as Mock).mockResolvedValueOnce({
voice: { channel: null },
});
(checkJoinable as Mock).mockReturnValueOnce(JOINABLE_TYPE_MAP.JOINABLE);

await expect(
voice.join({ interaction: mockedInteraction })
).rejects.toThrowError('Channel is null. Please check checkJoinable()');
});

it('should join the voice channel and return true', async () => {
const mockedGuildMember = {
voice: {
channel: {
id: 'channelId',
guild: {
id: 'guildId',
voiceAdapterCreator: vi.fn(),
joinable: true,
viewable: true,
},
},
},
} as const as unknown as Readonly<GuildMember>;

const mockedInteraction = {
reply: vi.fn(),
} as const as unknown as Readonly<ChatInputCommandInteraction>;

(getInteractionMemberId as Mock).mockResolvedValueOnce(mockedGuildMember);
(joinVoiceChannel as Mock).mockReturnValueOnce('connection');
(checkJoinable as Mock).mockReturnValueOnce(JOINABLE_TYPE_MAP.JOINABLE);

const result = await voice.join({ interaction: mockedInteraction });

expect(checkJoinable).toHaveBeenCalledWith(mockedGuildMember.voice);
expect(joinVoiceChannel).toHaveBeenCalledWith({
channelId: 'channelId',
guildId: 'guildId',
adapterCreator: expect.any(Function),
});
expect(voice['connection']).toEqual([
{ guildId: 'guildId', connection: 'connection' },
]);
expect(result).toBe(true);
});

it('should throw an error if the joinable type is unknown', async () => {
const interaction = {
voice: { channel: { joinable: true, viewable: true } },
reply: vi.fn(),
} as unknown as Readonly<ChatInputCommandInteraction>;

(getInteractionMemberId as Mock).mockResolvedValueOnce({
voice: { channel: null },
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(checkJoinable as Mock).mockReturnValueOnce('unknown' as any);

await expect(voice.join({ interaction })).rejects.toThrowError(
'Unknown joinable type.'
);
});
});

describe('leave', () => {
it('should return false if guildId is null', async () => {
const interaction = {
guildId: null,
} as unknown as Readonly<ChatInputCommandInteraction>;

const result = await voice.leave({ interaction });

expect(result).toBe(false);
});

it('should destroy the connection and return true if the target connection exists', async () => {
const interaction = {
guildId: 'guildId',
} as unknown as Readonly<ChatInputCommandInteraction>;

voice['connection'] = [
{
guildId: 'guildId',
connection: {
destroy: vi.fn(),
} as unknown as VoiceConnection,
},
];

const result = await voice.leave({ interaction });

expect(voice['connection']).toEqual([]);
expect(result).toBe(true);
});

it('should destroy the connection and return true if the target connection does not exist but the member has a connection', async () => {
const interaction = {
guildId: 'guildId',
} as unknown as Readonly<ChatInputCommandInteraction>;

(getInteractionMemberId as Mock).mockResolvedValueOnce({
guild: { id: 'guildId' },
});
(getVoiceConnection as Mock).mockReturnValueOnce({
destroy: vi.fn(),
});

const result = await voice.leave({ interaction });

expect(getVoiceConnection).toHaveBeenCalledWith('guildId');
expect(result).toBe(true);
});

it('should return false if the target connection does not exist and the member does not have a connection', async () => {
const interaction = {
guildId: 'guildId',
} as unknown as Readonly<ChatInputCommandInteraction>;

(getInteractionMemberId as Mock).mockResolvedValueOnce({
guild: { id: 'guildId' },
});
(getVoiceConnection as Mock).mockReturnValueOnce(null);

const result = await voice.leave({ interaction });

expect(result).toBe(false);
});
});
});
37 changes: 13 additions & 24 deletions src/features/core/internal/Voice/internal/Voice.class.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type {
VoiceConnection,
ChatInputCommandInteraction,
VoiceBasedChannel,
} from '@/features/library/index.js';
import {
getVoiceConnection,
Expand All @@ -11,10 +10,13 @@ import {
import { LogicException } from '@/features/others/Error/LogicException.js';
import { getInteractionMemberId } from '@/features/others/discord/index.js';

import { JOINABLE_TYPE_MAP } from './Voice.constants.js';
import {
JOINABLE_TYPE_MAP,
checkJoinable,
} from './funcs/checkJoinable/index.js';

export class Voice {
#connection: Array<{
connection: Array<{
guildId: string;
connection: VoiceConnection;
}> = [];
Expand All @@ -27,7 +29,7 @@ export class Voice {
const {
voice: { channel },
} = await getInteractionMemberId(interaction);
const joinable = this.checkJoinable({
const joinable = checkJoinable({
channel,
});

Expand Down Expand Up @@ -64,15 +66,18 @@ export class Voice {
adapterCreator: channel.guild.voiceAdapterCreator,
});

this.#connection.push({
this.connection.push({
guildId: channel.guild.id,
connection,
});
return true;

default:
throw new LogicException('Unknown joinable type.');
// HACK: ここでrejectするとSwitch外のカバレッジがuncoveredになる
break;
}

return Promise.reject(new LogicException('Unknown joinable type.'));
}

public async leave({
Expand All @@ -83,7 +88,7 @@ export class Voice {
const guildId = interaction.guildId;
if (isNil(guildId)) return false;

const targetConnection = this.#connection.find(
const targetConnection = this.connection.find(
connection => connection.guildId === guildId
);

Expand All @@ -98,26 +103,10 @@ export class Voice {
}
} else {
targetConnection.connection.destroy();
this.#connection = this.#connection.filter(
this.connection = this.connection.filter(
connection => connection.guildId !== guildId
);
return true;
}
}

checkJoinable({
channel,
}: {
channel: VoiceBasedChannel | null;
}): (typeof JOINABLE_TYPE_MAP)[keyof typeof JOINABLE_TYPE_MAP] {
if (isNil(channel)) {
return JOINABLE_TYPE_MAP.NOT_FOUND;
} else if (!channel.joinable) {
return JOINABLE_TYPE_MAP.NOT_JOINABLE;
} else if (!channel.viewable) {
return JOINABLE_TYPE_MAP.NOT_VIEWABLE;
} else {
return JOINABLE_TYPE_MAP.JOINABLE;
}
}
}
7 changes: 0 additions & 7 deletions src/features/core/internal/Voice/internal/Voice.constants.ts
Original file line number Diff line number Diff line change
@@ -1,7 +0,0 @@
export const JOINABLE_TYPE_MAP = {
JOINABLE: 'JOINABLE',
NOT_JOINABLE: 'NOT_JOINABLE',
NOT_FOUND: 'NOT_FOUND',
NOT_VIEWABLE: 'NOT_VIEWABLE',
ALREADY_JOINED: 'ALREADY_JOINED',
} as const;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { checkJoinable } from './internal/checkJoinable.func.js';
export { JOINABLE_TYPE_MAP } from './internal/checkJoinable.constants.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const JOINABLE_TYPE_MAP = {
JOINABLE: 'JOINABLE',
NOT_JOINABLE: 'NOT_JOINABLE',
NOT_FOUND: 'NOT_FOUND',
NOT_VIEWABLE: 'NOT_VIEWABLE',
ALREADY_JOINED: 'ALREADY_JOINED',
} as const;
Loading

0 comments on commit c1ac5b6

Please sign in to comment.