diff --git a/src/jetstream/api/structs.go b/src/jetstream/api/structs.go index dae28bacfc..5f9bc171f2 100644 --- a/src/jetstream/api/structs.go +++ b/src/jetstream/api/structs.go @@ -411,6 +411,7 @@ type PortalConfig struct { LogLevel string `configName:"LOG_LEVEL"` UIListMaxSize int64 `configName:"UI_LIST_MAX_SIZE"` UIListAllowLoadMaxed bool `configName:"UI_LIST_ALLOW_LOAD_MAXED"` + AutoRefreshCNSITokens bool `configName:"AUTOREFRESH_CNSI_TOKENS"` CFAdminIdentifier string CloudFoundryInfo *CFInfo HTTPS bool diff --git a/src/jetstream/api/tokens.go b/src/jetstream/api/tokens.go index cfa4a2c7d4..fe6249cfa5 100644 --- a/src/jetstream/api/tokens.go +++ b/src/jetstream/api/tokens.go @@ -12,6 +12,7 @@ type TokenRepository interface { FindAuthToken(userGUID string, encryptionKey []byte) (TokenRecord, error) SaveAuthToken(userGUID string, tokenRecord TokenRecord, encryptionKey []byte) error + ListAllEnabledConnectedCNSITokens(encryptionKey []byte) ([]BackupTokenRecord, error) FindCNSIToken(cnsiGUID string, userGUID string, encryptionKey []byte) (TokenRecord, error) FindCNSITokenIncludeDisconnected(cnsiGUID string, userGUID string, encryptionKey []byte) (TokenRecord, error) FindAllCNSITokenBackup(cnsiGUID string, encryptionKey []byte) ([]BackupTokenRecord, error) diff --git a/src/jetstream/cnsi.go b/src/jetstream/cnsi.go index e8d69efc33..e086f5e675 100644 --- a/src/jetstream/cnsi.go +++ b/src/jetstream/cnsi.go @@ -9,6 +9,7 @@ import ( "net/url" "strconv" "strings" + "time" "github.com/labstack/echo/v4" log "github.com/sirupsen/logrus" @@ -699,6 +700,63 @@ func (p *portalProxy) updateTokenAuth(userGUID string, t api.TokenRecord) error return nil } +func (p *portalProxy) startCNSITokenRefreshRoutines() error { + log.Debug("startCNSITokenRefreshRoutines") + + tokenRepo, err := p.GetStoreFactory().TokenStore() + if err != nil { + log.Errorf(dbReferenceError, err) + return fmt.Errorf(dbReferenceError, err) + } + + tokens, err := tokenRepo.ListAllEnabledConnectedCNSITokens(p.Config.EncryptionKeyInBytes) + if err != nil { + msg := "unable to list enabled and connected cnsi tokens: %v" + log.Errorf(msg, err) + return fmt.Errorf(msg, err) + } + + for _, token := range tokens { + p.refreshRoutines.wg.Add(1) + go p.refreshToken(token) + } + + return nil +} + +func (p *portalProxy) refreshToken(token api.BackupTokenRecord) { + log.Debug("refreshToken") + defer p.refreshRoutines.wg.Done() + for { + endpoint, err := p.GetCNSIRecord(token.EndpointGUID) + if err != nil { + // Check if the endpoint doesn't exist anymore, if so shut down routine + // Depends on the implementation of EndpointRepository interface from api/cnsis.go, + // but all current implementations pass through to repository/cnsis/pgsql_cnsis.go line 308 eventually + if err.Error() == "No match for that Endpoint" { + log.Infof("endpoint '%v' no longer exists, shutting down token refresher routine", token.EndpointGUID) + return + } + // If any other error occurred, log it and retry + log.Errorf("could not get retrieve endpoint record to refresh cnsi token '%v': %v", token.TokenRecord.TokenGUID, err) + continue + } + expiry := time.Unix(token.TokenRecord.TokenExpiry, 0) + select { + case <-time.After(time.Until(expiry)): + case <-p.refreshRoutines.context.Done(): + return + } + + updatedTokenRecord, err := p.RefreshOAuthToken(endpoint.SkipSSLValidation, token.EndpointGUID, token.UserGUID, endpoint.ClientId, endpoint.ClientSecret, endpoint.TokenEndpoint) + if err != nil { + log.Errorf("could not refresh cnsi token '%v': %v", token.TokenRecord.TokenGUID, err) + continue + } + token.TokenRecord = updatedTokenRecord + } +} + func (p *portalProxy) setCNSITokenRecord(cnsiGUID string, userGUID string, t api.TokenRecord) error { log.Debug("setCNSITokenRecord") tokenRepo, err := p.GetStoreFactory().TokenStore() @@ -714,6 +772,11 @@ func (p *portalProxy) setCNSITokenRecord(cnsiGUID string, userGUID string, t api return fmt.Errorf(msg, err) } + if p.Config.AutoRefreshCNSITokens { + p.refreshRoutines.wg.Add(1) + go p.refreshToken(api.BackupTokenRecord{TokenRecord: t, UserGUID: userGUID, EndpointGUID: cnsiGUID}) + } + return nil } diff --git a/src/jetstream/cnsi_test.go b/src/jetstream/cnsi_test.go index b6335ed01c..93dccb4056 100644 --- a/src/jetstream/cnsi_test.go +++ b/src/jetstream/cnsi_test.go @@ -1,9 +1,14 @@ package main import ( + "bytes" + "encoding/base64" + "encoding/json" "errors" "net/http" "net/http/httptest" + "net/url" + "strings" "testing" "time" @@ -251,6 +256,159 @@ func TestGetCFv2InfoWithInvalidEndpoint(t *testing.T) { } } +func TestRegisterEndpointStartsRefreshRoutine(t *testing.T) { + t.Parallel() + + Convey("Request to register endpoint", t, func() { + // mock StratosAuthService + ctrl := gomock.NewController(t) + mockStratosAuth := mock_api.NewMockStratosAuth(ctrl) + defer ctrl.Finish() + + // setup mock DB, PortalProxy and mock StratosAuthService + pp, db, mock := setupPortalProxyWithAuthService(mockStratosAuth) + defer db.Close() + + pp.Config.AutoRefreshCNSITokens = true + + // mock individual APIEndpoints + mockV2Info := setupMockEndpointServer(t) + defer mockV2Info.Close() + + mockUAAResponseModifiedExpiry := mockUAAResponse + + splits := strings.Split(mockUAAResponse.AccessToken, ".") + + decoded, _ := base64.RawStdEncoding.DecodeString(splits[1]) + + u := new(api.JWTUserTokenInfo) + json.Unmarshal(decoded, &u) + + u.TokenExpiry = time.Now().Add(time.Minute * 5).Unix() + + encode, _ := json.Marshal(u) + + splits[1] = base64.RawStdEncoding.EncodeToString(encode) + + mockUAAResponseModifiedExpiry.AccessToken = strings.Join(splits, ".") + + mockUAA := setupMockServer(t, + msRoute("/oauth/token"), + msMethod("POST"), + msStatus(http.StatusOK), + msBody(jsonMust(mockUAAResponseModifiedExpiry))) + + // mock different users + mockAdmin := setupMockUser(mockAdminGUID, true, []string{}) + + pp.GetConfig().UserEndpointsEnabled = config.UserEndpointsConfigEnum.Enabled + + // setup + adminEndpoint := setupMockEndpointRegisterRequest(t, mockAdmin.ConnectedUser, mockV2Info, "CF Cluster 1", true, true) + + if errSession := pp.setSessionValues(adminEndpoint.EchoContext, mockAdmin.SessionValues); errSession != nil { + t.Error(errors.New("unable to mock/stub user in session object")) + } + + Convey("registering a new endpoint and logging in leads to a refresh routine being started", func() { + // mock executions + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(mockAdmin.ConnectedUser.GUID)). + Return(mockAdmin.ConnectedUser, nil) + + mock. + ExpectQuery(selectFromCNSIs). + WillReturnRows( + sqlmock.NewRows( + []string{"guid", "name", "cnsi_type", "api_endpoint", "auth_endpoint", "token_endpoint", "doppler_logging_endpoint", "skip_ssl_validation", "client_id", "client_secret", "sso_allowed", "sub_type", "meta_data", "creator", "ca_cert"}, + ), + ) + mock. + ExpectExec(insertIntoCNSIs). + WillReturnResult(sqlmock.NewResult(1, 1)) + + fetchInfo := getCFPlugin(pp, "cf").Info + err := pp.RegisterEndpoint(adminEndpoint.EchoContext, fetchInfo) + + So(err, ShouldBeNil) + + first := adminEndpoint.QueryArgs[:4] + newRow := append(first, mockUAA.URL) + last := adminEndpoint.QueryArgs[5:] + newRow = append(newRow, last...) + + mock. + ExpectQuery(selectAnyFromCNSIs). + WillReturnRows( + sqlmock.NewRows( + []string{"guid", "name", "cnsi_type", "api_endpoint", "auth_endpoint", "token_endpoint", "doppler_logging_endpoint", "skip_ssl_validation", "client_id", "client_secret", "sso_allowed", "sub_type", "meta_data", "creator", "ca_cert"}, + ).AddRow(newRow...), + ) + + mock. + ExpectQuery(selectAnyFromCNSIs). + WillReturnRows( + sqlmock.NewRows( + []string{"guid", "name", "cnsi_type", "api_endpoint", "auth_endpoint", "token_endpoint", "doppler_logging_endpoint", "skip_ssl_validation", "client_id", "client_secret", "sso_allowed", "sub_type", "meta_data", "creator", "ca_cert"}, + ).AddRow(newRow...), + ) + + mock. + ExpectQuery(selectAnyFromTokens). + WithArgs(newRow[0], mockAdmin.ConnectedUser.GUID). + WillReturnRows(sqlmock.NewRows([]string{"count(*)"}).AddRow(0)) + + mock. + ExpectExec(insertIntoTokens). + WillReturnResult(sqlmock.NewResult(1, 1)) + + mock. + ExpectQuery(selectAnyFromCNSIs). + WillReturnRows( + sqlmock.NewRows( + []string{"guid", "name", "cnsi_type", "api_endpoint", "auth_endpoint", "token_endpoint", "doppler_logging_endpoint", "skip_ssl_validation", "client_id", "client_secret", "sso_allowed", "sub_type", "meta_data", "creator", "ca_cert"}, + ).AddRow(newRow...), + ) + + mock. + ExpectQuery(selectAnyFromCNSIs). + WillReturnRows( + sqlmock.NewRows( + []string{"guid", "name", "cnsi_type", "api_endpoint", "auth_endpoint", "token_endpoint", "doppler_logging_endpoint", "skip_ssl_validation", "client_id", "client_secret", "sso_allowed", "sub_type", "meta_data", "creator", "ca_cert"}, + ).AddRow(newRow...), + ) + + mockStratosAuth. + EXPECT(). + GetUser(gomock.Eq(mockAdmin.ConnectedUser.GUID)). + Return(mockAdmin.ConnectedUser, nil) + + // value are irrelevant, since we mock the reponse from the uaa regardless but the login won't work without them + formDataForApiLogin := url.Values{} + formDataForApiLogin.Set("username", "test") + formDataForApiLogin.Set("password", "test") + newReq, _ := http.NewRequest(http.MethodPost, "localhost:9999/some/fake/url", bytes.NewBufferString(formDataForApiLogin.Encode())) + newReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + newContext := adminEndpoint.EchoContext.Echo().NewContext(newReq, adminEndpoint.EchoContext.Response()) + _, err = pp.DoLoginToCNSI(newContext, adminEndpoint.InsertArgs[0].(string), true) + + // Asynchronosly wait 5 seconds, then cancel the refresh routines + go func() { + time.Sleep(time.Second * 5) + pp.refreshRoutines.cancel() + }() + + // Wait until all refresh routines have terminated (portalProxy does the same on graceful shutdown) + pp.refreshRoutines.wg.Wait() + + So(err, ShouldBeNil) + So(mock.ExpectationsWereMet(), ShouldBeNil) + }) + }) +} + func TestRegisterWithUserEndpointsEnabled(t *testing.T) { // execute this in parallel t.Parallel() diff --git a/src/jetstream/main.go b/src/jetstream/main.go index 624baab76c..9fb6d1193b 100644 --- a/src/jetstream/main.go +++ b/src/jetstream/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "crypto/sha1" "database/sql" "encoding/gob" @@ -282,6 +283,8 @@ func main() { log.Info("Initialization complete.") + ctx, cancel := context.WithCancel(context.Background()) + portalProxy.SetRefreshRoutineContext(ctx, cancel) c := make(chan os.Signal, 2) signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { @@ -290,6 +293,9 @@ func main() { fmt.Println() log.Info("Attempting to shut down gracefully...") + // Cancel portal proxy context + cancel() + // Database connection pool log.Info(`... Closing database connection pool`) databaseConnectionPool.Close() @@ -310,6 +316,8 @@ func main() { pCleanup.Destroy() } } + // wait for any goroutines to shut down + portalProxy.refreshRoutines.wg.Wait() log.Info("Graceful shut down complete") os.Exit(1) @@ -804,6 +812,12 @@ func start(config api.PortalConfig, p *portalProxy, needSetupMiddleware bool, is go stopEchoWhenUpgraded(e, p.Env()) } + if p.Config.AutoRefreshCNSITokens { + if err := p.startCNSITokenRefreshRoutines(); err != nil { + return err + } + } + var engineErr error address := config.TLSAddress if config.HTTPS { @@ -1194,3 +1208,9 @@ func (portalProxy *portalProxy) SetStoreFactory(f api.StoreFactory) api.StoreFac portalProxy.StoreFactory = f return old } + +// SetContext sets the context +func (portalProxy *portalProxy) SetRefreshRoutineContext(ctx context.Context, cancel context.CancelFunc) { + portalProxy.refreshRoutines.context = ctx + portalProxy.refreshRoutines.cancel = cancel +} diff --git a/src/jetstream/mock_server_test.go b/src/jetstream/mock_server_test.go index 4b464858ca..6e9612de3b 100644 --- a/src/jetstream/mock_server_test.go +++ b/src/jetstream/mock_server_test.go @@ -1,6 +1,7 @@ package main import ( + "context" "crypto/sha1" "database/sql" "database/sql/driver" @@ -182,6 +183,8 @@ func setupPortalProxy(db *sql.DB) *portalProxy { store := factory.NewDefaultStoreFactory(db) pp.SetStoreFactory(store) + pp.SetRefreshRoutineContext(context.WithCancel(context.Background())) + return pp } diff --git a/src/jetstream/plugins/desktop/helm/tokens.go b/src/jetstream/plugins/desktop/helm/tokens.go index 9385450730..febdd57031 100644 --- a/src/jetstream/plugins/desktop/helm/tokens.go +++ b/src/jetstream/plugins/desktop/helm/tokens.go @@ -18,6 +18,10 @@ func (d *TokenStore) SaveAuthToken(userGUID string, tokenRecord api.TokenRecord, return d.store.SaveAuthToken(userGUID, tokenRecord, encryptionKey) } +func (d *TokenStore) ListAllEnabledConnectedCNSITokens(encryptionKey []byte) ([]api.BackupTokenRecord, error) { + return d.store.ListAllEnabledConnectedCNSITokens(encryptionKey) +} + func (d *TokenStore) FindCNSIToken(cnsiGUID string, userGUID string, encryptionKey []byte) (api.TokenRecord, error) { return d.store.FindCNSIToken(cnsiGUID, userGUID, encryptionKey) } diff --git a/src/jetstream/plugins/desktop/kubernetes/tokens.go b/src/jetstream/plugins/desktop/kubernetes/tokens.go index 8dcbc792d4..698e9db4b5 100644 --- a/src/jetstream/plugins/desktop/kubernetes/tokens.go +++ b/src/jetstream/plugins/desktop/kubernetes/tokens.go @@ -18,6 +18,10 @@ func (d *TokenStore) SaveAuthToken(userGUID string, tokenRecord api.TokenRecord, return d.store.SaveAuthToken(userGUID, tokenRecord, encryptionKey) } +func (d *TokenStore) ListAllEnabledConnectedCNSITokens(encryptionKey []byte) ([]api.BackupTokenRecord, error) { + return d.store.ListAllEnabledConnectedCNSITokens(encryptionKey) +} + func (d *TokenStore) FindCNSIToken(cnsiGUID string, userGUID string, encryptionKey []byte) (api.TokenRecord, error) { local, cfg, err := ListKubernetes() diff --git a/src/jetstream/plugins/desktop/tokens.go b/src/jetstream/plugins/desktop/tokens.go index a2c2c69fc1..79f2e310c1 100644 --- a/src/jetstream/plugins/desktop/tokens.go +++ b/src/jetstream/plugins/desktop/tokens.go @@ -21,6 +21,10 @@ func (d *TokenStore) SaveAuthToken(userGUID string, tokenRecord api.TokenRecord, return d.store.SaveAuthToken(userGUID, tokenRecord, encryptionKey) } +func (d *TokenStore) ListAllEnabledConnectedCNSITokens(encryptionKey []byte) ([]api.BackupTokenRecord, error) { + return d.store.ListAllEnabledConnectedCNSITokens(encryptionKey) +} + func (d *TokenStore) FindCNSIToken(cnsiGUID string, userGUID string, encryptionKey []byte) (api.TokenRecord, error) { // Main method that we need to override to get the token for the given endpoint diff --git a/src/jetstream/portal_proxy.go b/src/jetstream/portal_proxy.go index 65384486d8..4110794909 100644 --- a/src/jetstream/portal_proxy.go +++ b/src/jetstream/portal_proxy.go @@ -1,8 +1,10 @@ package main import ( + "context" "database/sql" "regexp" + "sync" "time" "github.com/cloudfoundry/stratos/src/jetstream/api" @@ -25,10 +27,15 @@ type portalProxy struct { EmptyCookieMatcher *regexp.Regexp // Used to detect and remove empty Cookies sent by certain browsers AuthProviders map[string]api.AuthProvider env *env.VarSet - StratosAuthService api.StratosAuth - APIKeysRepository apikeys.Repository - PluginRegisterRoutes map[string]func(echo.Context) error - StoreFactory api.StoreFactory + refreshRoutines struct { + wg sync.WaitGroup + context context.Context + cancel context.CancelFunc + } + StratosAuthService api.StratosAuth + APIKeysRepository apikeys.Repository + PluginRegisterRoutes map[string]func(echo.Context) error + StoreFactory api.StoreFactory } // HttpSessionStore - Interface for a store that can manage HTTP Sessions diff --git a/src/jetstream/repository/tokens/pgsql_tokens.go b/src/jetstream/repository/tokens/pgsql_tokens.go index d0b31907f3..2256179b9e 100644 --- a/src/jetstream/repository/tokens/pgsql_tokens.go +++ b/src/jetstream/repository/tokens/pgsql_tokens.go @@ -35,6 +35,10 @@ var getTokenConnected = `SELECT token_guid, auth_token, refresh_token, token_exp FROM tokens WHERE user_guid = $1 AND token_guid = $2 AND disconnected = '0'` +var listAllEnabledConnectedCNSITokens = `SELECT cnsi_guid, token_guid, auth_token, refresh_token, token_expiry, user_guid + FROM tokens + WHERE token_type = 'cnsi' AND enabled = '1' AND disconnected = '0'` + var findCNSIToken = `SELECT token_guid, auth_token, refresh_token, token_expiry, disconnected, auth_type, meta_data, user_guid, linked_token, enabled FROM tokens WHERE cnsi_guid = $1 AND (user_guid = $2 OR user_guid = $3) AND token_type = 'cnsi'` @@ -87,6 +91,7 @@ func InitRepositoryProvider(databaseProvider string) { countAuthTokens = datastore.ModifySQLStatement(countAuthTokens, databaseProvider) insertAuthToken = datastore.ModifySQLStatement(insertAuthToken, databaseProvider) updateAuthToken = datastore.ModifySQLStatement(updateAuthToken, databaseProvider) + listAllEnabledConnectedCNSITokens = datastore.ModifySQLStatement(listAllEnabledConnectedCNSITokens, databaseProvider) findCNSIToken = datastore.ModifySQLStatement(findCNSIToken, databaseProvider) findCNSITokenConnected = datastore.ModifySQLStatement(findCNSITokenConnected, databaseProvider) findAllCNSIToken = datastore.ModifySQLStatement(findAllCNSIToken, databaseProvider) @@ -323,6 +328,81 @@ func (p *PgsqlTokenRepository) SaveCNSIToken(cnsiGUID string, userGUID string, t return nil } +func (p *PgsqlTokenRepository) ListAllEnabledConnectedCNSITokens(encryptionKey []byte) ([]api.BackupTokenRecord, error) { + log.Debug("ListAllEnabledConnectedCNSITokens") + + rows, err := p.db.Query(listAllEnabledConnectedCNSITokens) + if err != nil { + msg := "Unable to Find All CNSI tokens: %v" + if err == sql.ErrNoRows { + log.Debugf(msg, err) + } else { + log.Errorf(msg, err) + } + return make([]api.BackupTokenRecord, 0), fmt.Errorf(msg, err) + } + + defer rows.Close() + + btrs := make([]api.BackupTokenRecord, 0) + + for rows.Next() { + // temp vars to retrieve db data + // cnsi_guid, token_guid, auth_token, refresh_token, token_expiry, user_guid + var ( + cnsiGUID sql.NullString + tokenGUID sql.NullString + ciphertextAuthToken []byte + ciphertextRefreshToken []byte + tokenExpiry sql.NullInt64 + tokenUserGUID sql.NullString + ) + err = rows.Scan(&cnsiGUID, &tokenGUID, &ciphertextAuthToken, &ciphertextRefreshToken, &tokenExpiry, &tokenUserGUID) + if err != nil { + return nil, fmt.Errorf("Unable to scan CNSI records: %v", err) + } + + log.Debug("Decrypting Auth Token") + plaintextAuthToken, err := crypto.DecryptToken(encryptionKey, ciphertextAuthToken) + if err != nil { + return make([]api.BackupTokenRecord, 0), err + } + + log.Debug("Decrypting Refresh Token") + plaintextRefreshToken, err := crypto.DecryptToken(encryptionKey, ciphertextRefreshToken) + if err != nil { + return make([]api.BackupTokenRecord, 0), err + } + + // Build a new TokenRecord based on the decrypted tokens + tr := new(api.TokenRecord) + if tokenGUID.Valid { + tr.TokenGUID = tokenGUID.String + } + tr.AuthToken = plaintextAuthToken + tr.RefreshToken = plaintextRefreshToken + if tokenExpiry.Valid { + tr.TokenExpiry = tokenExpiry.Int64 + } + if tokenUserGUID.Valid { + tr.SystemShared = tokenUserGUID.String == SystemSharedUserGuid + } + + btr := new(api.BackupTokenRecord) + btr.TokenRecord = *tr + if tokenUserGUID.Valid { + btr.UserGUID = tokenUserGUID.String + } + if cnsiGUID.Valid { + btr.EndpointGUID = cnsiGUID.String + } + + btrs = append(btrs, *btr) + } + + return btrs, nil +} + func (p *PgsqlTokenRepository) FindCNSIToken(cnsiGUID string, userGUID string, encryptionKey []byte) (api.TokenRecord, error) { log.Debug("FindCNSIToken") return p.findCNSIToken(cnsiGUID, userGUID, encryptionKey, false) diff --git a/src/jetstream/repository/tokens/pgsql_tokens_test.go b/src/jetstream/repository/tokens/pgsql_tokens_test.go index 0975734ebe..a9d4731448 100644 --- a/src/jetstream/repository/tokens/pgsql_tokens_test.go +++ b/src/jetstream/repository/tokens/pgsql_tokens_test.go @@ -313,6 +313,31 @@ func TestFindUAATokens(t *testing.T) { }) } + +func TestListAllEnabledConnectedCNSITokens(t *testing.T) { + Convey("ListAllEnabledConnectedCNSITokens Tests", t, func() { + db, mock, repository := initialiseRepo(t) + + Convey("should return only cnsi tokens that are connected and enabled", func() { + rs := sqlmock.NewRows([]string{"cnsi_guid", "token_guid", "auth_token", "refresh_token", "token_expiry", "user_guid"}). + AddRow(mockCNSIGuid, mockTokenGUID, mockCNSIToken, mockCNSIToken, mockTokenExpiry, mockUserGuid) + + mock.ExpectQuery(listAllEnabledConnectedCNSITokens).WillReturnRows(rs) + + tokens, err := repository.ListAllEnabledConnectedCNSITokens(mockEncryptionKey) + log.Print("listAllEnabledConnectedCNSITokens returned ", err) + + So(err, ShouldBeNil) + So(len(tokens), ShouldEqual, 1) + So(mock.ExpectationsWereMet(), ShouldBeNil) + }) + + Reset(func() { + db.Close() + }) + }) +} + func TestFindCNSITokens(t *testing.T) { Convey("FindAuthToken Tests", t, func() {