Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

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"');
});
});