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

badges / shields / 19457485593

16 Nov 2025 08:32PM UTC coverage: 98.15% (+0.04%) from 98.113%
19457485593

push

github

web-flow
Migrate [FlathubVersion] badge to v2 API (#11507)

* Migrate [FlathubVersion] badge to v2 API

* Rename to FlathubVersion

* Replace test with package name containing hyphens

5940 of 6202 branches covered (95.78%)

43 of 43 new or added lines in 1 file covered. (100.0%)

104 existing lines in 6 files now uncovered.

49861 of 50801 relevant lines covered (98.15%)

133.5 hits per line

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

82.81
/services/github/github-api-provider.js
1
import Joi from 'joi'
4✔
2
import log from '../../core/server/log.js'
4✔
3
import { TokenPool } from '../../core/token-pooling/token-pool.js'
4✔
4
import { getUserAgent } from '../../core/base-service/got-config.js'
4✔
5
import { nonNegativeInteger } from '../validators.js'
4✔
6
import { ImproperlyConfigured } from '../index.js'
4✔
7

4✔
8
const userAgent = getUserAgent()
4✔
9

4✔
10
const headerSchema = Joi.object({
4✔
11
  'x-ratelimit-limit': nonNegativeInteger,
4✔
12
  'x-ratelimit-remaining': nonNegativeInteger,
4✔
13
  'x-ratelimit-reset': nonNegativeInteger,
4✔
14
})
4✔
15
  .required()
4✔
16
  .unknown(true)
4✔
17

4✔
18
const bodySchema = Joi.object({
4✔
19
  data: Joi.object({
4✔
20
    rateLimit: Joi.object({
4✔
21
      limit: nonNegativeInteger,
4✔
22
      remaining: nonNegativeInteger,
4✔
23
      resetAt: Joi.date().iso(),
4✔
24
    })
4✔
25
      .required()
4✔
26
      .unknown(true),
4✔
27
  })
4✔
28
    .required()
4✔
29
    .unknown(true),
4✔
30
})
4✔
31
  .required()
4✔
32
  .unknown(true)
4✔
33

4✔
34
// Provides an interface to the Github API. Manages the base URL.
4✔
35
class GithubApiProvider {
4✔
36
  static AUTH_TYPES = {
4✔
37
    NO_AUTH: 'No Auth',
4✔
38
    GLOBAL_TOKEN: 'Global Token',
4✔
39
    TOKEN_POOL: 'Token Pool',
4✔
40
  }
4✔
41

4✔
42
  // reserveFraction: The amount of much of a token's quota we avoid using, to
4✔
43
  //   reserve it for the user.
4✔
44
  constructor({
4✔
45
    baseUrl,
33✔
46
    authType = this.constructor.AUTH_TYPES.NO_AUTH,
33✔
47
    onTokenInvalidated = tokenString => {},
33✔
48
    globalToken,
33✔
49
    reserveFraction = 0.25,
33✔
50
    restApiVersion,
33✔
51
  }) {
33✔
52
    Object.assign(this, {
33✔
53
      baseUrl,
33✔
54
      authType,
33✔
55
      onTokenInvalidated,
33✔
56
      globalToken,
33✔
57
      reserveFraction,
33✔
58
    })
33✔
59

33✔
60
    if (this.authType === this.constructor.AUTH_TYPES.TOKEN_POOL) {
33✔
61
      this.standardTokens = new TokenPool({ batchSize: 25 })
13✔
62
      this.searchTokens = new TokenPool({ batchSize: 5 })
13✔
63
      this.graphqlTokens = new TokenPool({ batchSize: 25 })
13✔
64
    }
13✔
65
    this.restApiVersion = restApiVersion
33✔
66
  }
33✔
67

4✔
68
  addToken(tokenString) {
4✔
69
    if (this.authType === this.constructor.AUTH_TYPES.TOKEN_POOL) {
1✔
70
      this.standardTokens.add(tokenString)
1✔
71
      this.searchTokens.add(tokenString)
1✔
72
      this.graphqlTokens.add(tokenString)
1✔
73
    } else {
1!
74
      throw Error('When not using a token pool, do not provide tokens')
×
75
    }
×
76
  }
1✔
77

4✔
78
  getTokenDebugInfo({ sanitize = true } = {}) {
4✔
UNCOV
79
    if (this.authType === this.constructor.AUTH_TYPES.TOKEN_POOL) {
×
UNCOV
80
      return {
×
UNCOV
81
        standardTokens: this.standardTokens.serializeDebugInfo({ sanitize }),
×
UNCOV
82
        searchTokens: this.searchTokens.serializeDebugInfo({ sanitize }),
×
UNCOV
83
        graphqlTokens: this.graphqlTokens.serializeDebugInfo({ sanitize }),
×
UNCOV
84
      }
×
UNCOV
85
    } else {
×
UNCOV
86
      return {}
×
UNCOV
87
    }
×
UNCOV
88
  }
×
89

4✔
90
  getV3RateLimitFromHeaders(headers) {
4✔
91
    const h = Joi.attempt(headers, headerSchema)
13✔
92
    return {
13✔
93
      rateLimit: h['x-ratelimit-limit'],
13✔
94
      totalUsesRemaining: h['x-ratelimit-remaining'],
13✔
95
      nextReset: h['x-ratelimit-reset'],
13✔
96
    }
13✔
97
  }
13✔
98

4✔
99
  getV4RateLimitFromBody(body) {
4✔
100
    const b = Joi.attempt(body, bodySchema)
2✔
101
    return {
2✔
102
      rateLimit: b.data.rateLimit.limit,
2✔
103
      totalUsesRemaining: b.data.rateLimit.remaining,
2✔
104
      nextReset: Date.parse(b.data.rateLimit.resetAt) / 1000,
2✔
105
    }
2✔
106
  }
2✔
107

4✔
108
  updateToken({ token, url, res }) {
4✔
109
    let rateLimit, totalUsesRemaining, nextReset
16✔
110
    if (url.startsWith('/graphql')) {
16✔
111
      try {
3✔
112
        const parsedBody = JSON.parse(res.body)
3✔
113

3✔
114
        if ('message' in parsedBody && !('data' in parsedBody)) {
3✔
115
          if (parsedBody.message === 'Sorry. Your account was suspended.') {
1✔
116
            this.invalidateToken(token)
1✔
117
            return
1✔
118
          }
1✔
119
        }
1✔
120

2✔
121
        ;({ rateLimit, totalUsesRemaining, nextReset } =
2✔
122
          this.getV4RateLimitFromBody(parsedBody))
2✔
123
      } catch (e) {
3!
124
        console.error(
×
125
          `Could not extract rate limit info from response body ${res.body}`,
×
126
        )
×
127
        log.error(e)
×
128
        return
×
129
      }
×
130
    } else {
16✔
131
      try {
13✔
132
        ;({ rateLimit, totalUsesRemaining, nextReset } =
13✔
133
          this.getV3RateLimitFromHeaders(res.headers))
13✔
134
      } catch (e) {
13!
135
        const logHeaders = {
×
136
          'x-ratelimit-limit': res.headers['x-ratelimit-limit'],
×
137
          'x-ratelimit-remaining': res.headers['x-ratelimit-remaining'],
×
UNCOV
138
          'x-ratelimit-reset': res.headers['x-ratelimit-reset'],
×
UNCOV
139
        }
×
UNCOV
140
        console.error(
×
UNCOV
141
          `Invalid GitHub rate limit headers ${JSON.stringify(
×
UNCOV
142
            logHeaders,
×
UNCOV
143
            undefined,
×
UNCOV
144
            2,
×
UNCOV
145
          )}`,
×
UNCOV
146
        )
×
UNCOV
147
        log.error(e)
×
UNCOV
148
        return
×
UNCOV
149
      }
×
150
    }
13✔
151

15✔
152
    const reserve = Math.ceil(this.reserveFraction * rateLimit)
15✔
153
    const usesRemaining = totalUsesRemaining - reserve
15✔
154

15✔
155
    token.update(usesRemaining, nextReset)
15✔
156
  }
16✔
157

4✔
158
  invalidateToken(token) {
4✔
159
    token.invalidate()
3✔
160
    this.onTokenInvalidated(token.id)
3✔
161
  }
3✔
162

4✔
163
  tokenForUrl(url) {
4✔
164
    if (url.startsWith('/search')) {
22✔
165
      return this.searchTokens.next()
1✔
166
    } else if (url.startsWith('/graphql')) {
22✔
167
      return this.graphqlTokens.next()
5✔
168
    } else {
21✔
169
      return this.standardTokens.next()
16✔
170
    }
16✔
171
  }
22✔
172

4✔
173
  async fetch(requestFetcher, url, options = {}) {
4✔
174
    const { baseUrl } = this
364✔
175

364✔
176
    let token
364✔
177
    let tokenString
364✔
178
    if (this.authType === this.constructor.AUTH_TYPES.TOKEN_POOL) {
364✔
179
      try {
22✔
180
        token = this.tokenForUrl(url)
22✔
181
      } catch (e) {
22!
UNCOV
182
        log.error(e)
×
UNCOV
183
        throw new ImproperlyConfigured({
×
UNCOV
184
          prettyMessage: 'Unable to select next GitHub token from pool',
×
UNCOV
185
        })
×
UNCOV
186
      }
×
187
      tokenString = token.id
22✔
188
    } else if (this.authType === this.constructor.AUTH_TYPES.GLOBAL_TOKEN) {
364✔
189
      tokenString = this.globalToken
332✔
190
    }
332✔
191

364✔
192
    const mergedOptions = {
364✔
193
      ...options,
364✔
194
      ...{
364✔
195
        headers: {
364✔
196
          'User-Agent': userAgent,
364✔
197
          'X-GitHub-Api-Version': this.restApiVersion,
364✔
198
          ...options.headers,
364✔
199
        },
364✔
200
      },
364✔
201
    }
364✔
202
    if (
364✔
203
      this.authType === this.constructor.AUTH_TYPES.TOKEN_POOL ||
364✔
204
      this.authType === this.constructor.AUTH_TYPES.GLOBAL_TOKEN
342✔
205
    ) {
364✔
206
      mergedOptions.headers.Authorization = `token ${tokenString}`
354✔
207
    }
354✔
208

364✔
209
    const response = await requestFetcher(`${baseUrl}${url}`, mergedOptions)
364✔
210
    if (this.authType === this.constructor.AUTH_TYPES.TOKEN_POOL) {
363✔
211
      if (response.res.statusCode === 401) {
21✔
212
        this.invalidateToken(token)
2✔
213
      } else if (response.res.statusCode < 500) {
21✔
214
        this.updateToken({ token, url, res: response.res })
16✔
215
      }
16✔
216
    }
21✔
217
    return response
363✔
218
  }
364✔
219
}
4✔
220

4✔
221
export default GithubApiProvider
4✔
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

© 2026 Coveralls, Inc