Skip to content
Draft
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
13 changes: 12 additions & 1 deletion src/renderer/__mocks__/user-mocks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { GitifyNotificationUser, GitifyUser, Link } from '../types';
import type { AuthorFieldsFragment } from '../utils/api/graphql/generated/graphql';
import type { RawUser } from '../utils/api/types';

export const mockGitifyUser: GitifyUser = {
Expand All @@ -19,7 +20,7 @@ export function createPartialMockUser(login: string): RawUser {
return mockUser as RawUser;
}

export function createMockNotificationUser(
export function createMockGitifyNotificationUser(
login: string,
): GitifyNotificationUser {
return {
Expand All @@ -29,3 +30,13 @@ export function createMockNotificationUser(
type: 'User',
};
}

/**
* Creates a mock author for use in GraphQL response mocks.
*/
export function createMockGraphQLAuthor(login: string): AuthorFieldsFragment {
return {
...createMockGitifyNotificationUser(login),
__typename: 'User',
} as AuthorFieldsFragment;
}
204 changes: 1 addition & 203 deletions src/renderer/hooks/useNotifications.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,8 @@ import { act, renderHook, waitFor } from '@testing-library/react';
import axios, { AxiosError } from 'axios';
import nock from 'nock';

import { mockGitHubCloudAccount } from '../__mocks__/account-mocks';
import { mockAuth, mockSettings, mockState } from '../__mocks__/state-mocks';
import {
mockNotificationUser,
mockSingleNotification,
} from '../utils/api/__mocks__/response-mocks';
import { mockSingleNotification } from '../utils/api/__mocks__/response-mocks';
import { Errors } from '../utils/errors';
import * as logger from '../utils/logger';
import { useNotifications } from './useNotifications';
Expand Down Expand Up @@ -118,204 +114,6 @@ describe('renderer/hooks/useNotifications.ts', () => {
expect(result.current.notifications[1].notifications.length).toBe(2);
});

it('should fetch detailed notifications with success', async () => {
const mockRepository = {
name: 'notifications-test',
full_name: 'gitify-app/notifications-test',
html_url: 'https://github.com/gitify-app/notifications-test',
owner: {
login: 'gitify-app',
avatar_url: 'https://avatar.url',
type: 'Organization',
},
};

const mockNotifications = [
{
id: '1',
unread: true,
updated_at: '2024-01-01T00:00:00Z',
reason: 'subscribed',
subject: {
title: 'This is a check suite workflow.',
type: 'CheckSuite',
url: null,
latest_comment_url: null,
},
repository: mockRepository,
},
{
id: '2',
unread: true,
updated_at: '2024-02-26T00:00:00Z',
reason: 'subscribed',
subject: {
title: 'This is a Discussion.',
type: 'Discussion',
url: null,
latest_comment_url: null,
},
repository: mockRepository,
},
{
id: '3',
unread: true,
updated_at: '2024-01-01T00:00:00Z',
reason: 'subscribed',
subject: {
title: 'This is an Issue.',
type: 'Issue',
url: 'https://api.github.com/repos/gitify-app/notifications-test/issues/3',
latest_comment_url:
'https://api.github.com/repos/gitify-app/notifications-test/issues/3/comments',
},
repository: mockRepository,
},
{
id: '4',
unread: true,
updated_at: '2024-01-01T00:00:00Z',
reason: 'subscribed',
subject: {
title: 'This is a Pull Request.',
type: 'PullRequest',
url: 'https://api.github.com/repos/gitify-app/notifications-test/pulls/4',
latest_comment_url:
'https://api.github.com/repos/gitify-app/notifications-test/issues/4/comments',
},
repository: mockRepository,
},
{
id: '5',
unread: true,
updated_at: '2024-01-01T00:00:00Z',
reason: 'subscribed',
subject: {
title: 'This is an invitation.',
type: 'RepositoryInvitation',
url: null,
latest_comment_url: null,
},
repository: mockRepository,
},
{
id: '6',
unread: true,
updated_at: '2024-01-01T00:00:00Z',
reason: 'subscribed',
subject: {
title: 'This is a workflow run.',
type: 'WorkflowRun',
url: null,
latest_comment_url: null,
},
repository: mockRepository,
},
];

nock('https://api.github.com')
.get('/notifications?participating=false')
.reply(200, mockNotifications);

nock('https://api.github.com')
.post('/graphql')
.reply(200, {
data: {
search: {
nodes: [
{
title: 'This is a Discussion.',
stateReason: null,
isAnswered: true,
url: 'https://github.com/gitify-app/notifications-test/discussions/612',
author: {
login: 'discussion-creator',
url: 'https://github.com/discussion-creator',
avatar_url:
'https://avatars.githubusercontent.com/u/133795385?s=200&v=4',
type: 'User',
},
comments: {
nodes: [
{
databaseId: 2297637,
createdAt: '2022-03-04T20:39:44Z',
author: {
login: 'comment-user',
url: 'https://github.com/comment-user',
avatar_url:
'https://avatars.githubusercontent.com/u/1?v=4',
type: 'User',
},
replies: {
nodes: [],
},
},
],
},
labels: null,
},
],
},
},
});

nock('https://api.github.com')
.get('/repos/gitify-app/notifications-test/issues/3')
.reply(200, {
state: 'closed',
merged: true,
user: mockNotificationUser,
labels: [],
});
nock('https://api.github.com')
.get('/repos/gitify-app/notifications-test/issues/3/comments')
.reply(200, {
user: mockNotificationUser,
});
nock('https://api.github.com')
.get('/repos/gitify-app/notifications-test/pulls/4')
.reply(200, {
state: 'closed',
merged: false,
user: mockNotificationUser,
labels: [],
});
nock('https://api.github.com')
.get('/repos/gitify-app/notifications-test/pulls/4/reviews')
.reply(200, {});
nock('https://api.github.com')
.get('/repos/gitify-app/notifications-test/issues/4/comments')
.reply(200, {
user: mockNotificationUser,
});

const { result } = renderHook(() => useNotifications());

act(() => {
result.current.fetchNotifications({
auth: {
accounts: [mockGitHubCloudAccount],
},
settings: {
...mockSettings,
detailedNotifications: true,
},
});
});

expect(result.current.status).toBe('loading');

await waitFor(() => {
expect(result.current.status).toBe('success');
});

expect(result.current.notifications[0].account.hostname).toBe(
'github.com',
);
expect(result.current.notifications[0].notifications.length).toBe(6);
});

it('should fetch notifications with same failures', async () => {
const code = AxiosError.ERR_BAD_REQUEST;
const status = 401;
Expand Down
55 changes: 37 additions & 18 deletions src/renderer/utils/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
apiRequestAuth,
type ExecutionResultWithHeaders,
performGraphQLRequest,
performGraphQLRequestString,
} from './request';
import type {
NotificationThreadSubscription,
Expand Down Expand Up @@ -194,6 +195,33 @@ export async function fetchAuthenticatedUserDetails(
);
}

/**
* Fetch GitHub Discussion by Discussion Number.
*/
export async function fetchDiscussionByNumber(
notification: GitifyNotification,
): Promise<ExecutionResult<FetchDiscussionByNumberQuery>> {
const url = getGitHubGraphQLUrl(notification.account.hostname);
const number = getNumberFromUrl(notification.subject.url);

return performGraphQLRequest(
url.toString() as Link,
notification.account.token,
FetchDiscussionByNumberDocument,
{
owner: notification.repository.owner.login,
name: notification.repository.name,
number: number,
firstLabels: 100,
lastThreadedComments: 10,
lastReplies: 10,
includeIsAnswered: isAnsweredDiscussionFeatureSupported(
notification.account,
),
},
);
}

/**
* Fetch GitHub Issue by Issue Number.
*/
Expand Down Expand Up @@ -234,37 +262,28 @@ export async function fetchPullByNumber(
owner: notification.repository.owner.login,
name: notification.repository.name,
number: number,
firstLabels: 100,
firstClosingIssues: 100,
firstLabels: 100,
lastComments: 1,
lastReviews: 100,
},
);
}

/**
* Fetch GitHub Discussion by Discussion Number.
* Fetch Batched Details for Discussions, Issues and Pull Requests.
*/
export async function fetchDiscussionByNumber(
export async function fetchMergedQueryDetails(
notification: GitifyNotification,
): Promise<ExecutionResult<FetchDiscussionByNumberQuery>> {
mergedQuery: string,
mergedVariables: Record<string, string | number | boolean>,
): Promise<ExecutionResult<Record<string, unknown>>> {
const url = getGitHubGraphQLUrl(notification.account.hostname);
const number = getNumberFromUrl(notification.subject.url);

return performGraphQLRequest(
return performGraphQLRequestString(
url.toString() as Link,
notification.account.token,
FetchDiscussionByNumberDocument,
{
owner: notification.repository.owner.login,
name: notification.repository.name,
number: number,
lastComments: 10,
lastReplies: 10,
firstLabels: 100,
includeIsAnswered: isAnsweredDiscussionFeatureSupported(
notification.account,
),
},
mergedQuery,
mergedVariables,
);
}
46 changes: 46 additions & 0 deletions src/renderer/utils/api/graphql/MergeQueryBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { FragmentInfo } from './utils';

type VarValue = string | number | boolean;

export class MergeQueryBuilder {
private selections: string[] = [];
private variableDefinitions: string[] = [];
private variableValues: Record<string, VarValue> = {};
private fragments: FragmentInfo[] = [];

addSelection(selection: string): this {
if (selection) {
this.selections.push(selection);
}
return this;
}

addVariableDefs(defs: string): this {
if (defs) {
this.variableDefinitions.push(defs);
}
return this;
}

setVar(name: string, value: VarValue): this {
this.variableValues[name] = value;
return this;
}

addFragments(fragments: FragmentInfo[] | undefined): this {
if (fragments?.length) {
this.fragments.push(...fragments);
}
return this;
}

buildQuery(docName = 'FetchMergedNotifications'): string {
const vars = this.variableDefinitions.join(', ');
const frags = this.fragments.map((f) => f.printed).join('\n');
return `query ${docName}(${vars}) {\n${this.selections.join('\n')}\n}\n\n${frags}\n`;
}

getVariables(): Record<string, VarValue> {
return this.variableValues;
}
}
2 changes: 1 addition & 1 deletion src/renderer/utils/api/graphql/common.graphql
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
fragment AuthorFields on Actor {
login
htmlUrl: url
avatarUrl
avatarUrl: avatarUrl
type: __typename
}

Expand Down
Loading
Loading