Repository: gitify-app/gitify
Pull request #901 (2024-03-19 19:20:09)

fix: improve error handling
Improve handling of attributes which may be null or responses that may be null.

I found some of these causing issues when testing locally


Patch:
diff --git a/src/utils/api-requests.ts b/src/utils/api-requests.ts
index 8938d52b..47e0066e 100644
--- a/src/utils/api-requests.ts
+++ b/src/utils/api-requests.ts
@@ -4,7 +4,7 @@ export function apiRequest(
   url: string,
   method: Method,
   data = {},
-): AxiosPromise {
+): AxiosPromise | null {
   axios.defaults.headers.common['Accept'] = 'application/json';
   axios.defaults.headers.common['Content-Type'] = 'application/json';
   axios.defaults.headers.common['Cache-Control'] = 'no-cache';
@@ -16,7 +16,7 @@ export function apiRequestAuth(
   method: Method,
   token: string,
   data = {},
-): AxiosPromise {
+): AxiosPromise | null {
   axios.defaults.headers.common['Accept'] = 'application/json';
   axios.defaults.headers.common['Authorization'] = `token ${token}`;
   axios.defaults.headers.common['Cache-Control'] = 'no-cache';
diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts
index 6da0b252..016605e8 100644
--- a/src/utils/helpers.ts
+++ b/src/utils/helpers.ts
@@ -136,8 +136,9 @@ async function getDiscussionUrl(
 
     let latestCommentId: string | number;
 
-    if (comments?.length) {
-      latestCommentId = getLatestDiscussionComment(comments)?.databaseId;
+    latestCommentId = getLatestDiscussionComment(comments)?.databaseId;
+
+    if (latestCommentId) {
       url += `#discussioncomment-${latestCommentId}`;
     }
   }
@@ -218,9 +219,13 @@ export async function fetchDiscussion(
 export function getLatestDiscussionComment(
   comments: DiscussionCommentNode[],
 ): DiscussionSubcommentNode | null {
+  if (!comments || comments.length == 0) {
+    return null;
+  }
+
   return comments
     .flatMap((comment) => comment.replies.nodes)
-    .concat([comments.at(-1)])
+    .concat([comments[comments.length - 1]])
     .reduce((a, b) => (a.createdAt > b.createdAt ? a : b));
 }
 
diff --git a/src/utils/subject.ts b/src/utils/subject.ts
index e4d2a051..96969a12 100644
--- a/src/utils/subject.ts
+++ b/src/utils/subject.ts
@@ -107,16 +107,12 @@ async function getGitifySubjectForDiscussion(
     }
   }
 
-  let comments = discussion.comments.nodes;
-  let discussionUser = null;
-
-  if (comments?.length) {
-    discussionUser = getLatestDiscussionComment(comments)?.author.login;
-  }
+  const discussionUser = getLatestDiscussionComment(discussion.comments.nodes)
+    ?.author.login;
 
   return {
     state: discussionState,
-    user: discussionUser,
+    user: discussionUser ?? null,
   };
 }
 
@@ -132,7 +128,7 @@ async function getGitifySubjectForIssue(
 
   return {
     state: issue.state_reason ?? issue.state,
-    user: issueCommentUser.login,
+    user: issueCommentUser?.login ?? null,
   };
 }
 
@@ -144,8 +140,6 @@ async function getGitifySubjectForPullRequest(
     await apiRequestAuth(notification.subject.url, 'GET', token)
   ).data;
 
-  const prCommentUser = await getLatestCommentUser(notification, token);
-
   let prState: PullRequestStateType = pr.state;
   if (pr.merged) {
     prState = 'merged';
@@ -153,9 +147,11 @@ async function getGitifySubjectForPullRequest(
     prState = 'draft';
   }
 
+  const prCommentUser = await getLatestCommentUser(notification, token);
+
   return {
     state: prState,
-    user: prCommentUser.login,
+    user: prCommentUser?.login ?? null,
   };
 }
 
@@ -217,12 +213,16 @@ function getWorkflowRunStatus(statusDisplayName: string): CheckSuiteStatus {
 async function getLatestCommentUser(
   notification: Notification,
   token: string,
-): Promise<User> {
+): Promise<User> | null {
+  if (!notification.subject.latest_comment_url) {
+    return null;
+  }
+
   const response: IssueComments | ReleaseComments = (
     await apiRequestAuth(notification.subject.latest_comment_url, 'GET', token)
-  ).data;
+  )?.data;
 
   return (
-    (response as IssueComments).user ?? (response as ReleaseComments).author
+    (response as IssueComments)?.user ?? (response as ReleaseComments).author
   );
 }


Test patch:
diff --git a/src/utils/__snapshots__/api-requests.test.ts.snap b/src/utils/__snapshots__/api-requests.test.ts.snap
index 86a0ee8e..420db2eb 100644
--- a/src/utils/__snapshots__/api-requests.test.ts.snap
+++ b/src/utils/__snapshots__/api-requests.test.ts.snap
@@ -1,34 +1,34 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`apiRequest should make a request with the correct parameters 1`] = `
+exports[`apiRequestAuth should make an authenticated request with the correct parameters 1`] = `
 {
   "Accept": "application/json",
+  "Authorization": "token yourAuthToken",
   "Cache-Control": "no-cache",
   "Content-Type": "application/json",
 }
 `;
 
-exports[`apiRequest should make a request with the correct parameters and default data 1`] = `
+exports[`apiRequestAuth should make an authenticated request with the correct parameters and default data 1`] = `
 {
   "Accept": "application/json",
+  "Authorization": "token yourAuthToken",
   "Cache-Control": "no-cache",
   "Content-Type": "application/json",
 }
 `;
 
-exports[`apiRequestAuth should make an authenticated request with the correct parameters 1`] = `
+exports[`utils/api-requests.ts should make a request with the correct parameters 1`] = `
 {
   "Accept": "application/json",
-  "Authorization": "token yourAuthToken",
   "Cache-Control": "no-cache",
   "Content-Type": "application/json",
 }
 `;
 
-exports[`apiRequestAuth should make an authenticated request with the correct parameters and default data 1`] = `
+exports[`utils/api-requests.ts should make a request with the correct parameters and default data 1`] = `
 {
   "Accept": "application/json",
-  "Authorization": "token yourAuthToken",
   "Cache-Control": "no-cache",
   "Content-Type": "application/json",
 }
diff --git a/src/utils/api-requests.test.ts b/src/utils/api-requests.test.ts
index 9bb71648..7d0bc663 100644
--- a/src/utils/api-requests.test.ts
+++ b/src/utils/api-requests.test.ts
@@ -6,7 +6,11 @@ jest.mock('axios');
 const url = 'https://example.com';
 const method = 'get';
 
-describe('apiRequest', () => {
+describe('utils/api-requests.ts', () => {
+  afterEach(() => {
+    jest.resetAllMocks();
+  });
+
   it('should make a request with the correct parameters', async () => {
     const data = { key: 'value' };
 
@@ -38,6 +42,10 @@ describe('apiRequest', () => {
 describe('apiRequestAuth', () => {
   const token = 'yourAuthToken';
 
+  afterEach(() => {
+    jest.resetAllMocks();
+  });
+
   it('should make an authenticated request with the correct parameters', async () => {
     const data = { key: 'value' };
 
diff --git a/src/utils/helpers.test.ts b/src/utils/helpers.test.ts
index bea3101c..dfe3f6e7 100644
--- a/src/utils/helpers.test.ts
+++ b/src/utils/helpers.test.ts
@@ -445,6 +445,34 @@ describe('utils/helpers.ts', () => {
           `https://github.com/manosim/notifications-test/discussions/612?${mockedNotificationReferrer}#discussioncomment-2300902`,
         );
       });
