fix(plugins): close R2 workspace gaps
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
||||
import { MemoryRouter, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { Login } from '@/pages/Login';
|
||||
import { dispatchPluginWorkspaceCommand } from '@/pages/Plugins/plugin-workspace-controller';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useProviderStore } from '@/stores/providers';
|
||||
|
||||
@@ -36,12 +37,63 @@ function resetAuthStore() {
|
||||
});
|
||||
}
|
||||
|
||||
function renderLogin() {
|
||||
function LocationProbe() {
|
||||
const location = useLocation();
|
||||
return <output data-testid="login-destination">{location.pathname}{location.search}</output>;
|
||||
}
|
||||
|
||||
function PluginSignInAction() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<>
|
||||
<output data-testid="login-destination">{location.pathname}{location.search}</output>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void dispatchPluginWorkspaceCommand({ kind: 'sign_in' }, {
|
||||
marketplace: {
|
||||
acquire: async () => undefined,
|
||||
remove: async () => undefined,
|
||||
install: async () => undefined,
|
||||
installBeta: async () => undefined,
|
||||
update: async () => undefined,
|
||||
uninstall: async () => undefined,
|
||||
},
|
||||
device: {
|
||||
setEnabled: async () => undefined,
|
||||
uninstall: async () => undefined,
|
||||
},
|
||||
project: { setEnabled: async () => undefined },
|
||||
loginReturnPath: `${location.pathname}${location.search}`,
|
||||
navigate,
|
||||
openSettings: () => undefined,
|
||||
})}
|
||||
>
|
||||
登录后获取插件
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function renderLogin(initialEntries: React.ComponentProps<typeof MemoryRouter>['initialEntries'] = ['/login']) {
|
||||
return render(
|
||||
<MemoryRouter initialEntries={['/login']}>
|
||||
<MemoryRouter initialEntries={initialEntries}>
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/module-select" element={<div>Module Selection</div>} />
|
||||
<Route path="/plugins" element={<LocationProbe />} />
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
}
|
||||
|
||||
function renderPluginLoginJourney(canonicalPath: string) {
|
||||
return render(
|
||||
<MemoryRouter initialEntries={[canonicalPath]}>
|
||||
<Routes>
|
||||
<Route path="/plugins" element={<PluginSignInAction />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/module-select" element={<div>Module Selection</div>} />
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
@@ -192,6 +244,39 @@ describe('Login page', () => {
|
||||
expect(importUserModelConfig).toHaveBeenCalledWith('password-access-token');
|
||||
});
|
||||
|
||||
it('returns a signed-out plugin action to the exact canonical workspace query after login', async () => {
|
||||
const canonicalPath = '/plugins?scope=all&source=official&state=available&q=notes&plugin=official%3Amakelore.notes';
|
||||
loginWithPassword.mockImplementation(async () => {
|
||||
useAuthStore.setState({ accessToken: 'password-access-token' });
|
||||
});
|
||||
renderPluginLoginJourney(canonicalPath);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '登录后获取插件' }));
|
||||
fireEvent.change(await screen.findByLabelText('用户名'), { target: { value: 'zhangsan' } });
|
||||
fireEvent.change(screen.getByLabelText('密码'), { target: { value: 'secret-password' } });
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: /我已阅读并同意/ }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '登录' }));
|
||||
|
||||
expect(await screen.findByTestId('login-destination')).toHaveTextContent(canonicalPath);
|
||||
});
|
||||
|
||||
it.each([
|
||||
'https://attacker.example/plugins',
|
||||
'//attacker.example/plugins',
|
||||
])('rejects unsafe login return %s and keeps the module chooser default', async (unsafeReturn) => {
|
||||
loginWithPassword.mockImplementation(async () => {
|
||||
useAuthStore.setState({ accessToken: 'password-access-token' });
|
||||
});
|
||||
renderLogin([{ pathname: '/login', state: { from: unsafeReturn } }]);
|
||||
|
||||
fireEvent.change(screen.getByLabelText('用户名'), { target: { value: 'zhangsan' } });
|
||||
fireEvent.change(screen.getByLabelText('密码'), { target: { value: 'secret-password' } });
|
||||
fireEvent.click(screen.getByRole('checkbox', { name: /我已阅读并同意/ }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '登录' }));
|
||||
|
||||
expect(await screen.findByText('Module Selection')).toBeVisible();
|
||||
});
|
||||
|
||||
it('validates a Chinese mobile number, uses one-time-code autocomplete, and submits only phone and SMS code', async () => {
|
||||
loginWithMobile.mockImplementation(async () => {
|
||||
useAuthStore.setState({ accessToken: 'mobile-access-token' });
|
||||
|
||||
Reference in New Issue
Block a user