Skip to content

Commit a1fa469

Browse files
authored
feat(auth): provide user choice on scopes (#2691)
* feat(auth): provide user choice on scopes Signed-off-by: Adam Setch <adam.setch@outlook.com> * feat(auth): provide user choice on scopes Signed-off-by: Adam Setch <adam.setch@outlook.com> * feat(auth): provide user choice on scopes Signed-off-by: Adam Setch <adam.setch@outlook.com> * chore: vscode formatter config Signed-off-by: Adam Setch <adam.setch@outlook.com> * feat: oauth scopes choice Signed-off-by: Adam Setch <adam.setch@outlook.com> --------- Signed-off-by: Adam Setch <adam.setch@outlook.com>
1 parent eb5c669 commit a1fa469

File tree

6 files changed

+526
-179
lines changed

6 files changed

+526
-179
lines changed

src/renderer/context/App.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,10 @@ import { defaultAuth, defaultSettings } from './defaults';
7272
export interface AppContextState {
7373
auth: AuthState;
7474
isLoggedIn: boolean;
75-
loginWithDeviceFlowStart: (hostname?: Hostname) => Promise<DeviceFlowSession>;
75+
loginWithDeviceFlowStart: (
76+
hostname?: Hostname,
77+
scopes?: string[],
78+
) => Promise<DeviceFlowSession>;
7679
loginWithDeviceFlowPoll: (
7780
session: DeviceFlowSession,
7881
) => Promise<Token | null>;
@@ -383,7 +386,8 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
383386
* Initiate device flow session.
384387
*/
385388
const loginWithDeviceFlowStart = useCallback(
386-
async (hostname?: Hostname) => await startGitHubDeviceFlow(hostname),
389+
async (hostname?: Hostname, scopes?: string[]) =>
390+
await startGitHubDeviceFlow(hostname, scopes),
387391
[],
388392
);
389393

src/renderer/routes/LoginWithDeviceFlow.test.tsx

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ describe('renderer/routes/LoginWithDeviceFlow.tsx', () => {
1818
vi.clearAllMocks();
1919
});
2020

21-
it('should render and initialize device flow', async () => {
21+
it('should render scope choice buttons', async () => {
2222
const loginWithDeviceFlowStartMock = vi.fn().mockResolvedValueOnce({
2323
hostname: 'github.com',
2424
clientId: 'test-id',
@@ -33,10 +33,39 @@ describe('renderer/routes/LoginWithDeviceFlow.tsx', () => {
3333
loginWithDeviceFlowStart: loginWithDeviceFlowStartMock,
3434
});
3535

36-
expect(loginWithDeviceFlowStartMock).toHaveBeenCalled();
36+
expect(screen.getByText('Receive notifications for:')).toBeInTheDocument();
37+
expect(screen.getByTestId('device-scope-public')).toBeInTheDocument();
38+
expect(screen.getByTestId('device-scope-full')).toBeInTheDocument();
3739

38-
await screen.findByText(/USER-1234/);
39-
expect(screen.getByText(/github.com\/login\/device/)).toBeInTheDocument();
40+
// Device flow should not start until user makes a choice
41+
expect(loginWithDeviceFlowStartMock).not.toHaveBeenCalled();
42+
});
43+
44+
it('should start device flow with public scope when clicking Public', async () => {
45+
const loginWithDeviceFlowStartMock = vi.fn().mockResolvedValueOnce({
46+
hostname: 'github.com',
47+
clientId: 'test-id',
48+
deviceCode: 'device-code',
49+
userCode: 'USER-1234',
50+
verificationUri: 'https://github.com/login/device',
51+
intervalSeconds: 5,
52+
expiresAt: Date.now() + 900000,
53+
});
54+
55+
renderWithAppContext(<LoginWithDeviceFlowRoute />, {
56+
loginWithDeviceFlowStart: loginWithDeviceFlowStartMock,
57+
});
58+
59+
await userEvent.click(screen.getByTestId('device-scope-public'));
60+
61+
expect(loginWithDeviceFlowStartMock).toHaveBeenCalledWith(undefined, [
62+
'notifications',
63+
'read:user',
64+
'public_repo',
65+
]);
66+
67+
await screen.findByTestId('device-user-code');
68+
expect(screen.getByTestId('device-verification-link')).toBeInTheDocument();
4069

4170
// Verify auto-copy and auto-open were called
4271
expect(copyToClipboardSpy).toHaveBeenCalledWith('USER-1234');
@@ -45,6 +74,32 @@ describe('renderer/routes/LoginWithDeviceFlow.tsx', () => {
4574
);
4675
});
4776

77+
it('should start device flow with full scope when clicking Public and Private', async () => {
78+
const loginWithDeviceFlowStartMock = vi.fn().mockResolvedValueOnce({
79+
hostname: 'github.com',
80+
clientId: 'test-id',
81+
deviceCode: 'device-code',
82+
userCode: 'USER-1234',
83+
verificationUri: 'https://github.com/login/device',
84+
intervalSeconds: 5,
85+
expiresAt: Date.now() + 900000,
86+
});
87+
88+
renderWithAppContext(<LoginWithDeviceFlowRoute />, {
89+
loginWithDeviceFlowStart: loginWithDeviceFlowStartMock,
90+
});
91+
92+
await userEvent.click(screen.getByTestId('device-scope-full'));
93+
94+
expect(loginWithDeviceFlowStartMock).toHaveBeenCalledWith(undefined, [
95+
'notifications',
96+
'read:user',
97+
'repo',
98+
]);
99+
100+
await screen.findByTestId('device-user-code');
101+
});
102+
48103
it('should copy user code to clipboard when clicking copy button', async () => {
49104
const loginWithDeviceFlowStartMock = vi.fn().mockResolvedValueOnce({
50105
hostname: 'github.com',
@@ -60,12 +115,13 @@ describe('renderer/routes/LoginWithDeviceFlow.tsx', () => {
60115
loginWithDeviceFlowStart: loginWithDeviceFlowStartMock,
61116
});
62117

63-
await screen.findByText(/USER-1234/);
118+
await userEvent.click(screen.getByTestId('device-scope-public'));
119+
await screen.findByTestId('device-user-code');
64120

65121
// Clear the auto-copy call from initialization
66122
copyToClipboardSpy.mockClear();
67123

68-
await userEvent.click(screen.getByLabelText('Copy device code'));
124+
await userEvent.click(screen.getByTestId('copy-device-code'));
69125

70126
expect(copyToClipboardSpy).toHaveBeenCalledWith('USER-1234');
71127
});
@@ -79,10 +135,12 @@ describe('renderer/routes/LoginWithDeviceFlow.tsx', () => {
79135
loginWithDeviceFlowStart: loginWithDeviceFlowStartMock,
80136
});
81137

138+
await userEvent.click(screen.getByTestId('device-scope-full'));
139+
82140
await screen.findByText(/Failed to start authentication/);
83141
});
84142

85-
it('should navigate back on cancel', async () => {
143+
it('should navigate back on cancel from scope choice', async () => {
86144
const loginWithDeviceFlowStartMock = vi.fn().mockResolvedValueOnce({
87145
hostname: 'github.com',
88146
clientId: 'test-id',
@@ -97,9 +155,7 @@ describe('renderer/routes/LoginWithDeviceFlow.tsx', () => {
97155
loginWithDeviceFlowStart: loginWithDeviceFlowStartMock,
98156
});
99157

100-
await screen.findByText(/USER-1234/);
101-
102-
await userEvent.click(screen.getByText('Cancel'));
158+
await userEvent.click(screen.getByTestId('cancel-button'));
103159

104160
expect(navigateMock).toHaveBeenCalledWith(-1);
105161
});

0 commit comments

Comments
 (0)