Skip to content

feat: new account management view #1172

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/__mocks__/state-mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ export const mockGitHubEnterpriseServerAccount: Account = {
user: mockGitifyUser,
};

export const mockGitHubAppAccount: Account = {
platform: 'GitHub Cloud',
method: 'GitHub App',
token: '987654321',
hostname: Constants.DEFAULT_AUTH_OPTIONS.hostname,
user: mockGitifyUser,
};

export const mockAuth: AuthState = {
accounts: [mockGitHubCloudAccount, mockGitHubEnterpriseServerAccount],
};
Expand Down
10 changes: 9 additions & 1 deletion src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { Loading } from './components/Loading';
import { Sidebar } from './components/Sidebar';
import { AppContext, AppProvider } from './context/App';
import { AccountsRoute } from './routes/Accounts';
import { LoginRoute } from './routes/Login';
import { LoginWithOAuthApp } from './routes/LoginWithOAuthApp';
import { LoginWithPersonalAccessToken } from './routes/LoginWithPersonalAccessToken';
Expand Down Expand Up @@ -50,7 +51,14 @@ export const App = () => {
</RequireAuth>
}
/>

<Route
path="/accounts"
element={
<RequireAuth>
<AccountsRoute />
</RequireAuth>
}
/>
<Route path="/login" element={<LoginRoute />} />
<Route
path="/login-personal-access-token"
Expand Down
13 changes: 13 additions & 0 deletions src/context/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { useInterval } from '../hooks/useInterval';
import { useNotifications } from '../hooks/useNotifications';
import {
type Account,
type AccountNotifications,
type AuthState,
type GitifyError,
Expand All @@ -27,6 +28,7 @@ import {
authGitHub,
getToken,
getUserData,
removeAccount,
} from '../utils/auth/utils';
import { setAutoLaunch, updateTrayTitle } from '../utils/comms';
import Constants from '../utils/constants';
Expand Down Expand Up @@ -62,6 +64,7 @@ interface AppContextState {
loginWithGitHubApp: () => void;
loginWithOAuthApp: (data: LoginOAuthAppOptions) => void;
loginWithPersonalAccessToken: (data: LoginPersonalAccessTokenOptions) => void;
logoutFromAccount: (account: Account) => void;
logout: () => void;

notifications: AccountNotifications[];
Expand Down Expand Up @@ -193,6 +196,15 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
[auth, settings],
);

const logoutFromAccount = useCallback(
async (account: Account) => {
const updatedAuth = removeAccount(auth, account);
setAuth(updatedAuth);
saveState({ auth: updatedAuth, settings });
},
[auth, settings],
);

const logout = useCallback(() => {
setAuth(defaultAuth);
clearState();
Expand Down Expand Up @@ -256,6 +268,7 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
loginWithGitHubApp,
loginWithOAuthApp,
loginWithPersonalAccessToken,
logoutFromAccount,
logout,

notifications,
Expand Down
279 changes: 279 additions & 0 deletions src/routes/Accounts.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
import { act, fireEvent, render, screen } from '@testing-library/react';
import { ipcRenderer, shell } from 'electron';
import { MemoryRouter } from 'react-router-dom';
import {
mockAuth,
mockGitHubAppAccount,
mockOAuthAccount,
mockPersonalAccessTokenAccount,
mockSettings,
} from '../__mocks__/state-mocks';
import { AppContext } from '../context/App';
import { AccountsRoute } from './Accounts';

const mockNavigate = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => mockNavigate,
}));

