Skip to content

Commit

Permalink
refactor: introduce branded types (#1223)
Browse files Browse the repository at this point in the history
* Add the hostname Branded type

* Commit branded-types file

* Add the token Branded type

* Add the ClientSecret and ClientID Branded types

* Add the AuthCode Branded type

* Rename HostName to Hostname

* Move types in branded-types to src/types

* Create WebUrl Branded type

* refactor: rename `WebUrl` to `Link`

---------

Co-authored-by: Adam Setch <[email protected]>
  • Loading branch information
dammy95 and setchy authored Jun 13, 2024
1 parent 8ccaf9f commit d3a4b17
Show file tree
Hide file tree
Showing 35 changed files with 580 additions and 476 deletions.
7 changes: 4 additions & 3 deletions src/__mocks__/partial-mocks.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Hostname, Link } from '../types';
import type { Notification, Subject, User } from '../typesGitHub';
import Constants from '../utils/constants';
import { mockGitifyUser, mockToken } from './state-mocks';
Expand All @@ -9,7 +10,7 @@ export function partialMockNotification(
account: {
method: 'Personal Access Token',
platform: 'GitHub Cloud',
hostname: Constants.GITHUB_API_BASE_URL,
hostname: Constants.GITHUB_API_BASE_URL as Hostname,
token: mockToken,
user: mockGitifyUser,
},
Expand All @@ -22,8 +23,8 @@ export function partialMockNotification(
export function partialMockUser(login: string): User {
const mockUser: Partial<User> = {
login: login,
html_url: `https://github.com/${login}`,
avatar_url: 'https://avatars.githubusercontent.com/u/583231?v=4',
html_url: `https://github.com/${login}` as Link,
avatar_url: 'https://avatars.githubusercontent.com/u/583231?v=4' as Link,
type: 'User',
};

Expand Down
22 changes: 12 additions & 10 deletions src/__mocks__/state-mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@ import {
type AuthState,
type GitifyState,
type GitifyUser,
type Hostname,
type SettingsState,
Theme,
type Token,
} from '../types';
import type { EnterpriseAccount } from '../utils/auth/types';
import Constants from '../utils/constants';

export const mockEnterpriseAccounts: EnterpriseAccount[] = [
{
hostname: 'github.gitify.io',
token: '1234568790',
hostname: 'github.gitify.io' as Hostname,
token: '1234568790' as Token,
},
];

Expand All @@ -25,39 +27,39 @@ export const mockGitifyUser: GitifyUser = {
export const mockPersonalAccessTokenAccount: Account = {
platform: 'GitHub Cloud',
method: 'Personal Access Token',
token: 'token-123-456',
token: 'token-123-456' as Token,
hostname: Constants.DEFAULT_AUTH_OPTIONS.hostname,
user: mockGitifyUser,
};

export const mockOAuthAccount: Account = {
platform: 'GitHub Enterprise Server',
method: 'OAuth App',
token: '1234568790',
hostname: 'github.gitify.io',
token: '1234568790' as Token,
hostname: 'github.gitify.io' as Hostname,
user: mockGitifyUser,
};

export const mockGitHubCloudAccount: Account = {
platform: 'GitHub Cloud',
method: 'Personal Access Token',
token: 'token-123-456',
token: 'token-123-456' as Token,
hostname: Constants.DEFAULT_AUTH_OPTIONS.hostname,
user: mockGitifyUser,
};

export const mockGitHubEnterpriseServerAccount: Account = {
platform: 'GitHub Enterprise Server',
method: 'Personal Access Token',
token: '1234568790',
hostname: 'github.gitify.io',
token: '1234568790' as Token,
hostname: 'github.gitify.io' as Hostname,
user: mockGitifyUser,
};

export const mockGitHubAppAccount: Account = {
platform: 'GitHub Cloud',
method: 'GitHub App',
token: '987654321',
token: '987654321' as Token,
hostname: Constants.DEFAULT_AUTH_OPTIONS.hostname,
user: mockGitifyUser,
};
Expand All @@ -66,7 +68,7 @@ export const mockAuth: AuthState = {
accounts: [mockGitHubCloudAccount, mockGitHubEnterpriseServerAccount],
};

export const mockToken = 'token-123-456';
export const mockToken = 'token-123-456' as Token;

export const mockSettings: SettingsState = {
participating: false,
Expand Down
5 changes: 3 additions & 2 deletions src/components/NotificationRow.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
mockSettings,
} from '../__mocks__/state-mocks';
import { AppContext } from '../context/App';
import type { Link } from '../types';
import type { Milestone, UserType } from '../typesGitHub';
import { mockSingleNotification } from '../utils/api/__mocks__/response-mocks';
import * as comms from '../utils/comms';
Expand Down Expand Up @@ -473,9 +474,9 @@ describe('components/NotificationRow.tsx', () => {
...mockSingleNotification.subject,
user: {
login: 'some-user',
html_url: 'https://github.com/some-user',
html_url: 'https://github.com/some-user' as Link,
avatar_url:
'https://avatars.githubusercontent.com/u/123456789?v=4',
'https://avatars.githubusercontent.com/u/123456789?v=4' as Link,
type: 'User' as UserType,
},
reviews: null,
Expand Down
3 changes: 2 additions & 1 deletion src/components/Repository.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { mockGitHubCloudAccount } from '../__mocks__/state-mocks';
import { AppContext } from '../context/App';
import type { Link } from '../types';
import {
mockGitHubNotifications,
mockSingleNotification,
Expand Down Expand Up @@ -81,7 +82,7 @@ describe('components/Repository.tsx', () => {
});

it('should use default repository icon when avatar is not available', () => {
props.repoNotifications[0].repository.owner.avatar_url = '';
props.repoNotifications[0].repository.owner.avatar_url = '' as Link;

const tree = render(
<AppContext.Provider value={{}}>
Expand Down
3 changes: 2 additions & 1 deletion src/components/fields/Button.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { MarkGithubIcon } from '@primer/octicons-react';
import { fireEvent, render, screen } from '@testing-library/react';
import { shell } from 'electron';
import type { Link } from '../../types';
import { Button, type IButton } from './Button';

describe('components/fields/Button.tsx', () => {
Expand All @@ -27,7 +28,7 @@ describe('components/fields/Button.tsx', () => {
});

it('should render with url', () => {
render(<Button {...props} url="https://github.com" />);
render(<Button {...props} url={'https://github.com' as Link} />);

const buttonElement = screen.getByLabelText('button');

Expand Down
3 changes: 2 additions & 1 deletion src/components/fields/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Icon } from '@primer/octicons-react';
import type { FC } from 'react';
import type { Link } from '../../types';
import { cn } from '../../utils/cn';
import { openExternalLink } from '../../utils/comms';

Expand All @@ -9,7 +10,7 @@ export interface IButton {
className?: string;
icon?: Icon;
size?: number;
url?: string;
url?: Link;
onClick?: () => void;
disabled?: boolean;
type?: 'button' | 'submit';
Expand Down
6 changes: 3 additions & 3 deletions src/context/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { act, fireEvent, render, waitFor } from '@testing-library/react';
import { useContext } from 'react';
import { mockAuth, mockSettings } from '../__mocks__/state-mocks';
import { useNotifications } from '../hooks/useNotifications';
import type { AuthState, SettingsState } from '../types';
import type { AuthState, Hostname, SettingsState, Token } from '../types';
import { mockSingleNotification } from '../utils/api/__mocks__/response-mocks';
import * as apiRequests from '../utils/api/request';
import * as comms from '../utils/comms';
Expand Down Expand Up @@ -280,8 +280,8 @@ describe('context/App.tsx', () => {
type="button"
onClick={() =>
loginWithPersonalAccessToken({
hostname: 'github.com',
token: '123-456',
hostname: 'github.com' as Hostname,
token: '123-456' as Token,
})
}
>
Expand Down
8 changes: 4 additions & 4 deletions src/routes/LoginWithOAuthApp.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { fireEvent, render, screen } from '@testing-library/react';
import { shell } from 'electron';
import { MemoryRouter } from 'react-router-dom';
import { AppContext } from '../context/App';
import type { AuthState } from '../types';
import type { AuthState, ClientID, ClientSecret, Hostname } from '../types';
import { LoginWithOAuthApp, validate } from './LoginWithOAuthApp';

const mockNavigate = jest.fn();
Expand Down Expand Up @@ -64,9 +64,9 @@ describe('routes/LoginWithOAuthApp.tsx', () => {

values = {
...emptyValues,
hostname: 'hello',
clientId: '!@£INVALID-.1',
clientSecret: '!@£INVALID-.1',
hostname: 'hello' as Hostname,
clientId: '!@£INVALID-.1' as ClientID,
clientSecret: '!@£INVALID-.1' as ClientSecret,
};
expect(validate(values).hostname).toBe('Invalid hostname.');
expect(validate(values).clientId).toBe('Invalid client id.');
Expand Down
9 changes: 5 additions & 4 deletions src/routes/LoginWithOAuthApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useNavigate } from 'react-router-dom';
import { Button } from '../components/fields/Button';
import { FieldInput } from '../components/fields/FieldInput';
import { AppContext } from '../context/App';
import type { ClientID, ClientSecret, Hostname, Token } from '../types';
import type { LoginOAuthAppOptions } from '../utils/auth/types';
import {
getNewOAuthAppURL,
Expand All @@ -20,9 +21,9 @@ import {
import Constants from '../utils/constants';

interface IValues {
hostname?: string;
clientId?: string;
clientSecret?: string;
hostname?: Hostname;
clientId?: ClientID;
clientSecret?: ClientSecret;
}

interface IFormErrors {
Expand All @@ -48,7 +49,7 @@ export const validate = (values: IValues): IFormErrors => {

if (!values.clientSecret) {
errors.clientSecret = 'Required';
} else if (!isValidToken(values.clientSecret)) {
} else if (!isValidToken(values.clientSecret as unknown as Token)) {
errors.clientSecret = 'Invalid client secret.';
}

Expand Down
7 changes: 4 additions & 3 deletions src/routes/LoginWithPersonalAccessToken.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useNavigate } from 'react-router-dom';
import { Button } from '../components/fields/Button';
import { FieldInput } from '../components/fields/FieldInput';
import { AppContext } from '../context/App';
import type { Hostname, Token } from '../types';
import type { LoginPersonalAccessTokenOptions } from '../utils/auth/types';
import {
getNewTokenURL,
Expand All @@ -19,8 +20,8 @@ import {
import { Constants } from '../utils/constants';

interface IValues {
token?: string;
hostname?: string;
token?: Token;
hostname?: Hostname;
}

interface IFormErrors {
Expand Down Expand Up @@ -158,7 +159,7 @@ export const LoginWithPersonalAccessToken: FC = () => {
<Form
initialValues={{
hostname: Constants.DEFAULT_AUTH_OPTIONS.hostname,
token: '',
token: '' as Token,
}}
onSubmit={login}
validate={validate}
Expand Down
26 changes: 22 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ import type {
PlatformType,
} from './utils/auth/types';

export type Status = 'loading' | 'success' | 'error';
declare const __brand: unique symbol;

type Brand<B> = { [__brand]: B };

export interface AuthState {
accounts: Account[];
/**
* @deprecated This attribute is deprecated and will be removed in a future release.
*/
token?: string;
token?: Token;
/**
* @deprecated This attribute is deprecated and will be removed in a future release.
*/
Expand All @@ -25,11 +27,27 @@ export interface AuthState {
user?: GitifyUser | null;
}

export type Branded<T, B> = T & Brand<B>;

export type AuthCode = Branded<string, 'AuthCode'>;

export type Token = Branded<string, 'Token'>;

export type ClientID = Branded<string, 'ClientID'>;

export type ClientSecret = Branded<string, 'ClientSecret'>;

export type Hostname = Branded<string, 'Hostname'>;

export type Link = Branded<string, 'WebUrl'>;

export type Status = 'loading' | 'success' | 'error';

export interface Account {
method: AuthMethod;
platform: PlatformType;
hostname: string;
token: string;
hostname: Hostname;
token: Token;
user: GitifyUser | null;
}

Expand Down
Loading

0 comments on commit d3a4b17

Please sign in to comment.