Skip to content

Commit c539b0b

Browse files
committed
refactor(api): use graphql endpoint for fetching authenticated user details
Signed-off-by: Adam Setch <[email protected]>
1 parent 05f95f7 commit c539b0b

File tree

11 files changed

+116
-100
lines changed

11 files changed

+116
-100
lines changed

src/renderer/__mocks__/user-mocks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { User } from '../typesGitHub';
44
export const mockGitifyUser: GitifyUser = {
55
login: 'octocat',
66
name: 'Mona Lisa Octocat',
7-
id: 123456789,
7+
id: '123456789',
88
avatar: 'https://avatars.githubusercontent.com/u/583231?v=4' as Link,
99
};
1010

src/renderer/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ export interface GitifyUser {
204204
login: string;
205205
name: string | null;
206206
avatar: Link | null;
207-
id: number;
207+
id: string;
208208
}
209209

210210
export interface GitifyError {

src/renderer/typesGitHub.ts

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -65,39 +65,6 @@ interface GitHubSubject {
6565
type: SubjectType;
6666
}
6767

68-
export type UserDetails = User & UserProfile;
69-
70-
export interface UserProfile {
71-
name: string;
72-
company: string;
73-
blog: string;
74-
location: string;
75-
email: string;
76-
hireable: string;
77-
bio: string;
78-
twitter_username: string;
79-
public_repos: number;
80-
public_gists: number;
81-
followers: number;
82-
following: number;
83-
created_at: string;
84-
updated_at: string;
85-
private_gists: number;
86-
total_private_repos: number;
87-
owned_private_repos: number;
88-
disk_usage: number;
89-
collaborators: number;
90-
two_factor_authentication: boolean;
91-
plan: Plan;
92-
}
93-
94-
export interface Plan {
95-
name: string;
96-
space: number;
97-
private_repos: number;
98-
collaborators: number;
99-
}
100-
10168
export interface User {
10269
login: string;
10370
id: number;

src/renderer/utils/api/client.test.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
mockNonCachedAuthHeaders,
1010
} from './__mocks__/request-mocks';
1111
import {
12-
getAuthenticatedUser,
1312
getHtmlUrl,
1413
headNotifications,
1514
ignoreNotificationThreadSubscription,
@@ -29,17 +28,6 @@ describe('renderer/utils/api/client.ts', () => {
2928
jest.clearAllMocks();
3029
});
3130

32-
it('getAuthenticatedUser - should fetch authenticated user', async () => {
33-
await getAuthenticatedUser(mockGitHubHostname, mockToken);
34-
35-
expect(axios).toHaveBeenCalledWith({
36-
url: 'https://api.github.com/user',
37-
headers: mockAuthHeaders,
38-
method: 'GET',
39-
data: {},
40-
});
41-
});
42-
4331
it('headNotifications - should fetch notifications head', async () => {
4432
await headNotifications(mockGitHubHostname, mockToken);
4533

src/renderer/utils/api/client.ts

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,40 +14,30 @@ import type {
1414
Notification,
1515
NotificationThreadSubscription,
1616
Release,
17-
UserDetails,
1817
} from '../../typesGitHub';
1918
import { isAnsweredDiscussionFeatureSupported } from '../features';
2019
import { rendererLogError } from '../logger';
2120
import {
21+
FetchAuthenticatedUserDetailsDocument,
22+
type FetchAuthenticatedUserDetailsQuery,
2223
FetchDiscussionByNumberDocument,
2324
type FetchDiscussionByNumberQuery,
2425
FetchIssueByNumberDocument,
2526
type FetchIssueByNumberQuery,
2627
FetchPullRequestByNumberDocument,
2728
type FetchPullRequestByNumberQuery,
2829
} from './graphql/generated/graphql';
29-
import { apiRequestAuth, performGraphQLRequest } from './request';
30+
import {
31+
apiRequestAuth,
32+
type ExecutionResultWithHeaders,
33+
performGraphQLRequest,
34+
} from './request';
3035
import {
3136
getGitHubAPIBaseUrl,
3237
getGitHubGraphQLUrl,
3338
getNumberFromUrl,
3439
} from './utils';
3540

36-
/**
37-
* Get the authenticated user
38-
*
39-
* Endpoint documentation: https://docs.github.com/en/rest/users/users#get-the-authenticated-user
40-
*/
41-
export function getAuthenticatedUser(
42-
hostname: Hostname,
43-
token: Token,
44-
): AxiosPromise<UserDetails> {
45-
const url = getGitHubAPIBaseUrl(hostname);
46-
url.pathname += 'user';
47-
48-
return apiRequestAuth(url.toString() as Link, 'GET', token);
49-
}
50-
5141
/**
5242
* Perform a HEAD operation, used to validate that connectivity is established.
5343
*
@@ -187,6 +177,22 @@ export async function getHtmlUrl(url: Link, token: Token): Promise<string> {
187177
}
188178
}
189179

180+
/**
181+
* Fetch GitHub Authenticated User.
182+
*/
183+
export async function fetchAuthenticatedUserDetails(
184+
hostname: Hostname,
185+
token: Token,
186+
): Promise<ExecutionResultWithHeaders<FetchAuthenticatedUserDetailsQuery>> {
187+
const url = getGitHubGraphQLUrl(hostname);
188+
189+
return performGraphQLRequest(
190+
url.toString() as Link,
191+
token,
192+
FetchAuthenticatedUserDetailsDocument,
193+
);
194+
}
195+
190196
/**
191197
* Fetch GitHub Issue by Issue Number.
192198
*/

src/renderer/utils/api/graphql/generated/gql.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@ type Documents = {
1919
"query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...DiscussionCommentFields\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n url\n}\n\nfragment DiscussionCommentFields on DiscussionComment {\n ...CommentFields\n replies(last: $lastReplies) {\n totalCount\n nodes {\n ...CommentFields\n }\n }\n}": typeof types.FetchDiscussionByNumberDocument,
2020
"query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}": typeof types.FetchIssueByNumberDocument,
2121
"query FetchPullRequestByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int, $lastComments: Int, $lastReviews: Int, $firstClosingIssues: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: $lastReviews) {\n totalCount\n nodes {\n ...PullRequestReviewFields\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: $firstClosingIssues) {\n nodes {\n number\n }\n }\n }\n }\n}\n\nfragment PullRequestReviewFields on PullRequestReview {\n state\n author {\n login\n }\n}": typeof types.FetchPullRequestByNumberDocument,
22+
"query FetchAuthenticatedUserDetails {\n viewer {\n id\n name\n login\n avatarUrl\n }\n}": typeof types.FetchAuthenticatedUserDetailsDocument,
2223
};
2324
const documents: Documents = {
2425
"fragment AuthorFields on Actor {\n login\n html_url: url\n avatar_url: avatarUrl\n type: __typename\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": types.AuthorFieldsFragmentDoc,
2526
"query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...DiscussionCommentFields\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n url\n}\n\nfragment DiscussionCommentFields on DiscussionComment {\n ...CommentFields\n replies(last: $lastReplies) {\n totalCount\n nodes {\n ...CommentFields\n }\n }\n}": types.FetchDiscussionByNumberDocument,
2627
"query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n }\n }\n}": types.FetchIssueByNumberDocument,
2728
"query FetchPullRequestByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int, $lastComments: Int, $lastReviews: Int, $firstClosingIssues: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: $lastReviews) {\n totalCount\n nodes {\n ...PullRequestReviewFields\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: $firstClosingIssues) {\n nodes {\n number\n }\n }\n }\n }\n}\n\nfragment PullRequestReviewFields on PullRequestReview {\n state\n author {\n login\n }\n}": types.FetchPullRequestByNumberDocument,
29+
"query FetchAuthenticatedUserDetails {\n viewer {\n id\n name\n login\n avatarUrl\n }\n}": types.FetchAuthenticatedUserDetailsDocument,
2830
};
2931

3032
/**
@@ -43,6 +45,10 @@ export function graphql(source: "query FetchIssueByNumber($owner: String!, $name
4345
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
4446
*/
4547
export function graphql(source: "query FetchPullRequestByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int, $lastComments: Int, $lastReviews: Int, $firstClosingIssues: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: $lastReviews) {\n totalCount\n nodes {\n ...PullRequestReviewFields\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: $firstClosingIssues) {\n nodes {\n number\n }\n }\n }\n }\n}\n\nfragment PullRequestReviewFields on PullRequestReview {\n state\n author {\n login\n }\n}"): typeof import('./graphql').FetchPullRequestByNumberDocument;
48+
/**
49+
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
50+
*/
51+
export function graphql(source: "query FetchAuthenticatedUserDetails {\n viewer {\n id\n name\n login\n avatarUrl\n }\n}"): typeof import('./graphql').FetchAuthenticatedUserDetailsDocument;
4652

4753

4854
export function graphql(source: string) {

src/renderer/utils/api/graphql/generated/graphql.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36029,6 +36029,11 @@ export type PullRequestReviewFieldsFragment = { __typename?: 'PullRequestReview'
3602936029
| { __typename?: 'User', login: string }
3603036030
| null };
3603136031

36032+
export type FetchAuthenticatedUserDetailsQueryVariables = Exact<{ [key: string]: never; }>;
36033+
36034+
36035+
export type FetchAuthenticatedUserDetailsQuery = { __typename?: 'Query', viewer: { __typename?: 'User', id: string, name?: string | null, login: string, avatarUrl: any } };
36036+
3603236037
export class TypedDocumentString<TResult, TVariables>
3603336038
extends String
3603436039
implements DocumentTypeDecoration<TResult, TVariables>
@@ -36262,4 +36267,14 @@ fragment PullRequestReviewFields on PullRequestReview {
3626236267
author {
3626336268
login
3626436269
}
36265-
}`) as unknown as TypedDocumentString<FetchPullRequestByNumberQuery, FetchPullRequestByNumberQueryVariables>;
36270+
}`) as unknown as TypedDocumentString<FetchPullRequestByNumberQuery, FetchPullRequestByNumberQueryVariables>;
36271+
export const FetchAuthenticatedUserDetailsDocument = new TypedDocumentString(`
36272+
query FetchAuthenticatedUserDetails {
36273+
viewer {
36274+
id
36275+
name
36276+
login
36277+
avatarUrl
36278+
}
36279+
}
36280+
`) as unknown as TypedDocumentString<FetchAuthenticatedUserDetailsQuery, FetchAuthenticatedUserDetailsQueryVariables>;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
query FetchAuthenticatedUserDetails {
2+
viewer {
3+
id
4+
name
5+
login
6+
avatarUrl
7+
}
8+
}

src/renderer/utils/api/request.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ import { rendererLogError } from '../logger';
1111
import type { TypedDocumentString } from './graphql/generated/graphql';
1212
import { getNextURLFromLinkHeader } from './utils';
1313

14+
/**
15+
* ExecutionResult with HTTP response headers
16+
*/
17+
export type ExecutionResultWithHeaders<T> = ExecutionResult<T> & {
18+
headers: Record<string, string>;
19+
};
20+
1421
/**
1522
* Perform an unauthenticated API request
1623
*
@@ -107,8 +114,11 @@ export async function performGraphQLRequest<TResult, TVariables>(
107114
},
108115
headers: headers,
109116
}).then((response) => {
110-
return response.data;
111-
}) as Promise<ExecutionResult<TResult>>;
117+
return {
118+
...response.data,
119+
headers: response.headers,
120+
};
121+
}) as Promise<ExecutionResultWithHeaders<TResult>>;
112122
}
113123

114124
/**

src/renderer/utils/auth/utils.test.ts

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { AxiosResponse } from 'axios';
22
import axios from 'axios';
3-
import nock from 'nock';
43

54
import { mockGitHubCloudAccount } from '../../__mocks__/account-mocks';
65
import { mockAuth } from '../../__mocks__/state-mocks';
@@ -16,12 +15,16 @@ import type {
1615
Token,
1716
} from '../../types';
1817
import * as comms from '../../utils/comms';
18+
import * as apiClient from '../api/client';
19+
import type { FetchAuthenticatedUserDetailsQuery } from '../api/graphql/generated/graphql';
1920
import * as apiRequests from '../api/request';
2021
import * as logger from '../logger';
2122
import type { AuthMethod } from './types';
2223
import * as authUtils from './utils';
2324
import { getNewOAuthAppURL, getNewTokenURL } from './utils';
2425

26+
type UserDetailsResponse = FetchAuthenticatedUserDetailsQuery['viewer'];
27+
2528
describe('renderer/utils/auth/utils.ts', () => {
2629
describe('authGitHub', () => {
2730
jest.spyOn(logger, 'rendererLogInfo').mockImplementation();
@@ -139,6 +142,10 @@ describe('renderer/utils/auth/utils.ts', () => {
139142

140143
describe('addAccount', () => {
141144
let mockAuthState: AuthState;
145+
const fetchAuthenticatedUserDetailsSpy = jest.spyOn(
146+
apiClient,
147+
'fetchAuthenticatedUserDetails',
148+
);
142149

143150
beforeEach(() => {
144151
mockAuthState = {
@@ -152,13 +159,17 @@ describe('renderer/utils/auth/utils.ts', () => {
152159

153160
describe('should add GitHub Cloud account', () => {
154161
beforeEach(() => {
155-
nock('https://api.github.com')
156-
.get('/user')
157-
.reply(
158-
200,
159-
{ ...mockGitifyUser, avatar_url: mockGitifyUser.avatar },
160-
{ 'x-oauth-scopes': Constants.OAUTH_SCOPES.RECOMMENDED },
161-
);
162+
fetchAuthenticatedUserDetailsSpy.mockResolvedValue({
163+
data: {
164+
viewer: {
165+
...mockGitifyUser,
166+
avatarUrl: mockGitifyUser.avatar,
167+
} as UserDetailsResponse,
168+
},
169+
headers: {
170+
'x-oauth-scopes': Constants.OAUTH_SCOPES.RECOMMENDED.join(', '),
171+
},
172+
});
162173
});
163174

164175
it('should add personal access token account', async () => {
@@ -206,16 +217,18 @@ describe('renderer/utils/auth/utils.ts', () => {
206217

207218
describe('should add GitHub Enterprise Server account', () => {
208219
beforeEach(() => {
209-
nock('https://github.gitify.io/api/v3')
210-
.get('/user')
211-
.reply(
212-
200,
213-
{ ...mockGitifyUser, avatar_url: mockGitifyUser.avatar },
214-
{
215-
'x-github-enterprise-version': '3.0.0',
216-
'x-oauth-scopes': Constants.OAUTH_SCOPES.RECOMMENDED,
217-
},
218-
);
220+
fetchAuthenticatedUserDetailsSpy.mockResolvedValue({
221+
data: {
222+
viewer: {
223+
...mockGitifyUser,
224+
avatarUrl: mockGitifyUser.avatar,
225+
} as UserDetailsResponse,
226+
},
227+
headers: {
228+
'x-github-enterprise-version': '3.0.0',
229+
'x-oauth-scopes': Constants.OAUTH_SCOPES.RECOMMENDED.join(', '),
230+
},
231+
});
219232
});
220233

221234
it('should add personal access token account', async () => {

0 commit comments

Comments
 (0)