describe('routes/Accounts.tsx', () => {
beforeEach(() => {
jest.clearAllMocks();
});

describe('General', () => {
it('should render itself & its children', async () => {
await act(async () => {
render(
<AppContext.Provider
value={{
auth: {
accounts: [
mockPersonalAccessTokenAccount,
mockOAuthAccount,
mockGitHubAppAccount,
],
},
settings: mockSettings,
}}
>
<MemoryRouter>
<AccountsRoute />
</MemoryRouter>
</AppContext.Provider>,
);
});

expect(screen.getByTestId('accounts')).toMatchSnapshot();
});

it('should go back by pressing the icon', async () => {
await act(async () => {
render(
<AppContext.Provider
value={{
auth: mockAuth,
settings: mockSettings,
}}
>
<MemoryRouter>
<AccountsRoute />
</MemoryRouter>
</AppContext.Provider>,
);
});

fireEvent.click(screen.getByLabelText('Go Back'));
expect(mockNavigate).toHaveBeenNthCalledWith(1, -1);
});
});

describe('Account interactions', () => {
it('open profile in external browser', async () => {
await act(async () => {
render(
<AppContext.Provider
value={{
auth: {
accounts: [mockPersonalAccessTokenAccount],
},
settings: mockSettings,
}}
>
<MemoryRouter>
<AccountsRoute />
</MemoryRouter>
</AppContext.Provider>,
);
});

fireEvent.click(screen.getByTitle('Open Profile'));

expect(shell.openExternal).toHaveBeenCalledTimes(1);
expect(shell.openExternal).toHaveBeenCalledWith(
'https://github.com/octocat',
);
});

it('open host in external browser', async () => {
await act(async () => {
render(
<AppContext.Provider
value={{
auth: {
accounts: [mockPersonalAccessTokenAccount],
},
settings: mockSettings,
}}
>
<MemoryRouter>
<AccountsRoute />
</MemoryRouter>
</AppContext.Provider>,
);
});

fireEvent.click(screen.getByTitle('Open Host'));

expect(shell.openExternal).toHaveBeenCalledTimes(1);
expect(shell.openExternal).toHaveBeenCalledWith('https://github.com');
});

it('open developer settings in external browser', async () => {
await act(async () => {
render(
<AppContext.Provider
value={{
auth: {
accounts: [mockPersonalAccessTokenAccount],
},
settings: mockSettings,
}}
>
<MemoryRouter>
<AccountsRoute />
</MemoryRouter>
</AppContext.Provider>,
);
});

fireEvent.click(screen.getByTitle('Open Developer Settings'));

expect(shell.openExternal).toHaveBeenCalledTimes(1);
expect(shell.openExternal).toHaveBeenCalledWith(
'https://github.com/settings/tokens',
);
});

it('should logout', async () => {
const logoutFromAccountMock = jest.fn();
await act(async () => {
render(
<AppContext.Provider
value={{
auth: {
accounts: [mockPersonalAccessTokenAccount],
},
settings: mockSettings,
logoutFromAccount: logoutFromAccountMock,
}}
>
<MemoryRouter>
<AccountsRoute />
</MemoryRouter>
</AppContext.Provider>,
);
});

fireEvent.click(screen.getByTitle('Logout octocat'));

expect(logoutFromAccountMock).toHaveBeenCalledTimes(1);

expect(ipcRenderer.send).toHaveBeenCalledTimes(2);
expect(ipcRenderer.send).toHaveBeenCalledWith('update-icon');
expect(ipcRenderer.send).toHaveBeenCalledWith('update-title', '');
expect(mockNavigate).toHaveBeenNthCalledWith(1, -1);
});
});

describe('Add new accounts', () => {
describe('Login with Personal Access Token', () => {
it('should show login with personal access token button if not logged in', async () => {
await act(async () => {
render(
<AppContext.Provider
value={{
auth: { accounts: [mockOAuthAccount] },
settings: mockSettings,
}}
>
<MemoryRouter>
<AccountsRoute />
</MemoryRouter>
</AppContext.Provider>,
);
});

expect(
screen.getByTitle('Login with Personal Access Token').hidden,
).toBe(false);

fireEvent.click(screen.getByTitle('Login with Personal Access Token'));
expect(mockNavigate).toHaveBeenNthCalledWith(
1,
'/login-personal-access-token',
{
replace: true,
},
);
});

it('should hide login with personal access token button if already logged in', async () => {
await act(async () => {
render(
<AppContext.Provider
value={{
auth: { accounts: [mockPersonalAccessTokenAccount] },
settings: mockSettings,
}}
>
<MemoryRouter>
<AccountsRoute />
</MemoryRouter>
</AppContext.Provider>,
);
});

expect(
screen.getByTitle('Login with Personal Access Token').hidden,
).toBe(true);
});
});

describe('Login with OAuth App', () => {
it('should show login with oauth app if not logged in', async () => {
await act(async () => {
render(
<AppContext.Provider
value={{
auth: { accounts: [mockPersonalAccessTokenAccount] },
settings: mockSettings,
}}
>
<MemoryRouter>
<AccountsRoute />
</MemoryRouter>
</AppContext.Provider>,
);
});

expect(screen.getByTitle('Login with OAuth App').hidden).toBe(false);

fireEvent.click(screen.getByTitle('Login with OAuth App'));
expect(mockNavigate).toHaveBeenNthCalledWith(1, '/login-oauth-app', {
replace: true,
});
});

it('should hide login with oauth app route if already logged in', async () => {
await act(async () => {
render(
<AppContext.Provider
value={{
auth: { accounts: [mockOAuthAccount] },
settings: mockSettings,
}}
>
<MemoryRouter>
<AccountsRoute />
</MemoryRouter>
</AppContext.Provider>,
);
});

expect(screen.getByTitle('Login with OAuth App').hidden).toBe(true);
});
});
});
});
Loading