diff --git a/frontend/src/views/admin/AccountsView.vue b/frontend/src/views/admin/AccountsView.vue index 6cc73880de..233fef9791 100644 --- a/frontend/src/views/admin/AccountsView.vue +++ b/frontend/src/views/admin/AccountsView.vue @@ -1891,10 +1891,13 @@ const handleBulkResetStatus = async () => { } const handleBulkRefreshToken = async () => { if (!confirm(t('common.confirm'))) return + const accountIds = [...selIds.value] try { - const result = await adminAPI.accounts.batchRefresh(selIds.value) + const result = await adminAPI.accounts.batchRefresh(accountIds) if (result.failed > 0) { appStore.showError(t('admin.accounts.bulkActions.partialSuccess', { success: result.success, failed: result.failed })) + const failedIds = result.errors?.map(error => error.account_id) ?? [] + setSelectedIds(failedIds.length > 0 ? failedIds : accountIds) } else { appStore.showSuccess(t('admin.accounts.bulkActions.refreshTokenSuccess', { count: result.success })) clearSelection() diff --git a/frontend/src/views/admin/__tests__/AccountsView.selectAllResults.spec.ts b/frontend/src/views/admin/__tests__/AccountsView.selectAllResults.spec.ts index e3fe225224..c80ddcaaab 100644 --- a/frontend/src/views/admin/__tests__/AccountsView.selectAllResults.spec.ts +++ b/frontend/src/views/admin/__tests__/AccountsView.selectAllResults.spec.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { flushPromises, mount } from '@vue/test-utils' import AccountsView from '../AccountsView.vue' @@ -6,6 +6,7 @@ import AccountsView from '../AccountsView.vue' const { listAccounts, listWithEtag, + batchRefresh, getBatchTodayStats, getUpstreamBillingProbeSettings, getAllProxies, @@ -14,6 +15,7 @@ const { } = vi.hoisted(() => ({ listAccounts: vi.fn(), listWithEtag: vi.fn(), + batchRefresh: vi.fn(), getBatchTodayStats: vi.fn(), getUpstreamBillingProbeSettings: vi.fn(), getAllProxies: vi.fn(), @@ -30,7 +32,7 @@ vi.mock('@/api/admin', () => ({ getUpstreamBillingProbeSettings, batchDelete: vi.fn(), batchClearError: vi.fn(), - batchRefresh: vi.fn(), + batchRefresh, bulkUpdate: vi.fn() }, proxies: { @@ -79,7 +81,7 @@ const makeAccounts = (count: number) => Array.from({ length: count }, (_, index) const AccountBulkActionsBarStub = { props: ['selectedIds', 'totalResults', 'selectingAll', 'allResultsSelected'], - emits: ['select-all-results', 'select-page', 'clear'], + emits: ['select-all-results', 'select-page', 'clear', 'refresh-token'], template: `
{{ selectedIds.length }} @@ -88,6 +90,7 @@ const AccountBulkActionsBarStub = { +
` } @@ -104,7 +107,10 @@ const mountView = () => mount(AccountsView, { TablePageLayout: { template: '
' }, - DataTable: { props: ['data'], template: '
' }, + DataTable: { + props: ['data'], + template: '
' + }, Pagination: true, ConfirmDialog: true, AccountTableActions: { template: '
' }, @@ -139,6 +145,7 @@ describe('admin AccountsView select all filtered results', () => { localStorage.clear() listAccounts.mockReset() listWithEtag.mockReset() + batchRefresh.mockReset() getBatchTodayStats.mockReset() getUpstreamBillingProbeSettings.mockReset() getAllProxies.mockReset() @@ -156,6 +163,38 @@ describe('admin AccountsView select all filtered results', () => { getAllGroups.mockResolvedValue([]) }) + afterEach(() => { + vi.restoreAllMocks() + }) + + it.each([ + { name: 'keeps only failed accounts selected', result: { total: 3, success: 2, failed: 1, errors: [{ account_id: 2, error: 'no refresh token available' }] }, expectedIds: [2] }, + { name: 'clears the selection after every account succeeds', result: { total: 3, success: 3, failed: 0 }, expectedIds: [] }, + { name: 'keeps the original selection when failure details are missing', result: { total: 3, success: 2, failed: 1 }, expectedIds: [1, 2, 3] }, + ])('$name after a batch token refresh and table reload', async ({ result, expectedIds }) => { + listAccounts.mockResolvedValue({ items: makeAccounts(3), total: 3, page: 1, page_size: 20, pages: 1 }) + batchRefresh.mockResolvedValue(result) + vi.spyOn(window, 'confirm').mockReturnValue(true) + const wrapper = mountView() + await flushPromises() + await wrapper.get('[data-test="select-page"]').trigger('click') + await wrapper.get('[data-test="refresh-token"]').trigger('click') + await flushPromises() + + expect(batchRefresh).toHaveBeenCalledWith([1, 2, 3]) + expect(listAccounts).toHaveBeenCalledTimes(2) + expect(wrapper.getComponent(AccountBulkActionsBarStub).props('selectedIds')).toEqual(expectedIds) + expect(wrapper.findAll('[data-test="data-table"] input').map(input => input.element.checked)) + .toEqual([1, 2, 3].map(id => expectedIds.includes(id))) + if (result.failed > 0) { + expect(showError).toHaveBeenCalledWith('admin.accounts.bulkActions.partialSuccess') + await wrapper.get('[data-test="refresh-token"]').trigger('click') + await flushPromises() + expect(batchRefresh).toHaveBeenLastCalledWith(expectedIds) + } + wrapper.unmount() + }) + it('selects all matching IDs in one commit and clears the selection when filters change', async () => { const allAccounts = makeAccounts(45) listAccounts.mockImplementation(async (_page: number, pageSize: number) => {