9b395f5cc3
Build Chrome Extension / build (push) Waiting to run
Trigger Website Rebuild (Docs Updated) / dispatch (push) Waiting to run
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
164 lines
7.1 KiB
JavaScript
164 lines
7.1 KiB
JavaScript
import { describe, expect, it, vi } from 'vitest';
|
|
import { getRegistry } from '@jackwener/opencli/registry';
|
|
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
import { parseSubredditName } from './subreddit-info.js';
|
|
import './subreddit-info.js';
|
|
|
|
function makePage(result) {
|
|
return {
|
|
goto: vi.fn().mockResolvedValue(undefined),
|
|
evaluate: vi.fn().mockResolvedValue(result),
|
|
};
|
|
}
|
|
|
|
describe('reddit subreddit-info command', () => {
|
|
const command = getRegistry().get('reddit/subreddit-info');
|
|
|
|
it('registers with the expected shape', () => {
|
|
expect(command).toBeDefined();
|
|
expect(command.access).toBe('read');
|
|
expect(command.browser).toBe(true);
|
|
expect(command.columns).toEqual(['field', 'value']);
|
|
});
|
|
|
|
it('parseSubredditName strips prefixes and validates the name shape', () => {
|
|
expect(parseSubredditName('python')).toBe('python');
|
|
expect(parseSubredditName('r/python')).toBe('python');
|
|
expect(parseSubredditName('/r/python')).toBe('python');
|
|
expect(parseSubredditName(' AskReddit ')).toBe('AskReddit');
|
|
expect(parseSubredditName('aw3some_sub')).toBe('aw3some_sub');
|
|
});
|
|
|
|
it('parseSubredditName rejects invalid names without silent fallback', () => {
|
|
for (const bad of [
|
|
'',
|
|
' ',
|
|
'py', // too short
|
|
'1abc', // must start with letter
|
|
'_sub', // must start with letter
|
|
'has space', // no spaces
|
|
'has-dash', // no dashes
|
|
'way_too_long_subreddit_name_here', // too long (>21)
|
|
]) {
|
|
expect(() => parseSubredditName(bad)).toThrow(ArgumentError);
|
|
}
|
|
});
|
|
|
|
it('rejects bad subreddit names BEFORE navigating', async () => {
|
|
const page = makePage({ kind: 'ok', info: {} });
|
|
await expect(command.func(page, { name: 'has space' })).rejects.toBeInstanceOf(ArgumentError);
|
|
expect(page.goto).not.toHaveBeenCalled();
|
|
expect(page.evaluate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('throws EmptyResultError for missing / banned / private / quarantined subreddits', async () => {
|
|
for (const detail of ['not found', 'banned', 'private', 'quarantined']) {
|
|
await expect(command.func(makePage({ kind: 'missing', detail }), { name: 'python' }))
|
|
.rejects.toBeInstanceOf(EmptyResultError);
|
|
}
|
|
});
|
|
|
|
it('treats HTTP 401/403/404 about.json responses as inaccessible subreddit, not auth-required', async () => {
|
|
for (const status of [401, 403, 404]) {
|
|
const scriptResults = [];
|
|
const page = {
|
|
goto: vi.fn().mockResolvedValue(undefined),
|
|
evaluate: vi.fn(async (script) => {
|
|
const result = await (new Function('fetch', `return (${script})`))(vi.fn(async () => ({
|
|
status,
|
|
ok: false,
|
|
})));
|
|
scriptResults.push(result);
|
|
return result;
|
|
}),
|
|
};
|
|
await expect(command.func(page, { name: 'python' })).rejects.toBeInstanceOf(EmptyResultError);
|
|
expect(scriptResults[0]).toMatchObject({ kind: 'missing' });
|
|
}
|
|
});
|
|
|
|
it('throws CommandExecutionError on HTTP and exception failure modes', async () => {
|
|
await expect(command.func(makePage({ kind: 'http', httpStatus: 500, where: 'about.json' }), { name: 'python' }))
|
|
.rejects.toBeInstanceOf(CommandExecutionError);
|
|
await expect(command.func(makePage({ kind: 'exception', detail: 'bad' }), { name: 'python' }))
|
|
.rejects.toBeInstanceOf(CommandExecutionError);
|
|
});
|
|
|
|
it('throws CommandExecutionError for malformed 200 subreddit payloads', async () => {
|
|
await expect(command.func(makePage({
|
|
kind: 'malformed',
|
|
detail: 'Reddit returned malformed subreddit info for r/python (missing data.display_name).',
|
|
}), { name: 'python' }))
|
|
.rejects.toMatchObject({
|
|
code: 'COMMAND_EXEC',
|
|
message: expect.stringContaining('malformed subreddit info'),
|
|
});
|
|
});
|
|
|
|
it('maps a normal subreddit payload into typed field/value rows', async () => {
|
|
const info = {
|
|
display_name: 'python',
|
|
display_name_prefixed: 'r/python',
|
|
title: 'Python',
|
|
public_description: ' News about the programming language Python. ',
|
|
subscribers: 1234567,
|
|
active_user_count: 4321,
|
|
over18: false,
|
|
subreddit_type: 'public',
|
|
created_utc: 1201881600, // 2008-02-01
|
|
url: '/r/python/',
|
|
};
|
|
const rows = await command.func(makePage({ kind: 'ok', info }), { name: 'python' });
|
|
const byField = Object.fromEntries(rows.map((r) => [r.field, r.value]));
|
|
|
|
expect(byField.Name).toBe('r/python');
|
|
expect(byField.Title).toBe('Python');
|
|
expect(byField.Subscribers).toBe('1234567');
|
|
expect(byField['Active Now']).toBe('4321');
|
|
expect(byField.NSFW).toBe('No');
|
|
expect(byField.Type).toBe('public');
|
|
expect(byField.Description).toBe('News about the programming language Python.');
|
|
expect(byField.Created).toBe('2008-02-01');
|
|
expect(byField.URL).toBe('https://www.reddit.com/r/python/');
|
|
|
|
for (const row of rows) {
|
|
expect(Object.keys(row).sort()).toEqual(['field', 'value']);
|
|
}
|
|
});
|
|
|
|
it('falls back to accounts_active when active_user_count is missing', async () => {
|
|
const info = {
|
|
display_name: 'sub',
|
|
display_name_prefixed: 'r/sub',
|
|
accounts_active: 99,
|
|
url: '/r/sub/',
|
|
};
|
|
const rows = await command.func(makePage({ kind: 'ok', info }), { name: 'sub' });
|
|
const byField = Object.fromEntries(rows.map((r) => [r.field, r.value]));
|
|
expect(byField['Active Now']).toBe('99');
|
|
});
|
|
|
|
it('emits typed null (not "-" sentinel) when subscribers / activity / created are missing', async () => {
|
|
const info = {
|
|
display_name: 'sparse',
|
|
display_name_prefixed: 'r/sparse',
|
|
url: '/r/sparse/',
|
|
};
|
|
const rows = await command.func(makePage({ kind: 'ok', info }), { name: 'sparse' });
|
|
const byField = Object.fromEntries(rows.map((r) => [r.field, r.value]));
|
|
expect(byField.Subscribers).toBeNull();
|
|
expect(byField['Active Now']).toBeNull();
|
|
expect(byField.Created).toBeNull();
|
|
expect(byField.Description).toBeNull();
|
|
expect(byField.Title).toBeNull();
|
|
expect(byField.Type).toBeNull();
|
|
});
|
|
|
|
it('encodes the subreddit name into the fetch URL embedded in evaluate', async () => {
|
|
const page = makePage({ kind: 'ok', info: { display_name: 'python', display_name_prefixed: 'r/python', url: '/r/python/' } });
|
|
await command.func(page, { name: 'r/python' });
|
|
const script = page.evaluate.mock.calls[0][0];
|
|
expect(script).toContain('const sub = "python"');
|
|
});
|
|
});
|