Skip to content

Commit 89312f7

Browse files
Chuanyi SuChuanyi Su
authored andcommitted
test: improve coverage for statistics.commands, playback.commands, layout-manager
1 parent a3033eb commit 89312f7

3 files changed

Lines changed: 1188 additions & 124 deletions

File tree

packages/cli/src/commands/playback.commands.test.ts

Lines changed: 298 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,10 @@
4747
*/
4848

4949
import { Command } from 'commander';
50-
import { registerPlaybackCommands, validateListType } from './playback.commands';
50+
import { registerPlaybackCommands, validateListType, validateOutputFormat } from './playback.commands';
5151
import { PlaybackHandler } from '../handlers/playback.handler';
52+
import { authAdapter } from '../config/auth-adapter';
53+
import { configManager } from '../config/manager';
5254

5355
// Mock the PlaybackHandler
5456
const mockListPlayback = jest.fn().mockResolvedValue(undefined);
@@ -1450,19 +1452,307 @@ describe('Playback Commands (Story 9.1 - ATDD RED Phase)', () => {
14501452

14511453
expect(mockMergePlayback).toHaveBeenCalled();
14521454
});
1455+
});
14531456

1454-
it('should handle API errors in merge action', async () => {
1455-
mockMergePlayback.mockRejectedValueOnce(new Error('API Error'));
1457+
// ============================================
1458+
// Auth & Config error handling tests
1459+
// ============================================
14561460

1461+
describe('auth failure handling', () => {
1462+
it('should handle auth failure in list action', async () => {
1463+
(authAdapter.tryGetAuthConfig as jest.Mock).mockReturnValueOnce(null);
14571464
const program = new Command();
14581465
registerPlaybackCommands(program);
14591466

14601467
await expect(
1461-
program.parseAsync([
1462-
'node', 'test', 'playback', 'merge',
1463-
'--channel-id', '3151318',
1464-
'--file-ids', '1,2,3',
1465-
])
1468+
program.parseAsync(['node', 'test', 'playback', 'list', '--channel-id', '3151318'])
1469+
).rejects.toThrow();
1470+
});
1471+
1472+
it('should handle auth failure in get action', async () => {
1473+
(authAdapter.tryGetAuthConfig as jest.Mock).mockReturnValueOnce(null);
1474+
const program = new Command();
1475+
registerPlaybackCommands(program);
1476+
1477+
await expect(
1478+
program.parseAsync(['node', 'test', 'playback', 'get', '--channel-id', '3151318', '--video-id', '123'])
1479+
).rejects.toThrow();
1480+
});
1481+
1482+
it('should handle auth failure in delete action', async () => {
1483+
(authAdapter.tryGetAuthConfig as jest.Mock).mockReturnValueOnce(null);
1484+
const program = new Command();
1485+
registerPlaybackCommands(program);
1486+
1487+
await expect(
1488+
program.parseAsync(['node', 'test', 'playback', 'delete', '--channel-id', '3151318', '--video-id', '123', '--force'])
1489+
).rejects.toThrow();
1490+
});
1491+
1492+
it('should handle auth failure in merge action', async () => {
1493+
(authAdapter.tryGetAuthConfig as jest.Mock).mockReturnValueOnce(null);
1494+
const program = new Command();
1495+
registerPlaybackCommands(program);
1496+
1497+
await expect(
1498+
program.parseAsync(['node', 'test', 'playback', 'merge', '--channel-id', '3151318', '--file-ids', '1,2,3'])
1499+
).rejects.toThrow();
1500+
});
1501+
});
1502+
1503+
describe('authentication error with diagnostics', () => {
1504+
it('should show diagnostics when auth error in list action', async () => {
1505+
(authAdapter.tryGetAuthConfig as jest.Mock).mockReturnValue({
1506+
config: { appId: 'test', appSecret: 'test' },
1507+
source: 'env',
1508+
});
1509+
mockListPlayback.mockRejectedValueOnce(new Error('Authentication failed'));
1510+
(authAdapter.getDiagnostics as jest.Mock).mockReturnValue({
1511+
availableSources: [{ appId: 'a', appSecret: 's', metadata: { source: 'env' }, type: 'env' }],
1512+
errors: [],
1513+
});
1514+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
1515+
const program = new Command();
1516+
registerPlaybackCommands(program);
1517+
1518+
await expect(
1519+
program.parseAsync(['node', 'test', 'playback', 'list', '--channel-id', '3151318'])
1520+
).rejects.toThrow();
1521+
1522+
expect(authAdapter.getDiagnostics).toHaveBeenCalled();
1523+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Authentication Diagnostics'));
1524+
consoleSpy.mockRestore();
1525+
});
1526+
1527+
it('should show diagnostics errors when available', async () => {
1528+
(authAdapter.tryGetAuthConfig as jest.Mock).mockReturnValue({
1529+
config: { appId: 'test', appSecret: 'test' },
1530+
source: 'env',
1531+
});
1532+
mockListPlayback.mockRejectedValueOnce(new Error('Authentication failed'));
1533+
(authAdapter.getDiagnostics as jest.Mock).mockReturnValue({
1534+
availableSources: [{ appId: '', appSecret: '', metadata: { source: 'env' }, type: 'env' }],
1535+
errors: ['Missing appId'],
1536+
});
1537+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
1538+
const program = new Command();
1539+
registerPlaybackCommands(program);
1540+
1541+
await expect(
1542+
program.parseAsync(['node', 'test', 'playback', 'list', '--channel-id', '3151318'])
1543+
).rejects.toThrow();
1544+
1545+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Errors:'));
1546+
consoleSpy.mockRestore();
1547+
});
1548+
1549+
it('should show diagnostics when auth error in merge action', async () => {
1550+
(authAdapter.tryGetAuthConfig as jest.Mock).mockReturnValue({
1551+
config: { appId: 'test', appSecret: 'test' },
1552+
source: 'env',
1553+
});
1554+
mockMergePlayback.mockRejectedValueOnce(new Error('Authentication failed'));
1555+
(authAdapter.getDiagnostics as jest.Mock).mockReturnValue({
1556+
availableSources: [],
1557+
errors: [],
1558+
});
1559+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
1560+
const program = new Command();
1561+
registerPlaybackCommands(program);
1562+
1563+
await expect(
1564+
program.parseAsync(['node', 'test', 'playback', 'merge', '--channel-id', '3151318', '--file-ids', '1,2,3'])
1565+
).rejects.toThrow();
1566+
1567+
expect(authAdapter.getDiagnostics).toHaveBeenCalled();
1568+
consoleSpy.mockRestore();
1569+
});
1570+
1571+
it('should show diagnostics with errors in merge action', async () => {
1572+
(authAdapter.tryGetAuthConfig as jest.Mock).mockReturnValue({
1573+
config: { appId: 'test', appSecret: 'test' },
1574+
source: 'env',
1575+
});
1576+
mockMergePlayback.mockRejectedValueOnce(new Error('Authentication failed'));
1577+
(authAdapter.getDiagnostics as jest.Mock).mockReturnValue({
1578+
availableSources: [{ appId: '', appSecret: '', metadata: { source: 'cli' }, type: 'cli' }],
1579+
errors: ['Missing secret'],
1580+
});
1581+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
1582+
const program = new Command();
1583+
registerPlaybackCommands(program);
1584+
1585+
await expect(
1586+
program.parseAsync(['node', 'test', 'playback', 'merge', '--channel-id', '3151318', '--file-ids', '1,2,3'])
1587+
).rejects.toThrow();
1588+
1589+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Errors:'));
1590+
consoleSpy.mockRestore();
1591+
});
1592+
1593+
it('should show diagnostics when auth error in get action', async () => {
1594+
(authAdapter.tryGetAuthConfig as jest.Mock).mockReturnValue({
1595+
config: { appId: 'test', appSecret: 'test' },
1596+
source: 'env',
1597+
});
1598+
mockGetPlayback.mockRejectedValueOnce(new Error('Authentication failed'));
1599+
(authAdapter.getDiagnostics as jest.Mock).mockReturnValue({
1600+
availableSources: [],
1601+
errors: [],
1602+
});
1603+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
1604+
const program = new Command();
1605+
registerPlaybackCommands(program);
1606+
1607+
await expect(
1608+
program.parseAsync(['node', 'test', 'playback', 'get', '--channel-id', '3151318', '--video-id', '123'])
1609+
).rejects.toThrow();
1610+
1611+
expect(authAdapter.getDiagnostics).toHaveBeenCalled();
1612+
consoleSpy.mockRestore();
1613+
});
1614+
1615+
it('should show diagnostics when auth error in delete action', async () => {
1616+
(authAdapter.tryGetAuthConfig as jest.Mock).mockReturnValue({
1617+
config: { appId: 'test', appSecret: 'test' },
1618+
source: 'env',
1619+
});
1620+
mockDeletePlayback.mockRejectedValueOnce(new Error('Authentication failed'));
1621+
(authAdapter.getDiagnostics as jest.Mock).mockReturnValue({
1622+
availableSources: [],
1623+
errors: [],
1624+
});
1625+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
1626+
const program = new Command();
1627+
registerPlaybackCommands(program);
1628+
1629+
await expect(
1630+
program.parseAsync(['node', 'test', 'playback', 'delete', '--channel-id', '3151318', '--video-id', '123', '--force'])
1631+
).rejects.toThrow();
1632+
1633+
expect(authAdapter.getDiagnostics).toHaveBeenCalled();
1634+
consoleSpy.mockRestore();
1635+
});
1636+
1637+
it('should show diagnostics with errors in delete action', async () => {
1638+
(authAdapter.tryGetAuthConfig as jest.Mock).mockReturnValue({
1639+
config: { appId: 'test', appSecret: 'test' },
1640+
source: 'env',
1641+
});
1642+
mockDeletePlayback.mockRejectedValueOnce(new Error('Authentication failed'));
1643+
(authAdapter.getDiagnostics as jest.Mock).mockReturnValue({
1644+
availableSources: [{ appId: '', appSecret: '', metadata: { source: 'cli' }, type: 'cli' }],
1645+
errors: ['Missing secret'],
1646+
});
1647+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
1648+
const program = new Command();
1649+
registerPlaybackCommands(program);
1650+
1651+
await expect(
1652+
program.parseAsync(['node', 'test', 'playback', 'delete', '--channel-id', '3151318', '--video-id', '123', '--force'])
1653+
).rejects.toThrow();
1654+
1655+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Errors:'));
1656+
consoleSpy.mockRestore();
1657+
});
1658+
});
1659+
1660+
describe('config manager error handling', () => {
1661+
it('should handle incomplete auth config error gracefully', async () => {
1662+
(configManager.load as jest.Mock).mockRejectedValueOnce(
1663+
new Error('Auth configuration is incomplete')
1664+
);
1665+
mockListPlayback.mockResolvedValueOnce({ contents: [], total: 0 });
1666+
const program = new Command();
1667+
registerPlaybackCommands(program);
1668+
1669+
await program.parseAsync(['node', 'test', 'playback', 'list', '--channel-id', '3151318']);
1670+
1671+
expect(mockListPlayback).toHaveBeenCalled();
1672+
});
1673+
1674+
it('should handle other config manager errors', async () => {
1675+
(configManager.load as jest.Mock).mockRejectedValueOnce(
1676+
new Error('Network error')
1677+
);
1678+
const program = new Command();
1679+
registerPlaybackCommands(program);
1680+
1681+
await expect(
1682+
program.parseAsync(['node', 'test', 'playback', 'list', '--channel-id', '3151318'])
1683+
).rejects.toThrow();
1684+
});
1685+
});
1686+
1687+
describe('verbose mode', () => {
1688+
it('should show auth source info when verbose is set', async () => {
1689+
(authAdapter.tryGetAuthConfig as jest.Mock).mockReturnValue({
1690+
config: { appId: 'test', appSecret: 'test' },
1691+
source: 'env',
1692+
accountName: 'test-account',
1693+
});
1694+
mockListPlayback.mockResolvedValueOnce({ contents: [], total: 0 });
1695+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
1696+
const program = new Command();
1697+
program.option('--verbose', 'verbose output');
1698+
registerPlaybackCommands(program);
1699+
1700+
await program.parseAsync(['node', 'test', '--verbose', 'playback', 'list', '--channel-id', '3151318']);
1701+
1702+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Authentication Source'));
1703+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Account'));
1704+
consoleSpy.mockRestore();
1705+
});
1706+
1707+
it('should handle missing accountName in verbose mode', async () => {
1708+
(authAdapter.tryGetAuthConfig as jest.Mock).mockReturnValue({
1709+
config: { appId: 'test', appSecret: 'test' },
1710+
source: 'env',
1711+
});
1712+
mockListPlayback.mockResolvedValueOnce({ contents: [], total: 0 });
1713+
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
1714+
const program = new Command();
1715+
program.option('--verbose', 'verbose output');
1716+
registerPlaybackCommands(program);
1717+
1718+
await program.parseAsync(['node', 'test', '--verbose', 'playback', 'list', '--channel-id', '3151318']);
1719+
1720+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Authentication Source'));
1721+
consoleSpy.mockRestore();
1722+
});
1723+
1724+
it('should handle missing source in auth result', async () => {
1725+
(authAdapter.tryGetAuthConfig as jest.Mock).mockReturnValue({
1726+
config: { appId: 'test', appSecret: 'test' },
1727+
});
1728+
mockListPlayback.mockResolvedValueOnce({ contents: [], total: 0 });
1729+
const program = new Command();
1730+
registerPlaybackCommands(program);
1731+
1732+
await program.parseAsync(['node', 'test', 'playback', 'list', '--channel-id', '3151318']);
1733+
1734+
expect(mockListPlayback).toHaveBeenCalled();
1735+
});
1736+
});
1737+
1738+
describe('non-Error exceptions', () => {
1739+
it('should handle non-Error thrown in list action', async () => {
1740+
mockListPlayback.mockRejectedValueOnce('string error');
1741+
const program = new Command();
1742+
registerPlaybackCommands(program);
1743+
1744+
await expect(
1745+
program.parseAsync(['node', 'test', 'playback', 'list', '--channel-id', '3151318'])
1746+
).rejects.toThrow();
1747+
});
1748+
1749+
it('should handle non-Error thrown in merge action', async () => {
1750+
mockMergePlayback.mockRejectedValueOnce(42);
1751+
const program = new Command();
1752+
registerPlaybackCommands(program);
1753+
1754+
await expect(
1755+
program.parseAsync(['node', 'test', 'playback', 'merge', '--channel-id', '3151318', '--file-ids', '1,2,3'])
14661756
).rejects.toThrow();
14671757
});
14681758
});

0 commit comments

Comments
 (0)