Skip to content
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
75 changes: 75 additions & 0 deletions src/js/actions/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,81 @@ describe('actions/index.js', () => {
});
});

it('should unsubscribe from a notification thread with success - github.com', () => {
const id = 123;
const hostname = 'github.com';

// The unsubscribe endpoint call.
nock('https://api.github.com/')
.delete(`/notifications/threads/${id}/subscription`)
.reply(204);

// The mark read endpoint call.
nock('https://api.github.com/')
.patch(`/notifications/threads/${id}`)
.reply(200);

const expectedActions = [
{ type: actions.UNSUBSCRIBE_NOTIFICATION.REQUEST },
{
type: actions.UNSUBSCRIBE_NOTIFICATION.SUCCESS,
meta: { id, hostname },
},
];

const store = createMockStore(
{
auth: {
token: 'THISISATOKEN',
enterpriseAccounts: mockedEnterpriseAccounts,
},
notifications: { response: [] },
},
expectedActions
);

return store
.dispatch(actions.unsubscribeNotification(id, hostname))
.then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});

it('should unsubscribe from a notification thread with failure - github.com', () => {
const id = 123;
const hostname = 'github.com';
const message = 'Oops! Something went wrong.';

nock('https://api.github.com/')
.delete(`/notifications/threads/${id}/subscription`)
.reply(400, { message });

const expectedActions = [
{ type: actions.UNSUBSCRIBE_NOTIFICATION.REQUEST },
{ type: actions.UNSUBSCRIBE_NOTIFICATION.FAILURE, payload: { message } },
];

const store = createMockStore(
{
auth: {
token: 'THISISATOKEN',
enterpriseAccounts: mockedEnterpriseAccounts,
},
settings: {
participating: false,
},
notifications: { response: [] },
},
expectedActions
);

return store
.dispatch(actions.unsubscribeNotification(id, hostname))
.then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});

