• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gitify-app / gitify / 9319904199

31 May 2024 02:38PM UTC coverage: 96.355% (-0.4%) from 96.787%
9319904199

Pull #1139

github

web-flow
Merge af6de505a into 72b432272
Pull Request #1139: feat(accounts): enhance auth account data structure

402 of 415 branches covered (96.87%)

Branch coverage included in aggregate %.

79 of 89 new or added lines in 12 files covered. (88.76%)

3 existing lines in 2 files now uncovered.

1052 of 1094 relevant lines covered (96.16%)

20.24 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.12
/src/utils/helpers.ts
1
import { formatDistanceToNow, parseISO } from 'date-fns';
36✔
2
import type { Account, AuthState } from '../types';
3
import type { Notification } from '../typesGitHub';
4
import { openExternalLink } from '../utils/comms';
36✔
5
import { getHtmlUrl, getLatestDiscussion } from './api/client';
36✔
6
import type { PlatformType } from './auth/types';
7
import { Constants } from './constants';
36✔
8
import {
36✔
9
  getCheckSuiteAttributes,
10
  getLatestDiscussionComment,
11
  getWorkflowRunAttributes,
12
} from './subject';
13

14
export function isPersonalAccessTokenLoggedIn(auth: AuthState): boolean {
36✔
15
  return auth.accounts.some(
94✔
16
    (account) => account.method === 'Personal Access Token',
92✔
17
  );
18
}
19

20
export function isOAuthAppLoggedIn(auth: AuthState): boolean {
36✔
21
  return auth.accounts.some((account) => account.method === 'OAuth App');
164✔
22
}
23

24
export function getAccountForHost(hostname: string, auth: AuthState): Account {
36✔
25
  return auth.accounts.find((account) => hostname.endsWith(account.hostname));
54✔
26
}
27

28
export function getPlatformFromHostname(hostname: string): PlatformType {
36✔
29
  return hostname.endsWith(Constants.DEFAULT_AUTH_OPTIONS.hostname)
16✔
30
    ? 'GitHub Cloud'
31
    : 'GitHub Enterprise Server';
32
}
33

34
export function isEnterpriseHost(hostname: string): boolean {
36✔
35
  return !hostname.endsWith(Constants.DEFAULT_AUTH_OPTIONS.hostname);
118✔
36
}
37

38
export function generateNotificationReferrerId(
36✔
39
  notification: Notification,
40
): string {
41
  const buffer = Buffer.from(
36✔
42
    `018:NotificationThread${notification.id}:${notification.account.user.id}`,
43
  );
44
  return buffer.toString('base64');
36✔
45
}
46

47
export function getCheckSuiteUrl(notification: Notification): string {
36✔
48
  const filters = [];
14✔
49

50
  const checkSuiteAttributes = getCheckSuiteAttributes(notification);
14✔
51

52
  if (checkSuiteAttributes?.workflowName) {
14✔
53
    filters.push(
10✔
54
      `workflow:"${checkSuiteAttributes.workflowName.replaceAll(' ', '+')}"`,
55
    );
56
  }
57

58
  if (checkSuiteAttributes?.status) {
14✔
59
    filters.push(`is:${checkSuiteAttributes.status}`);
8✔
60
  }
61

62
  if (checkSuiteAttributes?.branchName) {
14✔
63
    filters.push(`branch:${checkSuiteAttributes.branchName}`);
10✔
64
  }
65

66
  return actionsURL(notification.repository.html_url, filters);
14✔
67
}
68

69
export function getWorkflowRunUrl(notification: Notification): string {
36✔
70
  const filters = [];
6✔
71

72
  const workflowRunAttributes = getWorkflowRunAttributes(notification);
6✔
73

74
  if (workflowRunAttributes?.status) {
6✔
75
    filters.push(`is:${workflowRunAttributes.status}`);
2✔
76
  }
77

78
  return actionsURL(notification.repository.html_url, filters);
6✔
79
}
80

81
/**
82
 * Construct a GitHub Actions URL for a repository with optional filters.
83
 */
84
export function actionsURL(repositoryURL: string, filters: string[]): string {
36✔
85
  const url = new URL(repositoryURL);
20✔
86
  url.pathname += '/actions';
20✔
87

88
  if (filters.length > 0) {
20✔
89
    url.searchParams.append('query', filters.join('+'));
12✔
90
  }
91

92
  // Note: the GitHub Actions UI cannot handle encoded '+' characters.
93
  return url.toString().replace(/%2B/g, '+');
20✔
94
}
95

96
async function getDiscussionUrl(notification: Notification): Promise<string> {
97
  const url = new URL(notification.repository.html_url);
6✔
98
  url.pathname += '/discussions';
6✔
99

100
  const discussion = await getLatestDiscussion(notification);
6✔
101

102
  if (discussion) {
6✔
103
    url.href = discussion.url;
2✔
104

105
    const latestComment = getLatestDiscussionComment(discussion.comments.nodes);
2✔
106

107
    if (latestComment) {
2✔
108
      url.hash = `#discussioncomment-${latestComment.databaseId}`;
2✔
109
    }
110
  }
111

112
  return url.toString();
6✔
113
}
114

115
export async function generateGitHubWebUrl(
36✔
116
  notification: Notification,
117
): Promise<string> {
118
  const url = new URL(notification.repository.html_url);
42✔
119

120
  if (notification.subject.latest_comment_url) {
42✔
121
    url.href = await getHtmlUrl(
10✔
122
      notification.subject.latest_comment_url,
123
      notification.account.token,
124
    );
125
  } else if (notification.subject.url) {
32✔
126
    url.href = await getHtmlUrl(
2✔
127
      notification.subject.url,
128
      notification.account.token,
129
    );
130
  } else {
131
    // Perform any specific notification type handling (only required for a few special notification scenarios)
132
    switch (notification.subject.type) {
30✔
133
      case 'CheckSuite':
134
        url.href = getCheckSuiteUrl(notification);
14✔
135
        break;
14✔
136
      case 'Discussion':
137
        url.href = await getDiscussionUrl(notification);
6✔
138
        break;
6✔
139
      case 'RepositoryInvitation':
140
        url.pathname += '/invitations';
2✔
141
        break;
2✔
142
      case 'WorkflowRun':
143
        url.href = getWorkflowRunUrl(notification);
6✔
144
        break;
6✔
145
      default:
146
        break;
2✔
147
    }
148
  }
149

150
  url.searchParams.set(
34✔
151
    'notification_referrer_id',
152
    generateNotificationReferrerId(notification),
153
  );
154

155
  return url.toString();
34✔
156
}
157

158
export function formatForDisplay(text: string[]): string {
36✔
159
  if (!text) {
36✔
160
    return '';
2✔
161
  }
162

163
  return text
34✔
164
    .join(' ')
165
    .replace(/([a-z])([A-Z])/g, '$1 $2') // Add space between lowercase character followed by an uppercase character
166
    .replace(/_/g, ' ') // Replace underscores with spaces
167
    .replace(/\w+/g, (word) => {
168
      // Convert to proper case (capitalize first letter of each word)
169
      return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
68✔
170
    });
171
}
172

173
export function formatNotificationUpdatedAt(
36✔
174
  notification: Notification,
175
): string {
176
  const date = notification.last_read_at ?? notification.updated_at;
26✔
177

178
  return formatDistanceToNow(parseISO(date), {
26✔
179
    addSuffix: true,
180
  });
181
}
182

183
export async function openInBrowser(notification: Notification) {
36✔
184
  const url = await generateGitHubWebUrl(notification);
8✔
185

UNCOV
186
  openExternalLink(url);
×
187
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc