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

box / box-java-sdk-gen / #293

24 Jun 2025 01:20PM UTC coverage: 35.661% (+0.03%) from 35.632%
#293

Pull #347

github

web-flow
Merge 2c100d09c into d8480ee6c
Pull Request #347: feat: Add Webhook Validation In Java (box/box-codegen#745)

68 of 82 new or added lines in 2 files covered. (82.93%)

11794 existing lines in 627 files now uncovered.

16937 of 47495 relevant lines covered (35.66%)

0.36 hits per line

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

72.76
/src/main/java/com/box/sdkgen/internal/utils/UtilsManager.java
1
package com.box.sdkgen.internal.utils;
2

3
import com.box.sdkgen.box.errors.BoxSDKError;
4
import com.box.sdkgen.internal.SerializableObject;
5
import com.box.sdkgen.serialization.json.EnumWrapper;
6
import com.box.sdkgen.serialization.json.JsonManager;
7
import com.box.sdkgen.serialization.json.Valuable;
8
import com.fasterxml.jackson.databind.JsonNode;
9
import com.fasterxml.jackson.databind.ObjectMapper;
10
import com.fasterxml.jackson.databind.node.ArrayNode;
11
import java.io.ByteArrayInputStream;
12
import java.io.ByteArrayOutputStream;
13
import java.io.FileNotFoundException;
14
import java.io.FileOutputStream;
15
import java.io.IOException;
16
import java.io.InputStream;
17
import java.io.OutputStream;
18
import java.io.StringReader;
19
import java.math.BigInteger;
20
import java.nio.charset.StandardCharsets;
21
import java.nio.file.Files;
22
import java.nio.file.Paths;
23
import java.security.MessageDigest;
24
import java.security.PrivateKey;
25
import java.security.Security;
26
import java.text.SimpleDateFormat;
27
import java.util.Arrays;
28
import java.util.Base64;
29
import java.util.Date;
30
import java.util.HashMap;
31
import java.util.Iterator;
32
import java.util.List;
33
import java.util.Locale;
34
import java.util.Map;
35
import java.util.Objects;
36
import java.util.Set;
37
import java.util.TimeZone;
38
import java.util.UUID;
39
import java.util.function.BiFunction;
40
import java.util.stream.Collectors;
41
import javax.crypto.Mac;
42
import javax.crypto.spec.SecretKeySpec;
43
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
44
import org.bouncycastle.jce.provider.BouncyCastleProvider;
45
import org.bouncycastle.openssl.PEMDecryptorProvider;
46
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
47
import org.bouncycastle.openssl.PEMKeyPair;
48
import org.bouncycastle.openssl.PEMParser;
49
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
50
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
51
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
52
import org.bouncycastle.operator.InputDecryptorProvider;
53
import org.bouncycastle.operator.OperatorCreationException;
54
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
55
import org.bouncycastle.pkcs.PKCSException;
56
import org.jose4j.jws.JsonWebSignature;
57
import org.jose4j.jwt.JwtClaims;
58
import org.jose4j.jwt.NumericDate;
59
import org.jose4j.lang.JoseException;
60