it('should mark a notification as read with success - github.com', () => {
const id = 123;
const hostname = 'github.com';
Expand Down
64 changes: 55 additions & 9 deletions src/js/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ export function makeAsyncActionSet(actionName) {
};
}

enum Methods {
GET = 'GET',
POST = 'POST',
PUT = 'PUT',
PATCH = 'PATCH',
DELETE = 'DELETE',
}

// Authentication

export const LOGIN = makeAsyncActionSet('LOGIN');
Expand All @@ -27,7 +35,6 @@ export function loginUser(authOptions, code) {

return (dispatch) => {
const url = `https://${hostname}/login/oauth/access_token`;
const method = 'POST';
const data = {
client_id: authOptions.clientId,
client_secret: authOptions.clientSecret,
Expand All @@ -36,7 +43,7 @@ export function loginUser(authOptions, code) {

dispatch({ type: LOGIN.REQUEST });

return apiRequest(url, method, data)
return apiRequest(url, Methods.POST, data)
.then(function (response) {
dispatch({
type: LOGIN.SUCCESS,
Expand All @@ -60,7 +67,6 @@ export function logout(): LogoutAction {
export const NOTIFICATIONS = makeAsyncActionSet('NOTIFICATIONS');
export function fetchNotifications() {
return (dispatch, getState: () => AppState) => {
const method = 'GET';
const { settings }: { settings: SettingsState } = getState();
const isGitHubLoggedIn = getState().auth.token !== null;
const endpointSuffix = `notifications?participating=${settings.participating}`;
Expand All @@ -72,7 +78,7 @@ export function fetchNotifications() {

const url = `https://api.${Constants.DEFAULT_AUTH_OPTIONS.hostname}/${endpointSuffix}`;
const token = getState().auth.token;
return apiRequestAuth(url, method, token);
return apiRequestAuth(url, Methods.GET, token);
}

function getEnterpriseNotifications() {
Expand All @@ -81,7 +87,7 @@ export function fetchNotifications() {
const hostname = account.hostname;
const token = account.token;
const url = `https://${hostname}/api/v3/${endpointSuffix}`;
return apiRequestAuth(url, method, token);
return apiRequestAuth(url, Methods.GET, token);
});
}

Expand Down Expand Up @@ -130,7 +136,6 @@ export const MARK_NOTIFICATION = makeAsyncActionSet('MARK_NOTIFICATION');
export function markNotification(id, hostname) {
return (dispatch, getState: () => AppState) => {
const url = `${generateGitHubAPIUrl(hostname)}notifications/threads/${id}`;
const method = 'PATCH';

const isEnterprise = hostname !== Constants.DEFAULT_AUTH_OPTIONS.hostname;
const entAccounts = getState().auth.enterpriseAccounts;
Expand All @@ -140,7 +145,7 @@ export function markNotification(id, hostname) {

dispatch({ type: MARK_NOTIFICATION.REQUEST });

return apiRequestAuth(url, method, token, {})
return apiRequestAuth(url, Methods.PATCH, token, {})
.then(function (response) {
dispatch({
type: MARK_NOTIFICATION.SUCCESS,
Expand All @@ -156,6 +161,48 @@ export function markNotification(id, hostname) {
};
}

export const UNSUBSCRIBE_NOTIFICATION = makeAsyncActionSet(
'UNSUBSCRIBE_NOTIFICATION'
);
export function unsubscribeNotification(id, hostname) {
return (dispatch, getState: () => AppState) => {
const markReadURL = `${generateGitHubAPIUrl(
hostname
)}notifications/threads/${id}`;
const unsubscribeURL = `${generateGitHubAPIUrl(
hostname
)}notifications/threads/${id}/subscription`;

const isEnterprise = hostname !== Constants.DEFAULT_AUTH_OPTIONS.hostname;
const entAccounts = getState().auth.enterpriseAccounts;
const token = isEnterprise
? getEnterpriseAccountToken(hostname, entAccounts)
: getState().auth.token;

dispatch({ type: UNSUBSCRIBE_NOTIFICATION.REQUEST });

return apiRequestAuth(unsubscribeURL, Methods.DELETE, token, {})
.then((response) => {
// The GitHub notifications API doesn't automatically mark things as read
// like it does in the UI, so after unsubscribing we also need to hit the
// endpoint to mark it as read.
return apiRequestAuth(markReadURL, Methods.PATCH, token, {});
})
.then((response) => {
dispatch({
type: UNSUBSCRIBE_NOTIFICATION.SUCCESS,
meta: { id, hostname },
});
})
.catch((error) => {
dispatch({
type: UNSUBSCRIBE_NOTIFICATION.FAILURE,
payload: error.response.data,
});
});
};
}

// Repo's Notification

export const MARK_REPO_NOTIFICATION = makeAsyncActionSet(
Expand All @@ -166,7 +213,6 @@ export function markRepoNotifications(repoSlug, hostname) {
const url = `${generateGitHubAPIUrl(
hostname
)}repos/${repoSlug}/notifications`;
const method = 'PUT';

const isEnterprise = hostname !== Constants.DEFAULT_AUTH_OPTIONS.hostname;
const entAccounts = getState().auth.enterpriseAccounts;
Expand All @@ -176,7 +222,7 @@ export function markRepoNotifications(repoSlug, hostname) {

dispatch({ type: MARK_REPO_NOTIFICATION.REQUEST });

return apiRequestAuth(url, method, token, {})
return apiRequestAuth(url, Methods.PUT, token, {})
.then(function (response) {
dispatch({
type: MARK_REPO_NOTIFICATION.SUCCESS,
Expand Down
28 changes: 28 additions & 0 deletions src/js/components/__snapshots__/notification.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,34 @@ exports[`components/notification.js should render itself & its children 1`] = `
- Updated

in over 3 years
<button
className="sc-AxheI hMkKFy"
onClick={[Function]}
title="Unsubscribe"
>
<svg
aria-hidden="false"
aria-label="Unsubscribe"
className="octicon"
height={13}
role="img"
style={
Object {
"display": "inline-block",
"fill": "currentColor",
"userSelect": "none",
"verticalAlign": "text-bottom",
}
}
viewBox="0 0 16 16"
width={13}
>
<path
d="M8 2.81v10.38c0 .67-.81 1-1.28.53L3 10H1c-.55 0-1-.45-1-1V7c0-.55.45-1 1-1h2l3.72-3.72C7.19 1.81 8 2.14 8 2.81zm7.53 3.22l-1.06-1.06-1.97 1.97-1.97-1.97-1.06 1.06L11.44 8 9.47 9.97l1.06 1.06 1.97-1.97 1.97 1.97 1.06-1.06L13.56 8l1.97-1.97z"
fillRule="evenodd"
/>
</svg>
</button>
</div>
</div>
<div
Expand Down
34 changes: 26 additions & 8 deletions src/js/components/notification.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ describe('components/notification.js', () => {

const props = {
markNotification: jest.fn(),
unsubscribeNotification: jest.fn(),
markOnClick: false,
notification: notification,
hostname: 'github.com',
Expand All @@ -44,6 +45,7 @@ describe('components/notification.js', () => {
it('should open a notification in the browser', () => {
const props = {
markNotification: jest.fn(),
unsubscribeNotification: jest.fn(),
markOnClick: false,
notification: notification,
hostname: 'github.com',
Expand All @@ -54,30 +56,46 @@ describe('components/notification.js', () => {
expect(shell.openExternal).toHaveBeenCalledTimes(1);
});

it('should mark a notification as read', () => {
it('should open a notification in browser & mark it as read', () => {
const props = {
markNotification: jest.fn(),
markOnClick: false,
unsubscribeNotification: jest.fn(),
markOnClick: true,
notification: notification,
hostname: 'github.com',
};

const { getByRole } = render(<NotificationItem {...props} />);
fireEvent.click(getByRole('button'));
fireEvent.click(getByRole('main'));
expect(shell.openExternal).toHaveBeenCalledTimes(1);
expect(props.markNotification).toHaveBeenCalledTimes(1);
});

it('should open a notification in browser & mark it as read', () => {
it('should mark a notification as read', () => {
const props = {
markNotification: jest.fn(),
markOnClick: true,
unsubscribeNotification: jest.fn(),
markOnClick: false,
notification: notification,
hostname: 'github.com',
};

const { getByRole } = render(<NotificationItem {...props} />);
fireEvent.click(getByRole('main'));
expect(shell.openExternal).toHaveBeenCalledTimes(1);
const { getByLabelText } = render(<NotificationItem {...props} />);
fireEvent.click(getByLabelText('Mark as Read'));
expect(props.markNotification).toHaveBeenCalledTimes(1);
});

it('should unsubscribe from a notification thread', () => {
const props = {
markNotification: jest.fn(),
unsubscribeNotification: jest.fn(),
markOnClick: false,
notification: notification,
hostname: 'github.com',
};

const { getByLabelText } = render(<NotificationItem {...props} />);
fireEvent.click(getByLabelText('Unsubscribe'));
expect(props.unsubscribeNotification).toHaveBeenCalledTimes(1);
});
});
Loading