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

vocdoni / saas-backend / 16598104829

29 Jul 2025 01:51PM UTC coverage: 58.021% (+0.1%) from 57.876%
16598104829

push

github

emmdim
api: Adds tests for publish group census endoint

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

123 existing lines in 6 files now uncovered.

5306 of 9145 relevant lines covered (58.02%)

27.02 hits per line

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

97.08
/api/api.go
1
// Package api provides the HTTP API for the Vocdoni SaaS Backend
2
//
3
//        @title                                                Vocdoni SaaS API
4
//        @version                                        1.0
5
//        @description                                API for Vocdoni SaaS Backend
6
//        @termsOfService                                http://swagger.io/terms/
7
//
8
//        @contact.name                                API Support
9
//        @contact.url                                https://vocdoni.io
10
//        @contact.email                                info@vocdoni.io
11
//
12
//        @license.name                                Apache 2.0
13
//        @license.url                                http://www.apache.org/licenses/LICENSE-2.0.html
14
//
15
//        @host                                                localhost:8080
16
//        @BasePath                                        /
17
//        @schemes                                        http https
18
//
19
//        @securityDefinitions.apikey        BearerAuth
20
//        @in                                                        header
21
//        @name                                                Authorization
22
//        @description                                Type "Bearer" followed by a space and the JWT token.
23
//
24
//        @tag.name                                        auth
25
//        @tag.description                        Authentication operations
26
//
27
//        @tag.name                                        users
28
//        @tag.description                        User management operations
29
//
30
//        @tag.name                                        organizations
31
//        @tag.description                        Organization management operations
32
//
33
//        @tag.name                                        plans
34
//        @tag.description                        Subscription plans operations
35
//
36
//        @tag.name                                        census
37
//        @tag.description                        Census management operations
38
//
39
//        @tag.name                                        process
40
//        @tag.description                        Voting process operations
41
//
42
//        @tag.name                                        storage
43
//        @tag.description                        Object storage operations
44
//
45
//        @tag.name                                        transactions
46
//        @tag.description                        Transaction signing operations
47
package api
48

49
import (
50
        "fmt"
51
        "net/http"
52
        "time"
53

54
        "github.com/go-chi/chi/v5"
55
        "github.com/go-chi/chi/v5/middleware"
56
        "github.com/go-chi/cors"
57
        "github.com/go-chi/jwtauth/v5"
58
        "github.com/vocdoni/saas-backend/account"
59
        "github.com/vocdoni/saas-backend/csp"
60
        "github.com/vocdoni/saas-backend/csp/handlers"
61
        "github.com/vocdoni/saas-backend/db"
62
        "github.com/vocdoni/saas-backend/notifications"
63
        "github.com/vocdoni/saas-backend/objectstorage"
64
        "github.com/vocdoni/saas-backend/subscriptions"
65
        "go.vocdoni.io/dvote/apiclient"
66
        "go.vocdoni.io/dvote/log"
67
)
68

69
const (
70
        jwtExpiration = 360 * time.Hour // 15 days
71
        passwordSalt  = "vocdoni365"    // salt for password hashing
72
)
73

74
type Config struct {
75
        Host        string
76
        Port        int
77
        Secret      string
78
        Chain       string
79
        DB          *db.MongoStorage
80
        Client      *apiclient.HTTPclient
81
        Account     *account.Account
82
        MailService notifications.NotificationService
83
        SMSService  notifications.NotificationService
84
        WebAppURL   string
85
        ServerURL   string
86
        // FullTransparentMode if true allows signing all transactions and does not
87
        // modify any of them.
88
        FullTransparentMode bool
89
        // Subscriptions permissions manager
90
        Subscriptions *subscriptions.Subscriptions
91
        // Object storage
92
        ObjectStorage *objectstorage.Client
93
        CSP           *csp.CSP
94
        // OAuth service URL
95
        OAuthServiceURL string
96
}
97

