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) => {