Skip to content
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

refactor: introduce branded types #1223

Merged
merged 12 commits into from
Jun 13, 2024
3 changes: 2 additions & 1 deletion src/__mocks__/partial-mocks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Notification, Subject, User } from '../typesGitHub';
import type { HostName } from '../utils/branded-types';
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 Down
21 changes: 11 additions & 10 deletions src/__mocks__/state-mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import {
Theme,
} from '../types';
import type { EnterpriseAccount } from '../utils/auth/types';
import type { HostName, Token } from '../utils/branded-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 +26,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 +67,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/context/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useNotifications } from '../hooks/useNotifications';
import type { AuthState, SettingsState } from '../types';
import { mockSingleNotification } from '../utils/api/__mocks__/response-mocks';
import * as apiRequests from '../utils/api/request';
import type { HostName, Token } from '../utils/branded-types';
import * as comms from '../utils/comms';
import Constants from '../utils/constants';
import * as notifications from '../utils/notifications';
Expand Down Expand Up @@ -280,8 +281,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
7 changes: 4 additions & 3 deletions src/routes/LoginWithOAuthApp.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { shell } from 'electron';
import { MemoryRouter } from 'react-router-dom';
import { AppContext } from '../context/App';
import type { AuthState } from '../types';
import type { ClientID, ClientSecret, HostName } from '../utils/branded-types';
import { LoginWithOAuthApp, validate } from './LoginWithOAuthApp';

const mockNavigate = jest.fn();
Expand Down Expand Up @@ -64,9 +65,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
14 changes: 10 additions & 4 deletions src/routes/LoginWithOAuthApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,18 @@ import {
isValidHostname,
isValidToken,
} from '../utils/auth/utils';
import type {
ClientID,
ClientSecret,
HostName,
Token,
} from '../utils/branded-types';
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 +54,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 @@ -16,11 +16,12 @@ import {
isValidHostname,
isValidToken,
} from '../utils/auth/utils';
import type { HostName, Token } from '../utils/branded-types';
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
7 changes: 4 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
EnterpriseAccount,
PlatformType,
} from './utils/auth/types';
import type { HostName, Token } from './utils/branded-types';

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