98
// API type represents the API HTTP server with JWT authentication capabilities.
99
type API struct {
100
        db              *db.MongoStorage
101
        auth            *jwtauth.JWTAuth
102
        host            string
103
        port            int
104
        router          *chi.Mux
105
        client          *apiclient.HTTPclient
106
        account         *account.Account
107
        mail            notifications.NotificationService
108
        sms             notifications.NotificationService
109
        secret          string
110
        webAppURL       string
111
        serverURL       string
112
        transparentMode bool
113
        subscriptions   *subscriptions.Subscriptions
114
        objectStorage   *objectstorage.Client
115
        csp             *csp.CSP
116
        oauthServiceURL string
117
}
118

119
// New creates a new API HTTP server. It does not start the server. Use Start() for that.
120
func New(conf *Config) *API {
1✔
121
        if conf == nil {
1✔
122
                return nil
×
123
        }
×
124
        // Set the ServerURL for the ObjectStorageClient
125
        if conf.ObjectStorage != nil {
1✔
126
                conf.ObjectStorage.ServerURL = conf.ServerURL
×
127
        }
×
128

129
        return &API{
1✔
130
                db:              conf.DB,
1✔
131
                auth:            jwtauth.New("HS256", []byte(conf.Secret), nil),
1✔
132
                host:            conf.Host,
1✔
133
                port:            conf.Port,
1✔
134
                client:          conf.Client,
1✔
135
                account:         conf.Account,
1✔
136
                mail:            conf.MailService,
1✔
137
                sms:             conf.SMSService,
1✔
138
                secret:          conf.Secret,
1✔
139
                webAppURL:       conf.WebAppURL,
1✔
140
                serverURL:       conf.ServerURL,
1✔
141
                transparentMode: conf.FullTransparentMode,
1✔
142
                subscriptions:   conf.Subscriptions,
1✔
143
                objectStorage:   conf.ObjectStorage,
1✔
144
                csp:             conf.CSP,
1✔
145
                oauthServiceURL: conf.OAuthServiceURL,
1✔
146
        }
1✔
147
}
148

149
// Start starts the API HTTP server (non blocking).
150
func (a *API) Start() {
1✔
151
        go func() {
2✔
152
                if err := http.ListenAndServe(fmt.Sprintf("%s:%d", a.host, a.port), a.initRouter()); err != nil {
1✔
153
                        log.Fatalf("failed to start the API server: %v", err) //revive:disable:deep-exit
×
154
                }
×
155
        }()
156
}
157

