2
0
mirror of https://github.com/QuantumNous/new-api.git synced 2026-08-31 02:41:34 +00:00

fix(web): restore admin unbinding for built-in providers (#6987)

* fix(web): align admin binding types

Refs #6985

* test(web): restore animation mock
This commit is contained in:
zcxads666 2026-08-29 13:34:22 +08:00 committed by GitHub
parent e468b73915
commit 692e8d6ee6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 154 additions and 6 deletions

View File

@ -0,0 +1,148 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'
import { api } from '@/lib/api'
import { UserBindingDialog } from '../user-binding-dialog'
type ApiMethod = (url: string) => Promise<{ data: unknown }>
type MockableApi = {
get: ApiMethod
delete: ApiMethod
}
const apiClient = api as unknown as MockableApi
const originalGet = apiClient.get
const originalDelete = apiClient.delete
const originalGetAnimations = Object.getOwnPropertyDescriptor(
HTMLElement.prototype,
'getAnimations'
)
const user = {
id: 7,
username: 'bound-user',
email: 'user@example.com',
github_id: 'github-user',
discord_id: 'discord-user',
wechat_id: 'wechat-user',
oidc_id: 'oidc-user',
telegram_id: 'telegram-user',
linux_do_id: 'linuxdo-user',
}
function findUnbindButton(provider: string): HTMLButtonElement {
let container = screen.getByText(provider).parentElement
while (container && !container.querySelector('button')) {
container = container.parentElement
}
const button = container?.querySelector<HTMLButtonElement>('button')
if (!button) {
throw new Error(`Expected unbind button for ${provider}`)
}
return button
}
beforeAll(() => {
Object.defineProperty(HTMLElement.prototype, 'getAnimations', {
configurable: true,
value: () => [],
})
})
afterAll(() => {
if (originalGetAnimations) {
Object.defineProperty(
HTMLElement.prototype,
'getAnimations',
originalGetAnimations
)
return
}
Reflect.deleteProperty(HTMLElement.prototype, 'getAnimations')
})
afterEach(() => {
apiClient.get = originalGet
apiClient.delete = originalDelete
})
describe('UserBindingDialog built-in bindings', () => {
test('submits every built-in provider type accepted by the backend', async () => {
const deletedUrls: string[] = []
apiClient.get = async (url) => {
switch (url) {
case '/api/user/7':
return { data: { success: true, data: user } }
case '/api/user/7/oauth/bindings':
return { data: { success: true, data: [] } }
case '/api/status':
return {
data: {
success: true,
data: {
github_oauth: true,
discord_oauth: true,
wechat_login: true,
oidc_enabled: true,
telegram_oauth: true,
linuxdo_oauth: true,
},
},
}
default:
throw new Error(`Unexpected GET ${url}`)
}
}
apiClient.delete = async (url) => {
deletedUrls.push(url)
return { data: { success: true, message: 'success' } }
}
render(<UserBindingDialog open userId={7} onOpenChange={() => undefined} />)
const expectedBindings = [
['Email', 'email'],
['GitHub', 'github'],
['Discord', 'discord'],
['WeChat', 'wechat'],
['OIDC', 'oidc'],
['Telegram', 'telegram'],
['LinuxDO', 'linuxdo'],
] as const
await screen.findByText('bound-user (ID: 7)')
for (const [provider, bindingType] of expectedBindings) {
fireEvent.click(findUnbindButton(provider))
fireEvent.click(screen.getByRole('button', { name: 'Confirm Unbind' }))
await waitFor(() => {
expect(deletedUrls.at(-1)).toBe(`/api/user/7/bindings/${bindingType}`)
})
await waitFor(() => {
expect(
screen.queryByRole('button', { name: 'Confirm Unbind' })
).not.toBeInTheDocument()
})
}
expect(deletedUrls).toHaveLength(expectedBindings.length)
})
})

View File

@ -102,42 +102,42 @@ const BUILTIN_BINDINGS: ReadonlyArray<{
statusKey: null,
},
{
key: 'github_id',
key: 'github',
field: 'github_id',
label: 'GitHub',
icon: <SiGithub className='h-4 w-4' />,
statusKey: 'github_oauth',
},
{
key: 'discord_id',
key: 'discord',
field: 'discord_id',
label: 'Discord',
icon: <SiDiscord className='h-4 w-4' />,
statusKey: 'discord_oauth',
},
{
key: 'wechat_id',
key: 'wechat',
field: 'wechat_id',
label: 'WeChat',
icon: <MessageCircle className='h-4 w-4' />,
statusKey: 'wechat_login',
},
{
key: 'oidc_id',
key: 'oidc',
field: 'oidc_id',
label: 'OIDC',
icon: <Globe className='h-4 w-4' />,
statusKey: 'oidc_enabled',
},
{
key: 'telegram_id',
key: 'telegram',
field: 'telegram_id',
label: 'Telegram',
icon: <Send className='h-4 w-4' />,
statusKey: 'telegram_oauth',
},
{
key: 'linux_do_id',
key: 'linuxdo',
field: 'linux_do_id',
label: 'LinuxDO',
icon: <Globe className='h-4 w-4' />,