Expand All @@ -14,7 +15,7 @@ export interface AuthState {
/**
* @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 @@ -28,8 +29,8 @@ export interface AuthState {
export interface Account {
method: AuthMethod;
platform: PlatformType;
hostname: string;
token: string;
hostname: HostName;
token: Token;
user: GitifyUser | null;
}

Expand Down
9 changes: 5 additions & 4 deletions src/utils/api/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
mockToken,
} from '../../__mocks__/state-mocks';
import type { SettingsState } from '../../types';
import type { HostName, Token } from '../branded-types';
import {
getAuthenticatedUser,
getHtmlUrl,
Expand All @@ -19,8 +20,8 @@ import * as apiRequests from './request';

jest.mock('axios');

const mockGitHubHostname = 'github.com';
const mockEnterpriseHostname = 'example.com';
const mockGitHubHostname = 'github.com' as HostName;
const mockEnterpriseHostname = 'example.com' as HostName;
const mockThreadId = '1234';
const mockRepoSlug = 'gitify-app/notifications-test';

Expand Down Expand Up @@ -270,7 +271,7 @@ describe('utils/api/client.ts', () => {

const result = await getHtmlUrl(
'https://api.github.com/repos/gitify-app/notifications-test/issues/785',
'123',
'123' as Token,
);
expect(result).toBe(
'https://github.com/gitify-app/notifications-test/issues/785',
Expand All @@ -288,7 +289,7 @@ describe('utils/api/client.ts', () => {

await getHtmlUrl(
'https://api.github.com/repos/gitify-app/gitify/issues/785',
'123',
'123' as Token,
);

expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to get html url');
Expand Down
41 changes: 21 additions & 20 deletions src/utils/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
Release,
UserDetails,
} from '../../typesGitHub';
import type { HostName, Token } from '../branded-types';
import { QUERY_SEARCH_DISCUSSIONS } from './graphql/discussions';
import { formatAsGitHubSearchSyntax } from './graphql/utils';
import { apiRequestAuth } from './request';
Expand All @@ -26,8 +27,8 @@ import { getGitHubAPIBaseUrl, getGitHubGraphQLUrl } from './utils';
* Endpoint documentation: https://docs.github.com/en/rest/users/users#get-the-authenticated-user
*/
export function getAuthenticatedUser(
hostname: string,
token: string,
hostname: HostName,
token: Token,
): AxiosPromise<UserDetails> {
const url = getGitHubAPIBaseUrl(hostname);
url.pathname += 'user';
Expand All @@ -37,8 +38,8 @@ export function getAuthenticatedUser(

//
export function headNotifications(
hostname: string,
token: string,
hostname: HostName,
token: Token,
): AxiosPromise<void> {
const url = getGitHubAPIBaseUrl(hostname);
url.pathname += 'notifications';
Expand Down Expand Up @@ -70,8 +71,8 @@ export function listNotificationsForAuthenticatedUser(
*/
export function markNotificationThreadAsRead(
threadId: string,
hostname: string,
token: string,
hostname: HostName,
token: Token,
): AxiosPromise<void> {
const url = getGitHubAPIBaseUrl(hostname);
url.pathname += `notifications/threads/${threadId}`;
Expand All @@ -87,8 +88,8 @@ export function markNotificationThreadAsRead(
*/
export function markNotificationThreadAsDone(
threadId: string,
hostname: string,
token: string,
hostname: HostName,
token: Token,
): AxiosPromise<void> {
const url = getGitHubAPIBaseUrl(hostname);
url.pathname += `notifications/threads/${threadId}`;
Expand All @@ -103,8 +104,8 @@ export function markNotificationThreadAsDone(
*/
export function ignoreNotificationThreadSubscription(
threadId: string,
hostname: string,
token: string,
hostname: HostName,
token: Token,
): AxiosPromise<NotificationThreadSubscription> {
const url = getGitHubAPIBaseUrl(hostname);
url.pathname += `notifications/threads/${threadId}/subscription`;
Expand All @@ -122,8 +123,8 @@ export function ignoreNotificationThreadSubscription(
*/
export function markRepositoryNotificationsAsRead(
repoSlug: string,
hostname: string,
token: string,
hostname: HostName,
token: Token,
): AxiosPromise<void> {
const url = getGitHubAPIBaseUrl(hostname);
url.pathname += `repos/${repoSlug}/notifications`;
Expand All @@ -136,7 +137,7 @@ export function markRepositoryNotificationsAsRead(
*
* Endpoint documentation: https://docs.github.com/en/rest/commits/commits#get-a-commit
*/
export function getCommit(url: string, token: string): AxiosPromise<Commit> {
export function getCommit(url: string, token: Token): AxiosPromise<Commit> {
return apiRequestAuth(url, 'GET', token);
}

Expand All @@ -148,7 +149,7 @@ export function getCommit(url: string, token: string): AxiosPromise<Commit> {
*/
export function getCommitComment(
url: string,
token: string,
token: Token,
): AxiosPromise<CommitComment> {
return apiRequestAuth(url, 'GET', token);
}
Expand All @@ -158,7 +159,7 @@ export function getCommitComment(
*
* Endpoint documentation: https://docs.github.com/en/rest/issues/issues#get-an-issue
*/
export function getIssue(url: string, token: string): AxiosPromise<Issue> {
export function getIssue(url: string, token: Token): AxiosPromise<Issue> {
return apiRequestAuth(url, 'GET', token);
}

Expand All @@ -170,7 +171,7 @@ export function getIssue(url: string, token: string): AxiosPromise<Issue> {
*/
export function getIssueOrPullRequestComment(
url: string,
token: string,
token: Token,
): AxiosPromise<IssueOrPullRequestComment> {
return apiRequestAuth(url, 'GET', token);
}
Expand All @@ -182,7 +183,7 @@ export function getIssueOrPullRequestComment(
*/
export function getPullRequest(
url: string,
token: string,
token: Token,
): AxiosPromise<PullRequest> {
return apiRequestAuth(url, 'GET', token);
}
Expand All @@ -194,7 +195,7 @@ export function getPullRequest(
*/
export function getPullRequestReviews(
url: string,
token: string,
token: Token,
): AxiosPromise<PullRequestReview[]> {
return apiRequestAuth(url, 'GET', token);
}
Expand All @@ -204,14 +205,14 @@ export function getPullRequestReviews(
*
* Endpoint documentation: https://docs.github.com/en/rest/releases/releases#get-a-release
*/
export function getRelease(url: string, token: string): AxiosPromise<Release> {
export function getRelease(url: string, token: Token): AxiosPromise<Release> {
return apiRequestAuth(url, 'GET', token);
}

/**
* Get the `html_url` from the GitHub response
*/
export async function getHtmlUrl(url: string, token: string): Promise<string> {
export async function getHtmlUrl(url: string, token: Token): Promise<string> {
try {
const response = (await apiRequestAuth(url, 'GET', token)).data;
return response.html_url;
Expand Down
Loading