+
+      it('default to base discussions url when graphql query fails', async () => {
+        const subject = {
+          title: '1.16.0',
+          url: null,
+          latest_comment_url: null,
+          type: 'Discussion' as SubjectType,
+        };
+
+        const requestPromise = new Promise((resolve) =>
+          resolve(null as AxiosResponse),
+        ) as AxiosPromise;
+
+        apiRequestAuthMock.mockResolvedValue(requestPromise);
+
+        const result = await generateGitHubWebUrl(
+          {
+            ...mockedSingleNotification,
+            subject: subject,
+          },
+          mockAccounts,
+        );
+
+        expect(apiRequestAuthMock).toHaveBeenCalledTimes(1);
+        expect(result).toBe(
+          `https://github.com/manosim/notifications-test/discussions?${mockedNotificationReferrer}`,
+        );
+      });
     });
 
     it('Repository Invitation url', async () => {
diff --git a/src/utils/subject.test.ts b/src/utils/subject.test.ts
index b2dc8597..77f3c2dd 100644
--- a/src/utils/subject.test.ts
+++ b/src/utils/subject.test.ts
@@ -134,461 +134,503 @@ describe('utils/subject.ts', () => {
     });
   });
 
-  describe('getGitifySubjectForDiscussion', () => {
-    it('answered discussion state', async () => {
-      const mockNotification = {
-        ...mockedSingleNotification,
-        subject: {
-          ...mockedSingleNotification.subject,
-          title: 'This is an answered discussion',
-          type: 'Discussion' as SubjectType,
-        },
-      };
-
-      nock('https://api.github.com')
-        .post('/graphql')
-        .reply(200, {
-          data: {
-            search: {
-              nodes: [
-                {
-                  title: 'This is an answered discussion',
-                  viewerSubscription: 'SUBSCRIBED',
-                  stateReason: null,
-                  isAnswered: true,
-                  comments: {
-                    nodes: [], //TODO - Update this to have real data
+  describe('getGitifySubjectDetails', () => {
+    describe('Discussions', () => {
+      it('answered discussion state', async () => {
+        const mockNotification = {
+          ...mockedSingleNotification,
+          subject: {
+            ...mockedSingleNotification.subject,
+            title: 'This is an answered discussion',
+            type: 'Discussion' as SubjectType,
+          },
+        };
+
+        nock('https://api.github.com')
+          .post('/graphql')
+          .reply(200, {
+            data: {
+              search: {
+                nodes: [
+                  {
+                    title: 'This is an answered discussion',
+                    viewerSubscription: 'SUBSCRIBED',
+                    stateReason: null,
+                    isAnswered: true,
+                    comments: {
+                      nodes: [], //TODO - Update this to have real data
+                    },
                   },
-                },
-              ],
+                ],
+              },
             },
+          });
+
+        const result = await getGitifySubjectDetails(
+          mockNotification,
+          mockAccounts.token,
+        );
+
+        expect(result.state).toBe('ANSWERED');
+        expect(result.user).toBe(null);
+      });
+
+      it('duplicate discussion state', async () => {
+        const mockNotification = {
+          ...mockedSingleNotification,
+          subject: {
+            ...mockedSingleNotification.subject,
+            title: 'This is a duplicate discussion',
+            type: 'Discussion' as SubjectType,
           },
-        });
-
-      const result = await getGitifySubjectDetails(
-        mockNotification,
-        mockAccounts.token,
-      );
-
-      expect(result.state).toBe('ANSWERED');
-      expect(result.user).toBe(null);
-    });
-
-    it('duplicate discussion state', async () => {
-      const mockNotification = {
-        ...mockedSingleNotification,
-        subject: {
-          ...mockedSingleNotification.subject,
-          title: 'This is a duplicate discussion',
-          type: 'Discussion' as SubjectType,
-        },
-      };
-
-      nock('https://api.github.com')
-        .post('/graphql')
-        .reply(200, {
-          data: {
-            search: {
-              nodes: [
-                {
-                  title: 'This is a duplicate discussion',
-                  viewerSubscription: 'SUBSCRIBED',
-                  stateReason: 'DUPLICATE',
-                  isAnswered: false,
-                  comments: {
-                    nodes: [], //TODO - Update this to have real data
+        };
+
+        nock('https://api.github.com')
+          .post('/graphql')
+          .reply(200, {
+            data: {
+              search: {
+                nodes: [
+                  {
+                    title: 'This is a duplicate discussion',
+                    viewerSubscription: 'SUBSCRIBED',
+                    stateReason: 'DUPLICATE',
+                    isAnswered: false,
+                    comments: {
+                      nodes: [], //TODO - Update this to have real data
+                    },
                   },
-                },
-              ],
+                ],
+              },
             },
+          });
+
+        const result = await getGitifySubjectDetails(
+          mockNotification,
+          mockAccounts.token,
+        );
+
+        expect(result.state).toBe('DUPLICATE');
+        expect(result.user).toBe(null);
+      });
+
+      it('open discussion state', async () => {
+        const mockNotification = {
+          ...mockedSingleNotification,
+          subject: {
+            ...mockedSingleNotification.subject,
+            title: 'This is an open discussion',
+            type: 'Discussion' as SubjectType,
           },
-        });
-
-      const result = await getGitifySubjectDetails(
-        mockNotification,
-        mockAccounts.token,
-      );
-
-      expect(result.state).toBe('DUPLICATE');
-      expect(result.user).toBe(null);
-    });
-
-    it('open discussion state', async () => {
-      const mockNotification = {
-        ...mockedSingleNotification,
-        subject: {
-          ...mockedSingleNotification.subject,
-          title: 'This is an open discussion',
-          type: 'Discussion' as SubjectType,
-        },
-      };
-
-      nock('https://api.github.com')
-        .post('/graphql')
-        .reply(200, {
-          data: {
-            search: {
-              nodes: [
-                {
-                  title: 'This is an open discussion',
-                  viewerSubscription: 'SUBSCRIBED',
-                  stateReason: null,
-                  isAnswered: false,
-                  comments: {
-                    nodes: [], //TODO - Update this to have real data
+        };
+
+        nock('https://api.github.com')
+          .post('/graphql')
+          .reply(200, {
+            data: {
+              search: {
+                nodes: [
+                  {
+                    title: 'This is an open discussion',
+                    viewerSubscription: 'SUBSCRIBED',
+                    stateReason: null,
+                    isAnswered: false,
+                    comments: {
+                      nodes: [], //TODO - Update this to have real data
+                    },
                   },
-                },
-              ],
+                ],
+              },
             },
+          });
+
+        const result = await getGitifySubjectDetails(
+          mockNotification,
+          mockAccounts.token,
+        );
+
+        expect(result.state).toBe('OPEN');
+        expect(result.user).toBe(null);
+      });
+
+      it('outdated discussion state', async () => {
+        const mockNotification = {
+          ...mockedSingleNotification,
+          subject: {
+            ...mockedSingleNotification.subject,
+            title: 'This is an outdated discussion',
+            type: 'Discussion' as SubjectType,
           },
-        });
-
-      const result = await getGitifySubjectDetails(
-        mockNotification,
-        mockAccounts.token,
-      );
-
-      expect(result.state).toBe('OPEN');
-      expect(result.user).toBe(null);
-    });
-
-    it('outdated discussion state', async () => {
-      const mockNotification = {
-        ...mockedSingleNotification,
-        subject: {
-          ...mockedSingleNotification.subject,
-          title: 'This is an outdated discussion',
-          type: 'Discussion' as SubjectType,
-        },
-      };
-
-      nock('https://api.github.com')
-        .post('/graphql')
-        .reply(200, {
-          data: {
-            search: {
-              nodes: [
-                {
-                  title: 'This is an outdated discussion',
-                  viewerSubscription: 'SUBSCRIBED',
-                  stateReason: 'OUTDATED',
-                  isAnswered: false,
-                  comments: {
-                    nodes: [], //TODO - Update this to have real data
+        };
+
+        nock('https://api.github.com')
+          .post('/graphql')
+          .reply(200, {
+            data: {
+              search: {
+                nodes: [
+                  {
+                    title: 'This is an outdated discussion',
+                    viewerSubscription: 'SUBSCRIBED',
+                    stateReason: 'OUTDATED',
+                    isAnswered: false,
+                    comments: {
+                      nodes: [], //TODO - Update this to have real data
+                    },
                   },
-                },
-              ],
+                ],
+              },
             },
+          });
+
+        const result = await getGitifySubjectDetails(
+          mockNotification,
+          mockAccounts.token,
+        );
+
+        expect(result.state).toBe('OUTDATED');
+        expect(result.user).toBe(null);
+      });
+
+      it('reopened discussion state', async () => {
+        const mockNotification = {
+          ...mockedSingleNotification,
+          subject: {
+            ...mockedSingleNotification.subject,
+            title: 'This is a reopened discussion',
+            type: 'Discussion' as SubjectType,
           },
-        });
-
-      const result = await getGitifySubjectDetails(
-        mockNotification,
-        mockAccounts.token,
-      );
-
-      expect(result.state).toBe('OUTDATED');
-      expect(result.user).toBe(null);
-    });
-
-    it('reopened discussion state', async () => {
-      const mockNotification = {
-        ...mockedSingleNotification,
-        subject: {
-          ...mockedSingleNotification.subject,
-          title: 'This is a reopened discussion',
-          type: 'Discussion' as SubjectType,
-        },
-      };
-
-      nock('https://api.github.com')
-        .post('/graphql')
-        .reply(200, {
-          data: {
-            search: {
-              nodes: [
-                {
-                  title: 'This is a reopened discussion',
-                  viewerSubscription: 'SUBSCRIBED',
-                  stateReason: 'REOPENED',
-                  isAnswered: false,
-                  comments: {
-                    nodes: [], //TODO - Update this to have real data
+        };
+
+        nock('https://api.github.com')
+          .post('/graphql')
+          .reply(200, {
+            data: {
+              search: {
+                nodes: [
+                  {
+                    title: 'This is a reopened discussion',
+                    viewerSubscription: 'SUBSCRIBED',
+                    stateReason: 'REOPENED',
+                    isAnswered: false,
+                    comments: {
+                      nodes: [], //TODO - Update this to have real data
+                    },
                   },
-                },
-              ],
+                ],
+              },
             },
+          });
+
+        const result = await getGitifySubjectDetails(
+          mockNotification,
+          mockAccounts.token,
+        );
+
+        expect(result.state).toBe('REOPENED');
+        expect(result.user).toBe(null);
+      });
+
+      it('resolved discussion state', async () => {
+        const mockNotification = {
+          ...mockedSingleNotification,
+          subject: {
+            ...mockedSingleNotification.subject,
+            title: 'This is a resolved discussion',
+            type: 'Discussion' as SubjectType,
           },
-        });
+        };
+
+        nock('https://api.github.com')
+          .post('/graphql')
+          .reply(200, {
+            data: {
+              search: {
+                nodes: [
+                  {
+                    title: 'This is a resolved discussion',
+                    viewerSubscription: 'SUBSCRIBED',
+                    stateReason: 'RESOLVED',
+                    isAnswered: false,
+                    comments: {
+                      nodes: [], //TODO - Update this to have real data
+                    },
+                  },
+                ],
+              },
+            },
+          });
+
+        const result = await getGitifySubjectDetails(
+          mockNotification,
+          mockAccounts.token,
+        );
+
+        expect(result.state).toBe('RESOLVED');
+        expect(result.user).toBe(null);
+      });
+
+      it('filtered response by subscribed', async () => {
+        const mockNotification = {
+          ...mockedSingleNotification,
+          subject: {
+            ...mockedSingleNotification.subject,
+            title: 'This is a discussion',
+            type: 'Discussion' as SubjectType,
+          },
+        };
+
+        nock('https://api.github.com')
+          .post('/graphql')
+          .reply(200, {
+            data: {
+              search: {
+                nodes: [
+                  {
+                    title: 'This is a discussion',
+                    viewerSubscription: 'SUBSCRIBED',
+                    stateReason: null,
+                    isAnswered: false,
+                    comments: {
+                      nodes: [], //TODO - Update this to have real data
+                    },
+                  },
+                  {
+                    title: 'This is a discussion',
+                    viewerSubscription: 'IGNORED',
+                    stateReason: null,
+                    isAnswered: true,
+                    comments: {
+                      nodes: [], //TODO - Update this to have real data
+                    },
+                  },
+                ],
+              },
+            },
+          });
 
-      const result = await getGitifySubjectDetails(
-        mockNotification,
-        mockAccounts.token,
-      );
+        const result = await getGitifySubjectDetails(
+          mockNotification,
+          mockAccounts.token,
+        );
 
-      expect(result.state).toBe('REOPENED');
-      expect(result.user).toBe(null);
+        expect(result.state).toBe('OPEN');
+        expect(result.user).toBe(null);
+      });
     });
 
-    it('resolved discussion state', async () => {
-      const mockNotification = {
-        ...mockedSingleNotification,
-        subject: {
-          ...mockedSingleNotification.subject,
-          title: 'This is a resolved discussion',
-          type: 'Discussion' as SubjectType,
-        },
-      };
-
-      nock('https://api.github.com')
-        .post('/graphql')
-        .reply(200, {
-          data: {
-            search: {
-              nodes: [
-                {
-                  title: 'This is a resolved discussion',
-                  viewerSubscription: 'SUBSCRIBED',
-                  stateReason: 'RESOLVED',
-                  isAnswered: false,
-                  comments: {
-                    nodes: [], //TODO - Update this to have real data
-                  },
-                },
-              ],
+    describe('Issues', () => {
+      it('open issue state', async () => {
+        nock('https://api.github.com')
+          .get('/repos/manosim/notifications-test/issues/1')
+          .reply(200, { state: 'open' });
+
+        nock('https://api.github.com')
+          .get('/repos/manosim/notifications-test/issues/comments/302888448')
+          .reply(200, { user: { login: 'some-user' } });
+
+        const result = await getGitifySubjectDetails(
+          mockedSingleNotification,
+          mockAccounts.token,
+        );
+
+        expect(result.state).toBe('open');
+        expect(result.user).toBe('some-user');
+      });
+
+      it('closed issue state', async () => {
+        nock('https://api.github.com')
+          .get('/repos/manosim/notifications-test/issues/1')
+          .reply(200, { state: 'closed' });
+
+        nock('https://api.github.com')
+          .get('/repos/manosim/notifications-test/issues/comments/302888448')
+          .reply(200, { user: { login: 'some-user' } });
+
+        const result = await getGitifySubjectDetails(
+          mockedSingleNotification,
+          mockAccounts.token,
+        );
+
+        expect(result.state).toBe('closed');
+        expect(result.user).toBe('some-user');
+      });
+
+      it('completed issue state', async () => {
+        nock('https://api.github.com')
+          .get('/repos/manosim/notifications-test/issues/1')
+          .reply(200, { state: 'closed', state_reason: 'completed' });
+
+        nock('https://api.github.com')
+          .get('/repos/manosim/notifications-test/issues/comments/302888448')
+          .reply(200, { user: { login: 'some-user' } });
+
+        const result = await getGitifySubjectDetails(
+          mockedSingleNotification,
+          mockAccounts.token,
+        );
+
+        expect(result.state).toBe('completed');
+        expect(result.user).toBe('some-user');
+      });
+
+      it('not_planned issue state', async () => {
+        nock('https://api.github.com')
+          .get('/repos/manosim/notifications-test/issues/1')
+          .reply(200, { state: 'open', state_reason: 'not_planned' });
+
+        nock('https://api.github.com')
+          .get('/repos/manosim/notifications-test/issues/comments/302888448')
+          .reply(200, { user: { login: 'some-user' } });
+
+        const result = await getGitifySubjectDetails(
+          mockedSingleNotification,
+          mockAccounts.token,
+        );
+
+        expect(result.state).toBe('not_planned');
+        expect(result.user).toBe('some-user');
+      });
+
+      it('reopened issue state', async () => {
+        nock('https://api.github.com')
+          .get('/repos/manosim/notifications-test/issues/1')
+          .reply(200, { state: 'open', state_reason: 'reopened' });
+
+        nock('https://api.github.com')
+          .get('/repos/manosim/notifications-test/issues/comments/302888448')
+          .reply(200, { user: { login: 'some-user' } });
+
+        const result = await getGitifySubjectDetails(
+          mockedSingleNotification,
+          mockAccounts.token,
+        );
+
+        expect(result.state).toBe('reopened');
+        expect(result.user).toBe('some-user');
+      });
+
+      it('handle issues without latest_comment_url', async () => {
+        nock('https://api.github.com')
+          .get('/repos/manosim/notifications-test/issues/1')
+          .reply(200, { state: 'open', draft: false, merged: false });
+
+        const result = await getGitifySubjectDetails(
+          {
+            ...mockedSingleNotification,
+            subject: {
+              ...mockedSingleNotification.subject,
+              latest_comment_url: null,
             },
           },
-        });
-
-      const result = await getGitifySubjectDetails(
-        mockNotification,
-        mockAccounts.token,
-      );
+          mockAccounts.token,
+        );
 
-      expect(result.state).toBe('RESOLVED');
-      expect(result.user).toBe(null);
+        expect(result.state).toBe('open');
+        expect(result.user).toBeNull();
+      });
     });
 
-    it('filtered response by subscribed', async () => {
+    describe('Pull Requests', () => {
       const mockNotification = {
         ...mockedSingleNotification,
         subject: {
           ...mockedSingleNotification.subject,
-          title: 'This is a discussion',
-          type: 'Discussion' as SubjectType,
+          type: 'PullRequest' as SubjectType,
         },
       };
 
-      nock('https://api.github.com')
-        .post('/graphql')
-        .reply(200, {
-          data: {
-            search: {
-              nodes: [
-                {
-                  title: 'This is a discussion',
-                  viewerSubscription: 'SUBSCRIBED',
-                  stateReason: null,
-                  isAnswered: false,
-                  comments: {
-                    nodes: [], //TODO - Update this to have real data
-                  },
-                },
-                {
-                  title: 'This is a discussion',
-                  viewerSubscription: 'IGNORED',
-                  stateReason: null,
-                  isAnswered: true,
-                  comments: {
-                    nodes: [], //TODO - Update this to have real data
-                  },
-                },
-              ],
+      it('closed pull request state', async () => {
+        nock('https://api.github.com')
+          .get('/repos/manosim/notifications-test/issues/1')
+          .reply(200, { state: 'closed', draft: false, merged: false });
+
+        nock('https://api.github.com')
+          .get('/repos/manosim/notifications-test/issues/comments/302888448')
+          .reply(200, { user: { login: 'some-user' } });
+
+        const result = await getGitifySubjectDetails(
+          mockNotification,
+          mockAccounts.token,
+        );
+
+        expect(result.state).toBe('closed');
+        expect(result.user).toBe('some-user');
+      });
+
+      it('draft pull request state', async () => {
+        nock('https://api.github.com')
+          .get('/repos/manosim/notifications-test/issues/1')
+          .reply(200, { state: 'open', draft: true, merged: false });
+
+        nock('https://api.github.com')
+          .get('/repos/manosim/notifications-test/issues/comments/302888448')
+          .reply(200, { user: { login: 'some-user' } });
+
+        const result = await getGitifySubjectDetails(
+          mockNotification,
+          mockAccounts.token,
+        );
+
+        expect(result.state).toBe('draft');
+        expect(result.user).toBe('some-user');
+      });
+
+      it('merged pull request state', async () => {
+        nock('https://api.github.com')
+          .get('/repos/manosim/notifications-test/issues/1')
+          .reply(200, { state: 'open', draft: false, merged: true });
+
+        nock('https://api.github.com')
+          .get('/repos/manosim/notifications-test/issues/comments/302888448')
+          .reply(200, { user: { login: 'some-user' } });
+
+        const result = await getGitifySubjectDetails(
+          mockNotification,
+          mockAccounts.token,
+        );
+
+        expect(result.state).toBe('merged');
+        expect(result.user).toBe('some-user');
+      });
+
+      it('open pull request state', async () => {
+        nock('https://api.github.com')
+          .get('/repos/manosim/notifications-test/issues/1')
+          .reply(200, { state: 'open', draft: false, merged: false });
+
+        nock('https://api.github.com')
+          .get('/repos/manosim/notifications-test/issues/comments/302888448')
+          .reply(200, { user: { login: 'some-user' } });
+
+        const result = await getGitifySubjectDetails(
+          mockNotification,
+          mockAccounts.token,
+        );
+
+        expect(result.state).toBe('open');
+        expect(result.user).toBe('some-user');
+      });
+
+      it('handle pull request without latest_comment_url', async () => {
+        nock('https://api.github.com')
+          .get('/repos/manosim/notifications-test/issues/1')
+          .reply(200, { state: 'open', draft: false, merged: false });
+
+        const result = await getGitifySubjectDetails(
+          {
+            ...mockNotification,
+            subject: {
+              ...mockNotification.subject,
+              latest_comment_url: null,
             },
           },
-        });
+          mockAccounts.token,
+        );
 
-      const result = await getGitifySubjectDetails(
-        mockNotification,
-        mockAccounts.token,
-      );
-
-      expect(result.state).toBe('OPEN');
-      expect(result.user).toBe(null);
-    });
-  });
-
-  describe('getGitifySubjectForIssue', () => {
-    it('open issue state', async () => {
-      nock('https://api.github.com')
-        .get('/repos/manosim/notifications-test/issues/1')
-        .reply(200, { state: 'open' });
-
-      nock('https://api.github.com')
-        .get('/repos/manosim/notifications-test/issues/comments/302888448')
-        .reply(200, { user: { login: 'some-user' } });
-
-      const result = await getGitifySubjectDetails(
-        mockedSingleNotification,
-        mockAccounts.token,
-      );
-
-      expect(result.state).toBe('open');
-      expect(result.user).toBe('some-user');
-    });
-
-    it('closed issue state', async () => {
-      nock('https://api.github.com')
-        .get('/repos/manosim/notifications-test/issues/1')
-        .reply(200, { state: 'closed' });
-
-      nock('https://api.github.com')
-        .get('/repos/manosim/notifications-test/issues/comments/302888448')
-        .reply(200, { user: { login: 'some-user' } });
-
-      const result = await getGitifySubjectDetails(
-        mockedSingleNotification,
-        mockAccounts.token,
-      );
-
-      expect(result.state).toBe('closed');
-      expect(result.user).toBe('some-user');
-    });
-
-    it('completed issue state', async () => {
-      nock('https://api.github.com')
-        .get('/repos/manosim/notifications-test/issues/1')
-        .reply(200, { state: 'closed', state_reason: 'completed' });
-
-      nock('https://api.github.com')
-        .get('/repos/manosim/notifications-test/issues/comments/302888448')
-        .reply(200, { user: { login: 'some-user' } });
-
-      const result = await getGitifySubjectDetails(
-        mockedSingleNotification,
-        mockAccounts.token,
-      );
-
-      expect(result.state).toBe('completed');
-      expect(result.user).toBe('some-user');
-    });
-
-    it('not_planned issue state', async () => {
-      nock('https://api.github.com')
-        .get('/repos/manosim/notifications-test/issues/1')
-        .reply(200, { state: 'open', state_reason: 'not_planned' });
-
-      nock('https://api.github.com')
-        .get('/repos/manosim/notifications-test/issues/comments/302888448')
-        .reply(200, { user: { login: 'some-user' } });
-
-      const result = await getGitifySubjectDetails(
-        mockedSingleNotification,
-        mockAccounts.token,
-      );
-
-      expect(result.state).toBe('not_planned');
-      expect(result.user).toBe('some-user');
-    });
-
-    it('reopened issue state', async () => {
-      nock('https://api.github.com')
-        .get('/repos/manosim/notifications-test/issues/1')
-        .reply(200, { state: 'open', state_reason: 'reopened' });
-
-      nock('https://api.github.com')
-        .get('/repos/manosim/notifications-test/issues/comments/302888448')
-        .reply(200, { user: { login: 'some-user' } });
-
-      const result = await getGitifySubjectDetails(
-        mockedSingleNotification,
-        mockAccounts.token,
-      );
-
-      expect(result.state).toBe('reopened');
-      expect(result.user).toBe('some-user');
-    });
-  });
-
-  describe('getGitifySubjectForPullRequest', () => {
-    const mockNotification = {
-      ...mockedSingleNotification,
-      subject: {
-        ...mockedSingleNotification.subject,
-        type: 'PullRequest' as SubjectType,
-      },
-    };
-
-    it('closed pull request state', async () => {
-      nock('https://api.github.com')
-        .get('/repos/manosim/notifications-test/issues/1')
-        .reply(200, { state: 'closed', draft: false, merged: false });
-
-      nock('https://api.github.com')
-        .get('/repos/manosim/notifications-test/issues/comments/302888448')
-        .reply(200, { user: { login: 'some-user' } });
-
-      const result = await getGitifySubjectDetails(
-        mockNotification,
-        mockAccounts.token,
-      );
-
-      expect(result.state).toBe('closed');
-      expect(result.user).toBe('some-user');
-    });
-
-    it('draft pull request state', async () => {
-      nock('https://api.github.com')
-        .get('/repos/manosim/notifications-test/issues/1')
-        .reply(200, { state: 'open', draft: true, merged: false });
-
-      nock('https://api.github.com')
-        .get('/repos/manosim/notifications-test/issues/comments/302888448')
-        .reply(200, { user: { login: 'some-user' } });
-
-      const result = await getGitifySubjectDetails(
-        mockNotification,
-        mockAccounts.token,
-      );
-
-      expect(result.state).toBe('draft');
-      expect(result.user).toBe('some-user');
-    });
-
-    it('merged pull request state', async () => {
-      nock('https://api.github.com')
-        .get('/repos/manosim/notifications-test/issues/1')
-        .reply(200, { state: 'open', draft: false, merged: true });
-
-      nock('https://api.github.com')
-        .get('/repos/manosim/notifications-test/issues/comments/302888448')
-        .reply(200, { user: { login: 'some-user' } });
-
-      const result = await getGitifySubjectDetails(
-        mockNotification,
-        mockAccounts.token,
-      );
-
-      expect(result.state).toBe('merged');
-      expect(result.user).toBe('some-user');
-    });
-
-    it('open pull request state', async () => {
-      nock('https://api.github.com')
-        .get('/repos/manosim/notifications-test/issues/1')
-        .reply(200, { state: 'open', draft: false, merged: false });
-
-      nock('https://api.github.com')
-        .get('/repos/manosim/notifications-test/issues/comments/302888448')
-        .reply(200, { user: { login: 'some-user' } });
-
-      const result = await getGitifySubjectDetails(
-        mockNotification,
-        mockAccounts.token,
-      );
-
-      expect(result.state).toBe('open');
-      expect(result.user).toBe('some-user');
+        expect(result.state).toBe('open');
+        expect(result.user).toBeNull();
+      });
     });
   });