fix: harden bookmark retry handling
This commit is contained in:
@@ -541,7 +541,7 @@ export function withTimelines<TBase extends AbstractConstructor<TwitterClientBas
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Retry-After supports delta-seconds only; HTTP-date falls back to backoff.
|
// Retry-After supports delta-seconds only; HTTP-date falls back to backoff.
|
||||||
const retryAfter = response.headers.get('retry-after');
|
const retryAfter = response.headers?.get?.('retry-after');
|
||||||
const retryAfterMs = retryAfter ? Number.parseInt(retryAfter, 10) * 1000 : Number.NaN;
|
const retryAfterMs = retryAfter ? Number.parseInt(retryAfter, 10) * 1000 : Number.NaN;
|
||||||
const backoffMs = Number.isFinite(retryAfterMs)
|
const backoffMs = Number.isFinite(retryAfterMs)
|
||||||
? retryAfterMs
|
? retryAfterMs
|
||||||
|
|||||||
@@ -17,12 +17,6 @@ describe('bookmarks command', () => {
|
|||||||
} as unknown as CliContext;
|
} as unknown as CliContext;
|
||||||
|
|
||||||
registerBookmarksCommand(program, ctx);
|
registerBookmarksCommand(program, ctx);
|
||||||
const command = program.commands.find((cmd) => cmd.name() === 'bookmarks');
|
|
||||||
if (!command) {
|
|
||||||
throw new Error('bookmarks command not registered');
|
|
||||||
}
|
|
||||||
|
|
||||||
const action = (command as { _actionHandler: (opts: Record<string, string>) => Promise<void> })._actionHandler;
|
|
||||||
const exitSpy = vi
|
const exitSpy = vi
|
||||||
.spyOn(process, 'exit')
|
.spyOn(process, 'exit')
|
||||||
.mockImplementation(((code?: number) => {
|
.mockImplementation(((code?: number) => {
|
||||||
@@ -31,7 +25,7 @@ describe('bookmarks command', () => {
|
|||||||
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await expect(action({ maxPages: '2' })).rejects.toThrow('exit 1');
|
await expect(program.parseAsync(['node', 'bird', 'bookmarks', '--max-pages', '2'])).rejects.toThrow('exit 1');
|
||||||
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('--max-pages requires --all'));
|
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('--max-pages requires --all'));
|
||||||
} finally {
|
} finally {
|
||||||
exitSpy.mockRestore();
|
exitSpy.mockRestore();
|
||||||
|
|||||||
@@ -155,10 +155,14 @@ describe('TwitterClient API coverage', () => {
|
|||||||
it('returns an error for non-ok responses', async () => {
|
it('returns an error for non-ok responses', async () => {
|
||||||
const mockFetch = vi
|
const mockFetch = vi
|
||||||
.fn()
|
.fn()
|
||||||
|
.mockResolvedValueOnce(makeResponse({ ok: false, status: 500, text: async () => 'down' }))
|
||||||
|
.mockResolvedValueOnce(makeResponse({ ok: false, status: 500, text: async () => 'down' }))
|
||||||
.mockResolvedValueOnce(makeResponse({ ok: false, status: 500, text: async () => 'down' }));
|
.mockResolvedValueOnce(makeResponse({ ok: false, status: 500, text: async () => 'down' }));
|
||||||
global.fetch = mockFetch as unknown as typeof fetch;
|
global.fetch = mockFetch as unknown as typeof fetch;
|
||||||
|
|
||||||
const client = new TwitterClient({ cookies: validCookies });
|
const client = new TwitterClient({ cookies: validCookies });
|
||||||
|
const clientPrivate = client as unknown as TwitterClientApiPrivate;
|
||||||
|
clientPrivate.getBookmarksQueryIds = async () => ['test'];
|
||||||
const result = await client.getBookmarks(1);
|
const result = await client.getBookmarks(1);
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
@@ -174,6 +178,8 @@ describe('TwitterClient API coverage', () => {
|
|||||||
global.fetch = mockFetch as unknown as typeof fetch;
|
global.fetch = mockFetch as unknown as typeof fetch;
|
||||||
|
|
||||||
const client = new TwitterClient({ cookies: validCookies });
|
const client = new TwitterClient({ cookies: validCookies });
|
||||||
|
const clientPrivate = client as unknown as TwitterClientApiPrivate;
|
||||||
|
clientPrivate.getBookmarksQueryIds = async () => ['test'];
|
||||||
const result = await client.getBookmarks(1);
|
const result = await client.getBookmarks(1);
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
@@ -185,6 +191,8 @@ describe('TwitterClient API coverage', () => {
|
|||||||
global.fetch = mockFetch as unknown as typeof fetch;
|
global.fetch = mockFetch as unknown as typeof fetch;
|
||||||
|
|
||||||
const client = new TwitterClient({ cookies: validCookies });
|
const client = new TwitterClient({ cookies: validCookies });
|
||||||
|
const clientPrivate = client as unknown as TwitterClientApiPrivate;
|
||||||
|
clientPrivate.getBookmarksQueryIds = async () => ['test'];
|
||||||
const result = await client.getBookmarks(1);
|
const result = await client.getBookmarks(1);
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
@@ -362,10 +370,14 @@ describe('TwitterClient API coverage', () => {
|
|||||||
it('returns an error for non-ok responses', async () => {
|
it('returns an error for non-ok responses', async () => {
|
||||||
const mockFetch = vi
|
const mockFetch = vi
|
||||||
.fn()
|
.fn()
|
||||||
|
.mockResolvedValueOnce(makeResponse({ ok: false, status: 500, text: async () => 'down' }))
|
||||||
|
.mockResolvedValueOnce(makeResponse({ ok: false, status: 500, text: async () => 'down' }))
|
||||||
.mockResolvedValueOnce(makeResponse({ ok: false, status: 500, text: async () => 'down' }));
|
.mockResolvedValueOnce(makeResponse({ ok: false, status: 500, text: async () => 'down' }));
|
||||||
global.fetch = mockFetch as unknown as typeof fetch;
|
global.fetch = mockFetch as unknown as typeof fetch;
|
||||||
|
|
||||||
const client = new TwitterClient({ cookies: validCookies });
|
const client = new TwitterClient({ cookies: validCookies });
|
||||||
|
const clientPrivate = client as unknown as TwitterClientApiPrivate;
|
||||||
|
clientPrivate.getBookmarkFolderQueryIds = async () => ['test'];
|
||||||
const result = await client.getBookmarkFolderTimeline('123', 1);
|
const result = await client.getBookmarkFolderTimeline('123', 1);
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
@@ -381,6 +393,8 @@ describe('TwitterClient API coverage', () => {
|
|||||||
global.fetch = mockFetch as unknown as typeof fetch;
|
global.fetch = mockFetch as unknown as typeof fetch;
|
||||||
|
|
||||||
const client = new TwitterClient({ cookies: validCookies });
|
const client = new TwitterClient({ cookies: validCookies });
|
||||||
|
const clientPrivate = client as unknown as TwitterClientApiPrivate;
|
||||||
|
clientPrivate.getBookmarkFolderQueryIds = async () => ['test'];
|
||||||
const result = await client.getBookmarkFolderTimeline('123', 1);
|
const result = await client.getBookmarkFolderTimeline('123', 1);
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
@@ -392,6 +406,8 @@ describe('TwitterClient API coverage', () => {
|
|||||||
global.fetch = mockFetch as unknown as typeof fetch;
|
global.fetch = mockFetch as unknown as typeof fetch;
|
||||||
|
|
||||||
const client = new TwitterClient({ cookies: validCookies });
|
const client = new TwitterClient({ cookies: validCookies });
|
||||||
|
const clientPrivate = client as unknown as TwitterClientApiPrivate;
|
||||||
|
clientPrivate.getBookmarkFolderQueryIds = async () => ['test'];
|
||||||
const result = await client.getBookmarkFolderTimeline('123', 1);
|
const result = await client.getBookmarkFolderTimeline('123', 1);
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user