Add mock data and unit tests for Channels and Members APIs

This commit is contained in:
2025-12-07 04:41:41 -05:00
parent 4673547b95
commit 58a938d0e7
12 changed files with 1377 additions and 0 deletions

View File

@@ -0,0 +1,352 @@
import {describe, expect, it, beforeEach, mock} from 'bun:test';
import {Channels, ChannelError, type Channel, type Member} from '../../../src/apis/Channels';
import type {PCastHttpRequests} from '../../../src/apis/PCastRequests';
import {HttpMethod} from '../../../src/net/http/HttpMethod';
import {mockChannel} from '../../mocks/Channel';
import {mockMember} from '../../mocks/Member';
describe('Channels', () => {
let mockHttpRequests: PCastHttpRequests;
let channels: Channels;
const createMockChannel = (overrides: Partial<Channel> = {}): Channel => ({...mockChannel, ...overrides});
const createMockMember = (overrides: Partial<Member> = {}): Member => ({...mockMember, ...overrides});
beforeEach(() => {
mockHttpRequests = {
request: mock(() => Promise.resolve({status: 'ok', channels: []}))
} as unknown as PCastHttpRequests;
channels = new Channels(mockHttpRequests);
});
describe('ChannelError', () => {
describe('when creating an error', () => {
it('should set the message and code properties', () => {
const error = new ChannelError('Something went wrong', 'TEST_ERROR');
expect(error.message).toBe('Something went wrong');
expect(error.code).toBe('TEST_ERROR');
expect(error.name).toBe('ChannelError');
});
it('should be an instance of Error', () => {
const error = new ChannelError('Test', 'CODE');
expect(error).toBeInstanceOf(Error);
});
});
});
describe('when creating a channel', () => {
it('should send a PUT request with channel data', async () => {
const mockChannel = createMockChannel();
(mockHttpRequests.request as any).mockResolvedValue({status: 'ok', channel: mockChannel});
const result = await channels.create('Test Channel', 'A test channel', ['option1']);
expect(mockHttpRequests.request).toHaveBeenCalledWith(HttpMethod.PUT, '/channel', expect.objectContaining({body: expect.any(String)}));
expect(result).toEqual(mockChannel);
});
it('should throw ChannelError when name is empty', async () => {
await expect(channels.create('', 'Description')).rejects.toThrow(ChannelError);
await expect(channels.create('', 'Description')).rejects.toThrow('Channel name cannot be empty');
});
it('should throw ChannelError when name is only whitespace', async () => {
await expect(channels.create(' ', 'Description')).rejects.toThrow(ChannelError);
});
it('should throw ChannelError when description is empty', async () => {
await expect(channels.create('Name', '')).rejects.toThrow('Channel description cannot be empty');
});
it('should throw ChannelError when options is not an array', async () => {
await expect(channels.create('Name', 'Desc', 'invalid' as any)).rejects.toThrow('Channel options must be an array');
});
it('should throw ChannelError when response is missing channel data', async () => {
(mockHttpRequests.request as any).mockResolvedValue({status: 'ok'});
await expect(channels.create('Name', 'Description')).rejects.toThrow('Invalid response format - missing channel data');
});
it('should cache the created channel by alias', async () => {
const mockChannel = createMockChannel({alias: 'new-channel'});
(mockHttpRequests.request as any).mockResolvedValue({status: 'ok', channel: mockChannel});
await channels.create('New Channel', 'Description');
// Now when we call get, it should use the cached value
const cachedChannel = await channels.get({alias: 'new-channel'});
expect(cachedChannel).toEqual(mockChannel);
});
});
describe('when listing channels', () => {
it('should return all channels from the API', async () => {
const mockChannels = [createMockChannel({channelId: '1'}), createMockChannel({channelId: '2'})];
(mockHttpRequests.request as any).mockResolvedValue({status: 'ok', channels: mockChannels});
const result = await channels.list();
expect(mockHttpRequests.request).toHaveBeenCalledWith(HttpMethod.GET, '/channels');
expect(result).toEqual(mockChannels);
});
it('should throw ChannelError when response is missing channels array', async () => {
(mockHttpRequests.request as any).mockResolvedValue({status: 'ok'});
await expect(channels.list()).rejects.toThrow('Invalid response format - missing channels data');
});
it('should update the internal cache with all channels', async () => {
const mockChannels = [createMockChannel({alias: 'channel-a'}), createMockChannel({alias: 'channel-b'})];
(mockHttpRequests.request as any).mockResolvedValue({status: 'ok', channels: mockChannels});
await channels.list();
// Verify cache was updated by checking get returns cached value
const cached = await channels.get({alias: 'channel-a'});
expect(cached?.alias).toBe('channel-a');
});
});
describe('when getting a channel', () => {
it('should throw ChannelError when neither alias nor channelId is provided', async () => {
await expect(channels.get({})).rejects.toThrow('Either alias or channelId must be provided');
});
it('should return cached channel when available', async () => {
const mockChannel = createMockChannel({alias: 'cached-channel'});
(mockHttpRequests.request as any).mockResolvedValue({status: 'ok', channels: [mockChannel]});
// First call populates cache
await channels.list();
// Second call should use cache
const result = await channels.get({alias: 'cached-channel'});
expect(result).toEqual(mockChannel);
});
it('should fetch from API and find by channelId', async () => {
const mockChannel = createMockChannel({channelId: 'target-id'});
(mockHttpRequests.request as any).mockResolvedValue({status: 'ok', channels: [mockChannel]});
const result = await channels.get({channelId: 'target-id'});
expect(result).toEqual(mockChannel);
});
it('should return undefined when channel is not found', async () => {
(mockHttpRequests.request as any).mockResolvedValue({status: 'ok', channels: []});
const result = await channels.get({alias: 'non-existent'});
expect(result).toBeUndefined();
});
});
describe('when getting publisher count', () => {
it('should return the count as a number', async () => {
(mockHttpRequests.request as any).mockResolvedValue('42');
const result = await channels.getPublisherCount('channel-123');
expect(mockHttpRequests.request).toHaveBeenCalledWith(HttpMethod.GET, '/channel/channel-123/publishers/count');
expect(result).toBe(42);
});
it('should URL encode the channel ID', async () => {
(mockHttpRequests.request as any).mockResolvedValue('0');
await channels.getPublisherCount('channel/with/slashes');
expect(mockHttpRequests.request).toHaveBeenCalledWith(HttpMethod.GET, '/channel/channel%2Fwith%2Fslashes/publishers/count');
});
});
describe('when getting channel members', () => {
it('should return the members array', async () => {
const mockMembers = [createMockMember()];
(mockHttpRequests.request as any).mockResolvedValue({status: 'ok', members: mockMembers});
const result = await channels.getMembers('channel-123');
expect(result).toEqual(mockMembers);
});
it('should throw ChannelError when channelId is empty', async () => {
await expect(channels.getMembers('')).rejects.toThrow('Channel ID cannot be empty');
});
it('should throw ChannelError when response is missing members', async () => {
(mockHttpRequests.request as any).mockResolvedValue({status: 'ok'});
await expect(channels.getMembers('channel-123')).rejects.toThrow('Invalid response format for channel members');
});
});
describe('when getting members by channel alias', () => {
it('should resolve channel and return members', async () => {
const mockChannel = createMockChannel({alias: 'my-alias', channelId: 'resolved-id'});
const mockMembers = [createMockMember()];
(mockHttpRequests.request as any).mockResolvedValueOnce({status: 'ok', channels: [mockChannel]}).mockResolvedValueOnce({status: 'ok', members: mockMembers});
const result = await channels.getMembersByChannelAlias('my-alias');
expect(result).toEqual(mockMembers);
});
it('should throw ChannelError when channel is not found', async () => {
(mockHttpRequests.request as any).mockResolvedValue({status: 'ok', channels: []});
await expect(channels.getMembersByChannelAlias('non-existent')).rejects.toThrow('Channel not found: non-existent');
});
});
describe('when deleting a channel', () => {
it('should send DELETE request with channelId', async () => {
(mockHttpRequests.request as any).mockResolvedValue({status: 'ok'});
await channels.delete({channelId: 'channel-to-delete'});
expect(mockHttpRequests.request).toHaveBeenCalledWith(HttpMethod.DELETE, '/channel/channel-to-delete');
});
it('should resolve alias to channelId before deleting', async () => {
const mockChannel = createMockChannel({alias: 'alias-to-delete', channelId: 'resolved-channel-id'});
(mockHttpRequests.request as any).mockResolvedValueOnce({status: 'ok', channels: [mockChannel]}).mockResolvedValueOnce({status: 'ok'});
await channels.delete({alias: 'alias-to-delete'});
expect(mockHttpRequests.request).toHaveBeenLastCalledWith(HttpMethod.DELETE, '/channel/resolved-channel-id');
});
it('should throw ChannelError when neither channelId nor alias is provided', async () => {
await expect(channels.delete({})).rejects.toThrow('Deleting a channel requires either a channelId or alias');
});
it('should throw ChannelError when channel is not found by alias', async () => {
(mockHttpRequests.request as any).mockResolvedValue({status: 'ok', channels: []});
await expect(channels.delete({alias: 'non-existent'})).rejects.toThrow('Unable to find room to delete');
});
it('should throw ChannelError when delete fails', async () => {
(mockHttpRequests.request as any).mockResolvedValue({status: 'error'});
await expect(channels.delete({channelId: 'channel-123'})).rejects.toThrow('Failed to delete channel');
});
it('should remove channel from cache after successful delete', async () => {
const mockChannel = createMockChannel({alias: 'cached-alias'});
(mockHttpRequests.request as any).mockResolvedValueOnce({status: 'ok', channels: [mockChannel]}).mockResolvedValueOnce({status: 'ok'});
// Populate cache
await channels.list();
// Delete by alias
await channels.delete({alias: 'cached-alias'});
// Verify cache was cleared
(mockHttpRequests.request as any).mockResolvedValue({status: 'ok', channels: []});
const result = await channels.get({alias: 'cached-alias'});
expect(result).toBeUndefined();
});
});
describe('when getting publish source stream id', () => {
it('should extract stream id from presenter URI', async () => {
const mockMember = createMockMember({
role: 'Presenter',
streams: [{type: 'video', uri: 'pcast://example.com/stream-id-abc123', audioState: 'active', videoState: 'active'}]
});
(mockHttpRequests.request as any).mockResolvedValue({status: 'ok', members: [mockMember]});
const result = await channels.getPublishSourceStreamId('channel-123');
expect(result).toBe('stream-id-abc123');
});
it('should return null when no members exist after retries', async () => {
(mockHttpRequests.request as any).mockResolvedValue({status: 'ok', members: []});
const result = await channels.getPublishSourceStreamId('channel-123', 1);
expect(result).toBeNull();
});
it('should return null when no presenter is found after retries', async () => {
const mockMember = createMockMember({role: 'Viewer'});
(mockHttpRequests.request as any).mockResolvedValue({status: 'ok', members: [mockMember]});
const result = await channels.getPublishSourceStreamId('channel-123', 1);
expect(result).toBeNull();
});
it('should retry when no presenter is found', async () => {
const mockMember = createMockMember({role: 'Viewer'});
const presenterMember = createMockMember({role: 'Presenter'});
(mockHttpRequests.request as any).mockResolvedValueOnce({status: 'ok', members: [mockMember]}).mockResolvedValueOnce({status: 'ok', members: [presenterMember]});
const result = await channels.getPublishSourceStreamId('channel-123', 2);
expect(result).not.toBeNull();
});
});
describe('when forking a channel', () => {
it('should send PUT request with source and destination channel ids', async () => {
(mockHttpRequests.request as any).mockResolvedValue({status: 'ok', members: []});
await channels.fork('source-channel', 'dest-channel');
expect(mockHttpRequests.request).toHaveBeenCalledWith(HttpMethod.PUT, '/channel/dest-channel/fork/source-channel');
});
it('should throw ChannelError when fork fails', async () => {
(mockHttpRequests.request as any).mockResolvedValue({status: 'error'});
await expect(channels.fork('source', 'dest')).rejects.toThrow('Failed to fork channel');
});
it('should return the response on success', async () => {
const mockResponse = {status: 'ok', members: [createMockMember()]};
(mockHttpRequests.request as any).mockResolvedValue(mockResponse);
const result = await channels.fork('source', 'dest');
expect(result).toEqual(mockResponse);
});
});
describe('when killing a channel', () => {
it('should send PUT request to kill endpoint', async () => {
(mockHttpRequests.request as any).mockResolvedValue({status: 'ok', killedMembers: []});
await channels.killChannel('channel-123');
expect(mockHttpRequests.request).toHaveBeenCalledWith(HttpMethod.PUT, '/channel/channel-123/kill');
});
it('should throw ChannelError when kill fails', async () => {
(mockHttpRequests.request as any).mockResolvedValue({status: 'error'});
await expect(channels.killChannel('channel-123')).rejects.toThrow('Failed to kill channel');
});
it('should return killed members on success', async () => {
const killedMember = createMockMember();
(mockHttpRequests.request as any).mockResolvedValue({status: 'ok', killedMembers: [killedMember]});
const result = await channels.killChannel('channel-123');
expect(result.killedMembers).toEqual([killedMember]);
});
});
});