158
// router creates the router with all the routes and middleware.
159
//
160
//revive:disable:function-length
161
func (a *API) initRouter() http.Handler {
1✔
162
        // Create the router with a basic middleware stack
1✔
163
        r := chi.NewRouter()
1✔
164
        r.Use(cors.New(cors.Options{
1✔
165
                AllowedOrigins:   []string{"*"},
1✔
166
                AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
1✔
167
                AllowedHeaders:   []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
1✔
168
                AllowCredentials: true,
1✔
169
                MaxAge:           300, // Maximum value not ignored by any of major browsers
1✔
170
        }).Handler)
1✔
171
        r.Use(middleware.Logger)
1✔
172
        r.Use(middleware.Recoverer)
1✔
173
        r.Use(middleware.Throttle(100))
1✔
174
        r.Use(middleware.ThrottleBacklog(5000, 40000, 60*time.Second))
1✔
175
        r.Use(middleware.Timeout(45 * time.Second))
1✔
176

1✔
177
        a.csp.PasswordSalt = passwordSalt
1✔
178
        cspHandlers := handlers.New(a.csp, a.db)
1✔
179

1✔
180
        // protected routes
1✔
181
        r.Group(func(r chi.Router) {
2✔
182
                // seek, verify and validate JWT tokens
1✔
183
                r.Use(jwtauth.Verifier(a.auth))
1✔
184
                // handle valid JWT tokens
1✔
185
                r.Use(a.authenticator)
1✔
186
                // refresh the token
1✔
187
                log.Infow("new route", "method", "POST", "path", authRefresTokenEndpoint)
1✔
188
                r.Post(authRefresTokenEndpoint, a.refreshTokenHandler)
1✔
189
                // writable organization addresses
1✔
190
                log.Infow("new route", "method", "GET", "path", authAddressesEndpoint)
1✔
191
                r.Get(authAddressesEndpoint, a.writableOrganizationAddressesHandler)
1✔
192
                // get user information
1✔
193
                log.Infow("new route", "method", "GET", "path", usersMeEndpoint)
1✔
194
                r.Get(usersMeEndpoint, a.userInfoHandler)
1✔
195
                // update user information
1✔
196
                log.Infow("new route", "method", "PUT", "path", usersMeEndpoint)
1✔
197
                r.Put(usersMeEndpoint, a.updateUserInfoHandler)
1✔
198
                // update user password
1✔
199
                log.Infow("new route", "method", "PUT", "path", usersPasswordEndpoint)
1✔
200
                r.Put(usersPasswordEndpoint, a.updateUserPasswordHandler)
1✔
201
                // sign a payload
1✔
202
                log.Infow("new route", "method", "POST", "path", signTxEndpoint)
1✔
203
                r.Post(signTxEndpoint, a.signTxHandler)
1✔
204
                // sign a message
1✔
205
                log.Infow("new route", "method", "POST", "path", signMessageEndpoint)
1✔
206
                r.Post(signMessageEndpoint, a.signMessageHandler)
1✔
207
                // create an organization
1✔
208
                log.Infow("new route", "method", "POST", "path", organizationsEndpoint)
1✔
209
                r.Post(organizationsEndpoint, a.createOrganizationHandler)
1✔
210
                // create a route for those endpoints that include the organization
1✔
211
                // address to get the organization data from the database
1✔
212
                // update the organization
1✔
213
                log.Infow("new route", "method", "PUT", "path", organizationEndpoint)
1✔
214
                r.Put(organizationEndpoint, a.updateOrganizationHandler)
1✔
215
                // get organization users
1✔
216
                log.Infow("new route", "method", "GET", "path", organizationUsersEndpoint)
1✔
217
                r.Get(organizationUsersEndpoint, a.organizationUsersHandler)
1✔
218
                // get organization subscription
1✔
219
                log.Infow("new route", "method", "GET", "path", organizationSubscriptionEndpoint)
1✔
220
                r.Get(organizationSubscriptionEndpoint, a.organizationSubscriptionHandler)
1✔
221
                // invite a new user to the organization
1✔
222
                log.Infow("new route", "method", "POST", "path", organizationAddUserEndpoint)
1✔
223
                r.Post(organizationAddUserEndpoint, a.inviteOrganizationUserHandler)
1✔
224
                // update an organization's user role
1✔
225
                log.Infow("new route", "method", "PUT", "path", organizationUpdateUserEndpoint)
1✔
226
                r.Put(organizationUpdateUserEndpoint, a.updateOrganizationUserHandler)
1✔
227
                // remove a user from an organization
1✔
228
                log.Infow("new route", "method", "DELETE", "path", organizationDeleteUserEndpoint)
1✔
229
                r.Delete(organizationDeleteUserEndpoint, a.removeOrganizationUserHandler)
1✔
230
                // get organization censuses
1✔
231
                log.Infow("new route", "method", "GET", "path", organizationCensusesEndpoint)
1✔
232
                r.Get(organizationCensusesEndpoint, a.organizationCensusesHandler)
1✔
233
                // pending organization invitations
1✔
234
                log.Infow("new route", "method", "GET", "path", organizationPendingUsersEndpoint)
1✔
235
                r.Get(organizationPendingUsersEndpoint, a.pendingOrganizationUsersHandler)
1✔
236
                // update pending organization invitation
1✔
237
                log.Infow("new route", "method", "PUT", "path", organizationHandlePendingInvitationEndpoint)
1✔
238
                r.Put(organizationHandlePendingInvitationEndpoint, a.updatePendingUserInvitationHandler)
1✔
239
                // delete pending organization invitation
1✔
240
                log.Infow("new route", "method", "DELETE", "path", organizationHandlePendingInvitationEndpoint)
1✔
241
                r.Delete(organizationHandlePendingInvitationEndpoint, a.deletePendingUserInvitationHandler)
1✔
242
                // get organization members
1✔
243
                log.Infow("new route", "method", "GET", "path", organizationMembersEndpoint)
1✔
244
                r.Get(organizationMembersEndpoint, a.organizationMembersHandler)
1✔
245
                // add organization members
1✔
246
                log.Infow("new route", "method", "POST", "path", organizationAddMembersEndpoint)
1✔
247
                r.Post(organizationAddMembersEndpoint, a.addOrganizationMembersHandler)
1✔
248
                // check the status of the add members job
1✔
249
                log.Infow("new route", "method", "GET", "path", organizationAddMembersJobStatusEndpoint)
1✔
250
                r.Get(organizationAddMembersJobStatusEndpoint, a.addOrganizationMembersJobStatusHandler)
1✔
251
                // delete a set of organization members
1✔
252
                log.Infow("new route", "method", "DELETE", "path", organizationDeleteMembersEndpoint)
1✔
253
                r.Delete(organizationDeleteMembersEndpoint, a.deleteOrganizationMembersHandler)
1✔
254
                // add/overwrite organization meta information
1✔
255
                log.Infow("new route", "method", "POST", "path", organizationMetaEndpoint)
1✔
256
                r.Post(organizationMetaEndpoint, a.addOrganizationMetaHandler)
1✔
257
                // update organization meta information
1✔
258
                log.Infow("new route", "method", "PUT", "path", organizationMetaEndpoint)
1✔
259
                r.Put(organizationMetaEndpoint, a.updateOrganizationMetaHandler)
1✔
260
                // get organization meta information
1✔
261
                log.Infow("new route", "method", "GET", "path", organizationMetaEndpoint)
1✔
262
                r.Get(organizationMetaEndpoint, a.organizationMetaHandler)
1✔
263
                // delete organization meta information
1✔
264
                log.Infow("new route", "method", "DELETE", "path", organizationMetaEndpoint)
1✔
265
                r.Delete(organizationMetaEndpoint, a.deleteOrganizationMetaHandler)
1✔
266
                // create a new ticket for the organization
1✔
267
                log.Infow("new route", "method", "POST", "path", organizationCreateTicketEndpoint)
1✔
268
                r.Post(organizationCreateTicketEndpoint, a.organizationCreateTicket)
1✔
269
                // create a new organization member group
1✔
270
                log.Infow("new route", "method", "POST", "path", organizationGroupsEndpoint)
1✔
271
                r.Post(organizationGroupsEndpoint, a.createOrganizationMemberGroupHandler)
1✔
272
                // get organization member groups list
1✔
273
                log.Infow("new route", "method", "GET", "path", organizationGroupsEndpoint)
1✔
274
                r.Get(organizationGroupsEndpoint, a.organizationMemberGroupsHandler)
1✔
275
                // get details of an organization member group
1✔
276
                log.Infow("new route", "method", "GET", "path", organizationGroupEndpoint)
1✔
277
                r.Get(organizationGroupEndpoint, a.organizationMemberGroupHandler)
1✔
278
                // get members of an organization member group
1✔
279
                log.Infow("new route", "method", "GET", "path", organizationGroupMembersEndpoint)
1✔
280
                r.Get(organizationGroupMembersEndpoint, a.listOrganizationMemberGroupsHandler)
1✔
281
                // update an organization member group
1✔
282
                log.Infow("new route", "method", "PUT", "path", organizationGroupEndpoint)
1✔
283
                r.Put(organizationGroupEndpoint, a.updateOrganizationMemberGroupHandler)
1✔
284
                // delete an organization member group
1✔
285
                log.Infow("new route", "method", "DELETE", "path", organizationGroupEndpoint)
1✔
286
                r.Delete(organizationGroupEndpoint, a.deleteOrganizationMemberGroupHandler)
1✔
287
                // validate the member data of an organization member group
1✔
288
                log.Infow("new route", "method", "POST", "path", organizationGroupValidateEndpoint)
1✔
289
                r.Post(organizationGroupValidateEndpoint, a.organizationMemberGroupValidateHandler)
1✔
290

1✔
291
                // handle stripe checkout session
1✔
292
                log.Infow("new route", "method", "POST", "path", subscriptionsCheckout)
1✔
293
                r.Post(subscriptionsCheckout, a.createSubscriptionCheckoutHandler)
1✔
294
                // get stripe checkout session info
1✔
295
                log.Infow("new route", "method", "GET", "path", subscriptionsCheckoutSession)
1✔
296
                r.Get(subscriptionsCheckoutSession, a.checkoutSessionHandler)
1✔
297
                // get stripe subscription portal session info
1✔
298
                log.Infow("new route", "method", "GET", "path", subscriptionsPortal)
1✔
299
                r.Get(subscriptionsPortal, a.createSubscriptionPortalSessionHandler)
1✔
300
                // upload an image to the object storage
1✔
301
                log.Infow("new route", "method", "POST", "path", objectStorageUploadTypedEndpoint)
1✔
302
                r.Post(objectStorageUploadTypedEndpoint, a.objectStorage.UploadImageWithFormHandler)
1✔
303
                // CENSUS ROUTES
1✔
304
                // create census
1✔
305
                log.Infow("new route", "method", "POST", "path", censusEndpoint)
1✔
306
                r.Post(censusEndpoint, a.createCensusHandler)
1✔
307
                // add census participants
1✔
308
                log.Infow("new route", "method", "POST", "path", censusIDEndpoint)
1✔
309
                r.Post(censusIDEndpoint, a.addCensusParticipantsHandler)
1✔
310
                // get census participants job
1✔
311
                log.Infow("new route", "method", "GET", "path", censusAddParticipantsJobStatusEndpoint)
1✔
312
                r.Get(censusAddParticipantsJobStatusEndpoint, a.censusAddParticipantsJobStatusHandler)
1✔
313
                // publish census
1✔
314
                log.Infow("new route", "method", "POST", "path", censusPublishEndpoint)
1✔
315
                r.Post(censusPublishEndpoint, a.publishCensusHandler)
1✔
316
                // publish group census
1✔
317
                log.Infow("new route", "method", "POST", "path", censusGroupPublishEndpoint)
1✔
318
                r.Post(censusGroupPublishEndpoint, a.publishCensusGroupHandler)
1✔
319
                // PROCESS ROUTES
1✔
320
                log.Infow("new route", "method", "POST", "path", processEndpoint)
1✔
321
                r.Post(processEndpoint, a.createProcessHandler)
1✔
322
                // PROCESS BUNDLE ROUTES (private)
1✔
323
                log.Infow("new route", "method", "POST", "path", processBundleEndpoint)
1✔
324
                r.Post(processBundleEndpoint, a.createProcessBundleHandler)
1✔
325
                log.Infow("new route", "method", "PUT", "path", processBundleUpdateEndpoint)
1✔
326
                r.Put(processBundleUpdateEndpoint, a.updateProcessBundleHandler)
1✔
327
        })
1✔
328

329
        // Public routes
330
        r.Group(func(r chi.Router) {
2✔
331
                r.Get("/ping", func(w http.ResponseWriter, _ *http.Request) {
2✔
332
                        if _, err := w.Write([]byte(".")); err != nil {
1✔
UNCOV
333
                                log.Warnw("failed to write ping response", "error", err)
×
UNCOV
334
                        }
×
335
                })
336
                // login
337
                log.Infow("new route", "method", "POST", "path", authLoginEndpoint)
1✔
338
                r.Post(authLoginEndpoint, a.authLoginHandler)
1✔
339
                // oauth login
1✔
340
                log.Infow("new route", "method", "POST", "path", oauthLoginEndpoint)
1✔
341
                r.Post(oauthLoginEndpoint, a.oauthLoginHandler)
1✔
342
                // register user
1✔
343
                log.Infow("new route", "method", "POST", "path", usersEndpoint)
1✔
344
                r.Post(usersEndpoint, a.registerHandler)
1✔
345
                // verify user
1✔
346
                log.Infow("new route", "method", "POST", "path", verifyUserEndpoint)
1✔
347
                r.Post(verifyUserEndpoint, a.verifyUserAccountHandler)
1✔
348
                // get user verification code information
1✔
349
                log.Infow("new route", "method", "GET", "path", verifyUserCodeEndpoint)
1✔
350
                r.Get(verifyUserCodeEndpoint, a.userVerificationCodeInfoHandler)
1✔
351
                // resend user verification code
1✔
352
                log.Infow("new route", "method", "POST", "path", verifyUserCodeEndpoint)
1✔
353
                r.Post(verifyUserCodeEndpoint, a.resendUserVerificationCodeHandler)
1✔
354
                // request user password recovery
1✔
355
                log.Infow("new route", "method", "POST", "path", usersRecoveryPasswordEndpoint)
1✔
356
                r.Post(usersRecoveryPasswordEndpoint, a.recoverUserPasswordHandler)
1✔
357
                // reset user password
1✔
358
                log.Infow("new route", "method", "POST", "path", usersResetPasswordEndpoint)
1✔
359
                r.Post(usersResetPasswordEndpoint, a.resetUserPasswordHandler)
1✔
360
                // get organization information
1✔
361
                log.Infow("new route", "method", "GET", "path", organizationEndpoint)
1✔
362
                r.Get(organizationEndpoint, a.organizationInfoHandler)
1✔
363
                // accept organization invitation
1✔
364
                log.Infow("new route", "method", "POST", "path", organizationAcceptUserEndpoint)
1✔
365
                r.Post(organizationAcceptUserEndpoint, a.acceptOrganizationUserInvitationHandler)
1✔
366
                // get organization roles
1✔
367
                log.Infow("new route", "method", "GET", "path", organizationRolesEndpoint)
1✔
368
                r.Get(organizationRolesEndpoint, a.organizationRolesHandler)
1✔
369
                // get organization types
1✔
370
                log.Infow("new route", "method", "GET", "path", organizationTypesEndpoint)
1✔
371
                r.Get(organizationTypesEndpoint, a.organizationsTypesHandler)
1✔
372
                // get subscriptions
1✔
373
                log.Infow("new route", "method", "GET", "path", plansEndpoint)
1✔
374
                r.Get(plansEndpoint, a.plansHandler)
1✔
375
                // get subscription info
1✔
376
                log.Infow("new route", "method", "GET", "path", planInfoEndpoint)
1✔
377
                r.Get(planInfoEndpoint, a.planInfoHandler)
1✔
378
                // handle stripe webhook
1✔
379
                log.Infow("new route", "method", "POST", "path", subscriptionsWebhook)
1✔
380
                r.Post(subscriptionsWebhook, a.handleWebhook)
1✔
381
                // upload an image to the object storage
1✔
382
                log.Infow("new route", "method", "GET", "path", objectStorageDownloadTypedEndpoint)
1✔
383
                r.Get(objectStorageDownloadTypedEndpoint, a.objectStorage.DownloadImageInlineHandler)
1✔
384
                // get census info
1✔
385
                log.Infow("new route", "method", "GET", "path", censusIDEndpoint)
1✔
386
                r.Get(censusIDEndpoint, a.censusInfoHandler)
1✔
387
                // process info handler
1✔
388
                log.Infow("new route", "method", "GET", "path", processEndpoint)
1✔
389
                r.Get(processEndpoint, a.processInfoHandler)
1✔
390
                // process sign info handler
1✔
391
                log.Infow("new route", "method", "POST", "path", processSignInfoEndpoint)
1✔
392
                r.Post(processSignInfoEndpoint, cspHandlers.ConsumedAddressHandler)
1✔
393
                // process bundle info handler
1✔
394
                log.Infow("new route", "method", "GET", "path", processBundleInfoEndpoint)
1✔
395
                r.Get(processBundleInfoEndpoint, a.processBundleInfoHandler)
1✔
396
                // process bundle auth handler
1✔
397
                log.Infow("new route", "method", "POST", "path", processBundleAuthEndpoint)
1✔
398
                // r.Post(processBundleAuthEndpoint, a.processBundleAuthHandler)
1✔
399
                r.Post(processBundleAuthEndpoint, cspHandlers.BundleAuthHandler)
1✔
400
                // process bundle sign handler
1✔
401
                log.Infow("new route", "method", "POST", "path", processBundleSignEndpoint)
1✔
402
                // r.Post(processBundleSignEndpoint, a.processBundleSignHandler)
1✔
403
                r.Post(processBundleSignEndpoint, cspHandlers.BundleSignHandler)
1✔
404
                // process bundle member info handler
1✔
405
                log.Infow("new route", "method", "GET", "path", processBundleMemberEndpoint)
1✔
406
                r.Get(processBundleMemberEndpoint, a.processBundleParticipantInfoHandler)
1✔
407
        })
408
        a.router = r
1✔
409
        return r
1✔
410
}
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