UNCOV
61
public class UtilsManager {
×
62
  private static final int BUFFER_SIZE = 8192;
63
  private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
1✔
64
  private static final SimpleDateFormat DATE_TIME_FORMAT =
1✔
65
      new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
66
  private static final SimpleDateFormat DATE_TIME_FORMAT_WITH_MILLIS =
1✔
67
      new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
68
  private static final SimpleDateFormat DATE_TIME_FORMAT_WITH_MICROS =
1✔
69
      new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSX");
70

71
  public static <K, V> Map<K, V> mapOf(Entry<K, V>... entries) {
72
    return Arrays.stream(entries)
1✔
73
        .collect(
1✔
74
            HashMap::new,
75
            (map, entry) -> map.put(entry.getKey(), entry.getValue()),
1✔
76
            HashMap::putAll);
77
  }
78

79
  public static <V> Set<V> setOf(V... values) {
UNCOV
80
    return Arrays.stream(values).collect(Collectors.toSet());
×
81
  }
82

83
  public static <K, V> Entry<K, V> entryOf(K key, V value) {
84
    return Entry.of(key, value);
1✔
85
  }
86

87
  public static <K, V> Map<K, V> mergeMaps(Map<K, V> map1, Map<K, V> map2) {
88
    Map<K, V> mergedMap = new HashMap<>();
1✔
89
    if (map1 != null) {
1✔
90
      mergedMap.putAll(map1);
1✔
91
    }
92
    if (map2 != null) {
1✔
93
      mergedMap.putAll(map2);
1✔
94
    }
95
    return mergedMap;
1✔
96
  }
97

98
  public static Map<String, String> prepareParams(Map<String, String> map) {
99
    map.values().removeIf(Objects::isNull);
1✔
100
    return map;
1✔
101
  }
102

103
  public static String convertToString(Object value) {
104
    if (value == null) {
1✔
105
      return null;
1✔
106
    }
107
    if (value instanceof EnumWrapper) {
1✔
108
      return ((EnumWrapper<?>) value).getStringValue();
1✔
109
    }
110
    if (value instanceof Valuable) {
1✔
111
      return ((Valuable) value).getValue();
1✔
112
    }
113
    if (value instanceof List) {
1✔
114
      List<?> list = (List<?>) value;
1✔
115
      if (!list.isEmpty() && list.get(0) instanceof SerializableObject) {
1✔
UNCOV
116
        return JsonManager.serialize(value).toString();
×
117
      } else {
118
        return ((List<?>) value)
1✔
119
            .stream().map(UtilsManager::convertToString).collect(Collectors.joining(","));
1✔
120
      }
121
    }
122
    if (value instanceof ArrayNode) {
1✔
123
      return convertToString(new ObjectMapper().convertValue(value, List.class));
1✔
124
    }
125
    if (value instanceof JsonNode) {
1✔
126
      return ((JsonNode) value).asText();
1✔
127
    }
128
    if (value instanceof SerializableObject) {
1✔
UNCOV
129
      return JsonManager.serialize(value).toString();
×
130
    }
131
    return value.toString();
1✔
132
  }
133

134
  public static void writeInputStreamToOutputStream(InputStream input, OutputStream output) {
135
    try {
136
      byte[] buffer = new byte[BUFFER_SIZE];
1✔
137
      int n = input.read(buffer);
1✔
138
      while (n != -1) {
1✔
139
        output.write(buffer, 0, n);
1✔
140
        n = input.read(buffer);
1✔
141
      }
UNCOV
142
    } catch (IOException e) {
×
143
      throw new RuntimeException(e);
×
144
    } finally {
145
      try {
146
        input.close();
1✔
147
        output.close();
1✔
UNCOV
148
      } catch (IOException e) {
×
UNCOV
149
        throw new RuntimeException(e);
×
150
      }
1✔
151
    }
152
  }
1✔
153

154
  public static String getUuid() {
155
    return UUID.randomUUID().toString();
1✔
156
  }
157

158
  public static byte[] generateByteBuffer(int size) {
159
    byte[] bytes = new byte[size];
1✔
160
    Arrays.fill(bytes, (byte) 0);
1✔
161
    return bytes;
1✔
162
  }
163

164
  public static InputStream generateByteStream(int size) {
165
    byte[] bytes = generateByteBuffer(size);
1✔
166
    return new ByteArrayInputStream(bytes);
1✔
167
  }
168

169
  public static InputStream generateByteStreamFromBuffer(byte[] buffer) {
170
    return new ByteArrayInputStream(buffer);
1✔
171
  }
172

173
  public static byte[] readByteStream(InputStream inputStream) {
174
    ByteArrayOutputStream buffer = new ByteArrayOutputStream();
1✔
175
    byte[] data = new byte[BUFFER_SIZE];
1✔
176
    int bytesRead;
177
    try {
178
      while ((bytesRead = inputStream.read(data, 0, data.length)) != -1) {
1✔
179
        buffer.write(data, 0, bytesRead);
1✔
180
      }
181
    } catch (IOException e) {
×
182
      throw new RuntimeException(e);
×
183
    } finally {
184
      try {
185
        inputStream.close();
1✔
UNCOV
186
      } catch (IOException e) {
×
UNCOV
187
        throw new RuntimeException(e);
×
188
      }
1✔
189
    }
190

191
    return buffer.toByteArray();
1✔
192
  }
193

194
  public static boolean bufferEquals(byte[] buffer1, byte[] buffer2) {
195
    return Arrays.equals(buffer1, buffer2);
1✔
196
  }
197

198
  public static int bufferLength(byte[] buffer) {
199
    return buffer.length;
1✔
200
  }
201

202
  public static InputStream decodeBase64ByteStream(String value) {
203
    return new ByteArrayInputStream(Base64.getDecoder().decode(value));
1✔
204
  }
205

206
  public static String decodeBase64(String value) {
207
    return new String(Base64.getDecoder().decode(value));
1✔
208
  }
209

210
  public static InputStream stringToByteStream(String value) {
211
    return new ByteArrayInputStream(value.getBytes());
1✔
212
  }
213

214
  public static OutputStream getFileOutputStream(String filePath) {
215
    try {
216
      return new FileOutputStream(filePath);
1✔
UNCOV
217
    } catch (FileNotFoundException e) {
×
UNCOV
218
      throw new RuntimeException(e);
×
219
    }
220
  }
221

222
  public static void closeFileOutputStream(OutputStream outputStream) {
223
    try {
224
      outputStream.close();
1✔
UNCOV
225
    } catch (IOException e) {
×
UNCOV
226
      throw new RuntimeException(e);
×
227
    }
1✔
228
  }
1✔
229

230
  public static byte[] readBufferFromFile(String filePath) {
231
    try {
232
      InputStream inputStream = Files.newInputStream(Paths.get(filePath));
1✔
233
      return readByteStream(inputStream);
1✔
UNCOV
234
    } catch (IOException e) {
×
UNCOV
235
      throw new RuntimeException(e);
×
236
    }
237
  }
238

239
  public static String getEnvVar(String envVar) {
240
    return System.getenv(envVar);
1✔
241
  }
242

243
  public static void delayInSeconds(int seconds) {
244
    try {
245
      Thread.sleep(seconds * 1000L);
1✔
UNCOV
246
    } catch (InterruptedException e) {
×
UNCOV
247
      throw new RuntimeException(e);
×
248
    }
1✔
249
  }
1✔
250

251
  public static String readTextFromFile(String filePath) {
252
    try {
UNCOV
253
      return new String(Files.readAllBytes(Paths.get(filePath)));
×
UNCOV
254
    } catch (IOException e) {
×
UNCOV
255
      throw new RuntimeException(e);
×
256
    }
257
  }
258

259
  public static boolean isBrowser() {
260
    return false;
1✔
261
  }
262

263
  public static long getEpochTimeInSeconds() {
264
    return System.currentTimeMillis() / 1000;
1✔
265
  }
266

267
  public static PrivateKey decryptPrivateKey(String encryptedPrivateKey, String passphrase) {
268
    Security.addProvider(new BouncyCastleProvider());
1✔
269
    PrivateKey decryptedPrivateKey;
270
    try {
271
      PEMParser keyReader = new PEMParser(new StringReader(encryptedPrivateKey));
1✔
272
      Object keyPair = keyReader.readObject();
1✔
273
      keyReader.close();
1✔
274

275
      if (keyPair instanceof PrivateKeyInfo) {
1✔
276
        PrivateKeyInfo keyInfo = (PrivateKeyInfo) keyPair;
×
277
        decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
×
278
      } else if (keyPair instanceof PEMEncryptedKeyPair) {
1✔
UNCOV
279
        JcePEMDecryptorProviderBuilder builder = new JcePEMDecryptorProviderBuilder();
×
UNCOV
280
        PEMDecryptorProvider decryptionProvider = builder.build(passphrase.toCharArray());
×
UNCOV
281
        keyPair = ((PEMEncryptedKeyPair) keyPair).decryptKeyPair(decryptionProvider);
×
UNCOV
282
        PrivateKeyInfo keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo();
×
UNCOV
283
        decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
×
284
      } else if (keyPair instanceof PKCS8EncryptedPrivateKeyInfo) {
1✔
285
        InputDecryptorProvider pkcs8Prov =
1✔
286
            new JceOpenSSLPKCS8DecryptorProviderBuilder()
287
                .setProvider("BC")
1✔
288
                .build(passphrase.toCharArray());
1✔
289
        PrivateKeyInfo keyInfo =
1✔
290
            ((PKCS8EncryptedPrivateKeyInfo) keyPair).decryptPrivateKeyInfo(pkcs8Prov);
1✔
291
        decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
1✔
292
      } else {
1✔
293
        PrivateKeyInfo keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo();
×
294
        decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
×
295
      }
296
    } catch (IOException e) {
×
UNCOV
297
      throw new BoxSDKError("Error parsing private key for Box Developer Edition.", e);
×
UNCOV
298
    } catch (OperatorCreationException e) {
×
UNCOV
299
      throw new BoxSDKError("Error parsing PKCS#8 private key for Box Developer Edition.", e);
×
UNCOV
300
    } catch (PKCSException e) {
×
UNCOV
301
      throw new BoxSDKError("Error parsing PKCS private key for Box Developer Edition.", e);
×
302
    }
1✔
303
    return decryptedPrivateKey;
1✔
304
  }
305

306
  public static String createJwtAssertion(
307
      Map<String, Object> claims, JwtKey jwtKey, JwtSignOptions jwtOptions) {
308
    JwtClaims jwtClaims = new JwtClaims();
1✔
309
    jwtClaims.setIssuer(jwtOptions.getIssuer());
1✔
310
    jwtClaims.setAudience(jwtOptions.getAudience());
1✔
311
    jwtClaims.setExpirationTime(NumericDate.fromSeconds((Long) claims.get("exp")));
1✔
312

313
    jwtClaims.setSubject(jwtOptions.getSubject());
1✔
314
    jwtClaims.setClaim("box_sub_type", claims.get("box_sub_type"));
1✔
315
    jwtClaims.setGeneratedJwtId(64);
1✔
316

317
    JsonWebSignature jws = new JsonWebSignature();
1✔
318
    jws.setPayload(jwtClaims.toJson());
1✔
319
    jws.setKey(decryptPrivateKey(jwtKey.getKey(), jwtKey.getPassphrase()));
1✔
320
    jws.setAlgorithmHeaderValue(jwtOptions.getAlgorithm().getValue());
1✔
321
    jws.setHeader("typ", "JWT");
1✔
322
    if ((jwtOptions.getKeyid() != null) && !jwtOptions.getKeyid().isEmpty()) {
1✔
323
      jws.setHeader("kid", jwtOptions.getKeyid());
1✔
324
    }
325

326
    String assertion;
327

328
    try {
329
      assertion = jws.getCompactSerialization();
1✔
UNCOV
330
    } catch (JoseException e) {
×
UNCOV
331
      throw new BoxSDKError("Error serializing JSON Web Token assertion.", e);
×
332
    }
1✔
333

334
    return assertion;
1✔
335
  }
336

337
  public static JsonNode getValueFromObjectRawData(SerializableObject obj, String key) {
338
    JsonNode value = obj.getRawData();
1✔
339
    for (String k : key.split("\\.")) {
1✔
340
      if (value == null || !value.has(k)) {
1✔
UNCOV
341
        return null;
×
342
      }
343
      value = value.get(k);
1✔
344
    }
345

346
    return value;
1✔
347
  }
348

349
  public static double random(double min, double max) {
UNCOV
350
    return Math.random() * (max - min) + min;
×
351
  }
352

353
  public static String hexToBase64(String hex) {
354
    return Base64.getEncoder().encodeToString(new BigInteger(hex, 16).toByteArray());
1✔
355
  }
356

357
  public static Iterator<InputStream> iterateChunks(
358
      InputStream stream, long chunkSize, long fileSize) {
359
    return new Iterator<InputStream>() {
1✔
360
      private boolean streamIsFinished = false;
1✔
361

362
      @Override
363
      public boolean hasNext() {
364
        return !streamIsFinished;
1✔
365
      }
366

367
      @Override
368
      public InputStream next() {
369
        try {
370
          byte[] buffer = new byte[(int) chunkSize];
1✔
371
          int bytesRead = 0;
1✔
372

373
          while (bytesRead < chunkSize) {
1✔
374
            int read = stream.read(buffer, bytesRead, (int) (chunkSize - bytesRead));
1✔
375
            if (read == -1) {
1✔
376
              // End of stream
377
              streamIsFinished = true;
1✔
378
              break;
1✔
379
            }
380
            bytesRead += read;
1✔
381
          }
1✔
382

383
          if (bytesRead == 0) {
1✔
384
            // No more data to yield
UNCOV
385
            streamIsFinished = true;
×
386
            return null;
×
387
          }
388

389
          // Return the chunk as a ByteArrayInputStream
390
          return new ByteArrayInputStream(buffer, 0, bytesRead);
1✔
UNCOV
391
        } catch (Exception e) {
×
UNCOV
392
          throw new RuntimeException("Error reading from stream", e);
×
393
        }
394
      }
395
    };
396
  }
397

398
  /**
399
   * Reduces an iterator using a reducer function and an initial value.
400
   *
401
   * @param <Accumulator> The type of the accumulator (result)
402
   * @param <T> The type of the items in the iterator
403
   * @param iterator The iterator to process
404
   * @param reducer The reducer function
405
   * @param initialValue The initial value for the accumulator
406
   * @return The accumulated result
407
   */
408
  public static <Accumulator, T> Accumulator reduceIterator(
409
      Iterator<T> iterator,
410
      BiFunction<Accumulator, T, Accumulator> reducer,
411
      Accumulator initialValue) {
412
    Accumulator result = initialValue;
1✔
413

414
    while (iterator.hasNext()) {
1✔
415
      T item = iterator.next();
1✔
416
      result = reducer.apply(result, item);
1✔
417
    }
1✔
418

419
    return result;
1✔
420
  }
421

422
  public static Map<String, String> sanitizeMap(
423
      Map<String, String> dictionary, Map<String, String> keysToSanitize) {
424
    return dictionary.entrySet().stream()
×
425
        .collect(
×
426
            Collectors.toMap(
×
427
                Map.Entry::getKey,
428
                entry ->
UNCOV
429
                    keysToSanitize.containsKey(entry.getKey().toLowerCase(Locale.ROOT))
×
UNCOV
430
                        ? JsonManager.sanitizedValue()
×
UNCOV
431
                        : entry.getValue()));
×
432
  }
433

434
  public static Date dateTimeFromString(String dateString) {
435
    SimpleDateFormat[] formats = {
1✔
436
      DATE_TIME_FORMAT, DATE_TIME_FORMAT_WITH_MILLIS, DATE_TIME_FORMAT_WITH_MICROS
437
    };
438

439
    for (SimpleDateFormat format : formats) {
1✔
440
      try {
441
        return format.parse(dateString);
1✔
442
      } catch (java.text.ParseException e) {
1✔
443
        // Ignore and try the next format
444
      }
445
    }
UNCOV
446
    return null;
×
447
  }
448

449
  public static String dateTimeToString(Date dateTime) {
450
    DATE_TIME_FORMAT_WITH_MILLIS.setTimeZone(TimeZone.getTimeZone("UTC"));
1✔
451
    return DATE_TIME_FORMAT_WITH_MILLIS.format(dateTime);
1✔
452
  }
453

454
  public static Date dateFromString(String dateString) {
455
    try {
456
      return DATE_FORMAT.parse(dateString);
1✔
UNCOV
457
    } catch (java.text.ParseException e) {
×
UNCOV
458
      return null;
×
459
    }
460
  }
461

462
  public static String dateToString(Date date) {
463
    DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
1✔
464
    return DATE_FORMAT.format(date);
1✔
465
  }
466

467
  public static String escapeUnicode(String value) {
468
    if (value == null) {
1✔
NEW
469
      return null;
×
470
    }
471

472
    StringBuilder result = new StringBuilder();
1✔
473
    for (int i = 0; i < value.length(); i++) {
1✔
474
      char ch = value.charAt(i);
1✔
475
      if (ch >= 0x007F) {
1✔
476
        result.append(String.format("\\u%04x", (int) ch));
1✔
477
      } else if (ch == '\\') {
1✔
478
        result.append("\\\\");
1✔
479
      } else if (ch == '\n') {
1✔
NEW
480
        result.append("\\n");
×
481
      } else if (ch == '\r') {
1✔
482
        result.append("\\r");
1✔
483
      } else if (ch == '\t') {
1✔
NEW
484
        result.append("\\t");
×
485
      } else if (ch == '/') {
1✔
486
        if (i == 0 || value.charAt(i - 1) != '\\') {
1✔
487
          result.append("\\/");
1✔
488
        } else {
NEW
489
          result.append(ch);
×
490
        }
491
      } else {
492
        result.append(ch);
1✔
493
      }
494
    }
495
    return result.toString();
1✔
496
  }
497

498
  public static Date epochSecondsToDateTime(long seconds) {
499
    return new Date(seconds * 1000);
1✔
500
  }
501

502
  public static long dateTimeToEpochSeconds(Date dateTime) {
503
    return dateTime.getTime() / 1000;
1✔
504
  }
505

506
  public static boolean compareSignatures(String expectedSignature, String receivedSignature) {
507
    if (expectedSignature == null || receivedSignature == null) {
1✔
NEW
508
      return false;
×
509
    }
510
    byte[] expectedBytes = expectedSignature.getBytes(StandardCharsets.UTF_8);
1✔
511
    byte[] receivedBytes = receivedSignature.getBytes(StandardCharsets.UTF_8);
1✔
512
    return MessageDigest.isEqual(expectedBytes, receivedBytes);
1✔
513
  }
514

515
  public static String computeWebhookSignature(
516
      String body, Map<String, String> headers, String signatureKey, boolean escapeBody) {
517
    if (signatureKey == null) {
1✔
NEW
518
      return null;
×
519
    }
520
    if (!"1".equals(headers.get("box-signature-version"))) {
1✔
NEW
521
      return null;
×
522
    }
523
    if (!"HmacSHA256".equals(headers.get("box-signature-algorithm"))) {
1✔
NEW
524
      return null;
×
525
    }
526
    if (!headers.containsKey("box-delivery-timestamp")) {
1✔
NEW
527
      return null;
×
528
    }
529

530
    try {
531
      String escapedBody = escapeBody ? escapeUnicode(body) : body;
1✔
532
      byte[] encodedSignatureKey = signatureKey.getBytes("UTF-8");
1✔
533
      byte[] encodedBody = escapedBody.getBytes("UTF-8");
1✔
534
      byte[] encodedTimestamp = headers.get("box-delivery-timestamp").getBytes("UTF-8");
1✔
535
      Mac mac = Mac.getInstance("HmacSHA256");
1✔
536
      SecretKeySpec secretKeySpec = new SecretKeySpec(encodedSignatureKey, "HmacSHA256");
1✔
537
      mac.init(secretKeySpec);
1✔
538
      mac.update(encodedBody);
1✔
539
      mac.update(encodedTimestamp);
1✔
540
      byte[] hmacDigest = mac.doFinal();
1✔
541
      return Base64.getEncoder().encodeToString(hmacDigest);
1✔
NEW
542
    } catch (Exception e) {
×
NEW
543
      System.err.println("Error computing webhook signature: " + e.getMessage());
×
NEW
544
      return null;
×
545
    }
546
  }
547
}
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