fix: follow/unfollow hardening (#54) (thanks @citizenlee)
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import { Command } from 'commander';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { CliContext } from '../src/cli/shared.js';
|
||||
import { registerFollowCommands } from '../src/commands/follow.js';
|
||||
import { TwitterClient } from '../src/lib/twitter-client.js';
|
||||
|
||||
const baseCtx = {
|
||||
resolveTimeoutFromOptions: () => undefined,
|
||||
resolveCredentialsFromOptions: async () => ({
|
||||
cookies: { authToken: 'auth', ct0: 'ct0', cookieHeader: 'auth=auth; ct0=ct0' },
|
||||
warnings: [],
|
||||
}),
|
||||
p: () => '',
|
||||
} as unknown as CliContext;
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('follow commands', () => {
|
||||
it('prefers username lookup for numeric handles', async () => {
|
||||
const program = new Command();
|
||||
registerFollowCommands(program, baseCtx);
|
||||
|
||||
const lookupSpy = vi.spyOn(TwitterClient.prototype, 'getUserIdByUsername').mockResolvedValue({
|
||||
success: true,
|
||||
userId: '999',
|
||||
username: '12345',
|
||||
});
|
||||
const followSpy = vi.spyOn(TwitterClient.prototype, 'follow').mockResolvedValue({ success: true });
|
||||
vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
||||
|
||||
await program.parseAsync(['node', 'bird', 'follow', '12345']);
|
||||
|
||||
expect(lookupSpy).toHaveBeenCalledWith('12345');
|
||||
expect(followSpy).toHaveBeenCalledWith('999');
|
||||
});
|
||||
|
||||
it('falls back to numeric user IDs when lookup fails', async () => {
|
||||
const program = new Command();
|
||||
registerFollowCommands(program, baseCtx);
|
||||
|
||||
vi.spyOn(TwitterClient.prototype, 'getUserIdByUsername').mockResolvedValue({
|
||||
success: false,
|
||||
error: 'User not found',
|
||||
});
|
||||
const followSpy = vi.spyOn(TwitterClient.prototype, 'follow').mockResolvedValue({ success: true });
|
||||
vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
||||
|
||||
await program.parseAsync(['node', 'bird', 'follow', '12345']);
|
||||
|
||||
expect(followSpy).toHaveBeenCalledWith('12345');
|
||||
});
|
||||
});
|
||||
+13
-7
@@ -149,15 +149,21 @@ d('live CLI (Twitter/X)', () => {
|
||||
expect(whoamiStdout).toContain('credentials:');
|
||||
});
|
||||
|
||||
it('about returns account JSON', async () => {
|
||||
const aboutHandle = (process.env.BIRD_LIVE_ABOUT_HANDLE ?? handle).trim() || handle;
|
||||
const about = await runBird([...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'about', aboutHandle, '--json'], {
|
||||
it('follow/unfollow works (opt-in)', async () => {
|
||||
const followHandle = (process.env.BIRD_LIVE_FOLLOW_HANDLE ?? '').trim();
|
||||
if (!followHandle) {
|
||||
return;
|
||||
}
|
||||
|
||||
const follow = await runBird([...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'follow', followHandle], {
|
||||
timeoutMs: 45_000,
|
||||
});
|
||||
expect(about.exitCode).toBe(0);
|
||||
const payload = parseJson<Record<string, unknown>>(about.stdout);
|
||||
expect(Array.isArray(payload)).toBe(false);
|
||||
expect(Object.keys(payload).length).toBeGreaterThan(0);
|
||||
expect(follow.exitCode).toBe(0);
|
||||
|
||||
const unfollow = await runBird([...baseArgs, '--cookie-timeout', cookieTimeoutArg, 'unfollow', followHandle], {
|
||||
timeoutMs: 45_000,
|
||||
});
|
||||
expect(unfollow.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
it('read returns tweet JSON', async () => {
|
||||
|
||||
Reference in New Issue
Block a user