From aee99ee3bd042c44080cdfe4ed189634058c612a Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Fri, 10 Aug 2018 14:00:03 +0100 Subject: [PATCH 01/20] Remove all tokens associated with a cnsi on unregister, also fix e2e - e2e tests were failing after endpoint guid --> url hash change - when an endpoint was disconnected only the tokens associated with itself and the requesting user were deleted - previously this wasn't an issue, as registering the same endpoint resulted in a different guid and any tokens left in the db were not used - with the guid --> url hash change re-registering the same endpoint results in the old tokens being used - this broke e2e tests that expected every user to have to connect after all endpoints were unregistered - not sure if there's any clean up of the tokens table over time, so for that reason and security we now remove all tokens associated with an unregistered endpoint - other e2e fixes make tests more robust --- src/backend/app-core/cnsi.go | 33 ++++++++++---- .../repository/tokens/pgsql_tokens.go | 24 +++++++++- .../repository/tokens/pgsql_tokens_test.go | 44 ++++++++++++++++++- .../app-core/repository/tokens/tokens.go | 1 + .../application-deploy-e2e.spec.ts | 8 ++-- src/test-e2e/e2e.ts | 2 +- .../endpoints/endpoints-connect-e2e.spec.ts | 6 ++- src/test-e2e/po/page-header.po.ts | 4 ++ src/test-e2e/po/snackbar.po.ts | 12 ++++- 9 files changed, 114 insertions(+), 20 deletions(-) diff --git a/src/backend/app-core/cnsi.go b/src/backend/app-core/cnsi.go index 34674f2f46..095e2b5116 100644 --- a/src/backend/app-core/cnsi.go +++ b/src/backend/app-core/cnsi.go @@ -12,11 +12,12 @@ import ( log "github.com/Sirupsen/logrus" "github.com/labstack/echo" + "crypto/sha1" + "encoding/base64" + "github.com/SUSE/stratos-ui/repository/cnsis" "github.com/SUSE/stratos-ui/repository/interfaces" "github.com/SUSE/stratos-ui/repository/tokens" - "crypto/sha1" - "encoding/base64" ) const dbReferenceError = "Unable to establish a database reference: '%v'" @@ -47,7 +48,7 @@ func (p *portalProxy) RegisterEndpoint(c echo.Context, fetchInfo interfaces.Info skipSSLValidation = false } - cnsiClientId := c.FormValue("cnsi_client_id") + cnsiClientId := c.FormValue("cnsi_client_id") cnsiClientSecret := c.FormValue("cnsi_client_secret") if cnsiClientId == "" { @@ -143,12 +144,7 @@ func (p *portalProxy) unregisterCluster(c echo.Context) error { p.unsetCNSIRecord(cnsiGUID) - userID, err := p.GetSessionStringValue(c, "user_id") - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, "Could not find correct session value") - } - - p.unsetCNSITokenRecord(cnsiGUID, userID) + p.unsetCNSITokenRecords(cnsiGUID) return nil } @@ -420,3 +416,22 @@ func (p *portalProxy) unsetCNSITokenRecord(cnsiGUID string, userGUID string) err return nil } + +func (p *portalProxy) unsetCNSITokenRecords(cnsiGUID string) error { + log.Debug("unsetCNSITokenRecord") + tokenRepo, err := tokens.NewPgsqlTokenRepository(p.DatabaseConnectionPool) + if err != nil { + msg := "Unable to establish a database reference: '%v'" + log.Errorf(msg, err) + return fmt.Errorf(msg, err) + } + + err = tokenRepo.DeleteCNSITokens(cnsiGUID) + if err != nil { + msg := "Unable to delete a CNSI Token: %v" + log.Errorf(msg, err) + return fmt.Errorf(msg, err) + } + + return nil +} diff --git a/src/backend/app-core/repository/tokens/pgsql_tokens.go b/src/backend/app-core/repository/tokens/pgsql_tokens.go index 4dbb2bf217..25de1b3b0d 100644 --- a/src/backend/app-core/repository/tokens/pgsql_tokens.go +++ b/src/backend/app-core/repository/tokens/pgsql_tokens.go @@ -44,9 +44,10 @@ var insertCNSIToken = `INSERT INTO tokens (cnsi_guid, user_guid, token_type, aut var updateCNSIToken = `UPDATE tokens SET auth_token = $1, refresh_token = $2, token_expiry = $3, disconnected = $4, meta_data = $5 WHERE cnsi_guid = $6 AND user_guid = $7 AND token_type = $8 AND auth_type = $9` - var deleteCNSIToken = `DELETE FROM tokens - WHERE token_type = 'cnsi' AND cnsi_guid = $1 AND user_guid = $2` + WHERE token_type = 'cnsi' AND cnsi_guid = $1 AND user_guid = $2` +var deleteCNSITokens = `DELETE FROM tokens + WHERE token_type = 'cnsi' AND cnsi_guid = $1` // TODO (wchrisjohnson) We need to adjust several calls ^ to accept a list of items (guids) as input @@ -74,6 +75,7 @@ func InitRepositoryProvider(databaseProvider string) { insertCNSIToken = datastore.ModifySQLStatement(insertCNSIToken, databaseProvider) updateCNSIToken = datastore.ModifySQLStatement(updateCNSIToken, databaseProvider) deleteCNSIToken = datastore.ModifySQLStatement(deleteCNSIToken, databaseProvider) + deleteCNSITokens = datastore.ModifySQLStatement(deleteCNSITokens, databaseProvider) } // saveAuthToken - Save the Auth token to the datastore @@ -387,3 +389,21 @@ func (p *PgsqlTokenRepository) DeleteCNSIToken(cnsiGUID string, userGUID string) return nil } + +func (p *PgsqlTokenRepository) DeleteCNSITokens(cnsiGUID string) error { + log.Debug("DeleteCNSITokens") + if cnsiGUID == "" { + msg := "Unable to delete CNSI Token without a valid CNSI GUID." + log.Debug(msg) + return errors.New(msg) + } + + _, err := p.db.Exec(deleteCNSITokens, cnsiGUID) + if err != nil { + msg := "Unable to Delete CNSI token: %v" + log.Debugf(msg, err) + return fmt.Errorf(msg, err) + } + + return nil +} diff --git a/src/backend/app-core/repository/tokens/pgsql_tokens_test.go b/src/backend/app-core/repository/tokens/pgsql_tokens_test.go index bf777d3b43..7dfc08170b 100644 --- a/src/backend/app-core/repository/tokens/pgsql_tokens_test.go +++ b/src/backend/app-core/repository/tokens/pgsql_tokens_test.go @@ -392,9 +392,9 @@ func TestDeleteCNSIToken(t *testing.T) { So(err, ShouldNotBeNil) }) - Convey("should through exception when encoutring DB error", func() { + Convey("should throw exception when encountering DB error", func() { mock.ExpectExec(deleteFromTokensSql). - WillReturnError(errors.New("doesnt exist")) + WillReturnError(errors.New("doesn't exist")) err := repository.DeleteCNSIToken(mockCNSIToken, mockUserGuid) So(err, ShouldNotBeNil) @@ -419,3 +419,43 @@ func TestDeleteCNSIToken(t *testing.T) { }) } + +func TestDeleteCNSITokens(t *testing.T) { + + Convey("DeleteCNSITokens Tests", t, func() { + + db, mock, repository := initialiseRepo(t) + + Convey("should fail to delete token with an invalid CNSI GUID", func() { + var cnsiGuid string = "" + err := repository.DeleteCNSITokens(cnsiGuid) + So(err, ShouldNotBeNil) + }) + + Convey("should throw exception when encountering DB error", func() { + mock.ExpectExec(deleteFromTokensSql). + WillReturnError(errors.New("doesn't exist")) + err := repository.DeleteCNSITokens(mockCNSIToken) + + So(err, ShouldNotBeNil) + So(mock.ExpectationsWereMet(), ShouldBeNil) + + }) + + Convey("Test successful path", func() { + mock.ExpectExec(deleteFromTokensSql). + WillReturnResult(sqlmock.NewResult(1, 1)) + err := repository.DeleteCNSITokens(mockCNSIToken) + + So(err, ShouldBeNil) + So(mock.ExpectationsWereMet(), ShouldBeNil) + + }) + + Reset(func() { + db.Close() + }) + + }) + +} diff --git a/src/backend/app-core/repository/tokens/tokens.go b/src/backend/app-core/repository/tokens/tokens.go index 0d97161d29..5327697028 100644 --- a/src/backend/app-core/repository/tokens/tokens.go +++ b/src/backend/app-core/repository/tokens/tokens.go @@ -19,5 +19,6 @@ type Repository interface { FindCNSIToken(cnsiGUID string, userGUID string, encryptionKey []byte) (interfaces.TokenRecord, error) FindCNSITokenIncludeDisconnected(cnsiGUID string, userGUID string, encryptionKey []byte) (interfaces.TokenRecord, error) DeleteCNSIToken(cnsiGUID string, userGUID string) error + DeleteCNSITokens(cnsiGUID string) error SaveCNSIToken(cnsiGUID string, userGUID string, tokenRecord interfaces.TokenRecord, encryptionKey []byte) error } diff --git a/src/test-e2e/application/application-deploy-e2e.spec.ts b/src/test-e2e/application/application-deploy-e2e.spec.ts index e65a031dd0..2658a4cbd0 100644 --- a/src/test-e2e/application/application-deploy-e2e.spec.ts +++ b/src/test-e2e/application/application-deploy-e2e.spec.ts @@ -1,12 +1,12 @@ +import { browser } from 'protractor'; + import { ApplicationsPage } from '../applications/applications.po'; import { e2e } from '../e2e'; +import { CFHelpers } from '../helpers/cf-helpers'; import { ConsoleUserType } from '../helpers/e2e-helpers'; import { SideNavigation, SideNavMenuItem } from '../po/side-nav.po'; import { ApplicationE2eHelper } from './application-e2e-helpers'; import { ApplicationSummary } from './application-summary.po'; -import { CreateApplicationStepper } from './create-application-stepper.po'; -import { CFHelpers } from '../helpers/cf-helpers'; -import { ExpectedConditions, browser } from 'protractor'; let nav: SideNavigation; let appWall: ApplicationsPage; @@ -99,7 +99,7 @@ describe('Application Deploy', function () { // Should be app summary ApplicationSummary.detect().then(appSummary => { appSummary.waitForPage(); - expect(appSummary.getAppName()).toBe('cf-quick-app'); + appSummary.header.waitForTitleText('cf-quick-app'); applicationE2eHelper.cfHelper.deleteApp(appSummary.cfGuid, appSummary.appGuid); }); }); diff --git a/src/test-e2e/e2e.ts b/src/test-e2e/e2e.ts index c528e1de66..781c329756 100644 --- a/src/test-e2e/e2e.ts +++ b/src/test-e2e/e2e.ts @@ -80,7 +80,7 @@ export class E2ESetup { // Adds the setup flow to the browser chain - this will run after all of the setup ops const that = this; protractor.promise.controlFlow().execute(() => { - E2E.debugLog('Logging in as user: ' + (userType === ConsoleUserType.admin ? 'admin' : 'user')); + E2E.debugLog('Logging in as user: ' + (that.loginUserType === ConsoleUserType.admin ? 'admin' : 'user')); return e2e.helper.setupApp(that.loginUserType); }); } diff --git a/src/test-e2e/endpoints/endpoints-connect-e2e.spec.ts b/src/test-e2e/endpoints/endpoints-connect-e2e.spec.ts index 297bf72aa7..71a1282998 100644 --- a/src/test-e2e/endpoints/endpoints-connect-e2e.spec.ts +++ b/src/test-e2e/endpoints/endpoints-connect-e2e.spec.ts @@ -1,3 +1,5 @@ +import { browser } from 'protractor'; + import { ApplicationsPage } from '../applications/applications.po'; import { CloudFoundryPage } from '../cloud-foundry/cloud-foundry.po'; import { e2e } from '../e2e'; @@ -20,6 +22,8 @@ describe('Endpoints', () => { describe('Connect/Disconnect endpoints -', () => { beforeAll(() => { + // Ran independently these tests are fine. However stacked with other spec files they'll fail without a pause + browser.sleep(1000); e2e.setup(ConsoleUserType.user) .clearAllEndpoints() .registerDefaultCloudFoundry(); @@ -33,7 +37,7 @@ describe('Endpoints', () => { expect(endpointsPage.isActivePage()).toBeTruthy(); // Close the snack bar telling us that there are no connected endpoints - connectDialog.snackBar.close(); + connectDialog.snackBar.safeClose(); // Get the row in the table for this endpoint endpointsPage.table.getRowForEndpoint(toConnect.name).then(row => { diff --git a/src/test-e2e/po/page-header.po.ts b/src/test-e2e/po/page-header.po.ts index 8fc87ebca8..af47f75105 100644 --- a/src/test-e2e/po/page-header.po.ts +++ b/src/test-e2e/po/page-header.po.ts @@ -45,6 +45,10 @@ export class PageHeader extends Component { return this.getTitle().getText(); } + waitForTitleText(text: string) { + browser.wait(this.until.textToBePresentInElement(this.getTitle(), text), 10000); + } + logout(): promise.Promise { return this.clickIconButton('more_vert').then(() => { browser.driver.sleep(2000); diff --git a/src/test-e2e/po/snackbar.po.ts b/src/test-e2e/po/snackbar.po.ts index 48491a7721..61a8bd7c6e 100644 --- a/src/test-e2e/po/snackbar.po.ts +++ b/src/test-e2e/po/snackbar.po.ts @@ -11,8 +11,18 @@ export class SnackBarComponent extends Component { super(element(by.css('.mat-simple-snackbar'))); } + private getButton(): ElementFinder { + return this.locator.element(by.tagName('button')); + } + close(): promise.Promise { - return this.locator.element(by.tagName('button')).click(); + return this.getButton().click(); + } + + safeClose(): promise.Promise { + return this.getButton().isPresent().then(isPresent => { + return isPresent ? this.getButton().click() : null; + }); } getButtonText(): promise.Promise { From 9413c98649c0d8cd6e847b7e4a4eba9423738d7c Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Mon, 13 Aug 2018 11:01:01 +0100 Subject: [PATCH 02/20] Remove sleep - Needed before, ran several times now without and not needed --- src/test-e2e/endpoints/endpoints-connect-e2e.spec.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/test-e2e/endpoints/endpoints-connect-e2e.spec.ts b/src/test-e2e/endpoints/endpoints-connect-e2e.spec.ts index 71a1282998..d4674b9730 100644 --- a/src/test-e2e/endpoints/endpoints-connect-e2e.spec.ts +++ b/src/test-e2e/endpoints/endpoints-connect-e2e.spec.ts @@ -22,8 +22,6 @@ describe('Endpoints', () => { describe('Connect/Disconnect endpoints -', () => { beforeAll(() => { - // Ran independently these tests are fine. However stacked with other spec files they'll fail without a pause - browser.sleep(1000); e2e.setup(ConsoleUserType.user) .clearAllEndpoints() .registerDefaultCloudFoundry(); From 0c5416d52c661127e3ac454823a7924343cbd654 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Mon, 13 Aug 2018 12:08:25 +0100 Subject: [PATCH 03/20] Remove stale tokens from Tokens table - Stale = type=cnsi and no corresponding endpoint --- .../20180813110300_RemoveStaleTokens.go | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/backend/app-core/datastore/20180813110300_RemoveStaleTokens.go diff --git a/src/backend/app-core/datastore/20180813110300_RemoveStaleTokens.go b/src/backend/app-core/datastore/20180813110300_RemoveStaleTokens.go new file mode 100644 index 0000000000..1cf5e372fd --- /dev/null +++ b/src/backend/app-core/datastore/20180813110300_RemoveStaleTokens.go @@ -0,0 +1,20 @@ +package datastore + +import ( + "database/sql" + + "bitbucket.org/liamstask/goose/lib/goose" +) + +func init() { + RegisterMigration(20180813110300, "RemoveStaleTokens", func(txn *sql.Tx, conf *goose.DBConf) error { + + removeStaleTokens := "DELETE t FROM tokens t LEFT JOIN cnsis c ON c.guid=t.cnsi_guid WHERE c.guid IS NULL AND t.token_type='cnsi';" + _, err := txn.Exec(removeStaleTokens) + if err != nil { + return err + } + + return nil + }) +} From a9f6ae0cd7ef310e537bc2a665086a873c5c803f Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Mon, 13 Aug 2018 15:10:33 +0100 Subject: [PATCH 04/20] Make deploy step agnostic of e2e user's other roles - if user can only see one cf, org and space the create app cf/org/space selections will be autopopulated - this means the `next` but is enabled on enter --- .../application/application-deploy-e2e.spec.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/test-e2e/application/application-deploy-e2e.spec.ts b/src/test-e2e/application/application-deploy-e2e.spec.ts index 2658a4cbd0..f01ea8df33 100644 --- a/src/test-e2e/application/application-deploy-e2e.spec.ts +++ b/src/test-e2e/application/application-deploy-e2e.spec.ts @@ -1,4 +1,4 @@ -import { browser } from 'protractor'; +import { browser, promise } from 'protractor'; import { ApplicationsPage } from '../applications/applications.po'; import { e2e } from '../e2e'; @@ -13,6 +13,7 @@ let appWall: ApplicationsPage; let applicationE2eHelper: ApplicationE2eHelper; let cfHelper: CFHelpers; +const cfName = e2e.secrets.getDefaultCFEndpoint().name; const orgName = e2e.secrets.getDefaultCFEndpoint().testOrg; const spaceName = e2e.secrets.getDefaultCFEndpoint().testSpace; @@ -55,9 +56,20 @@ describe('Application Deploy', function () { expect(steps[3]).toBe('Deploy'); }); expect(deployApp.stepper.getActiveStepName()).toBe('Cloud Foundry'); - expect(deployApp.stepper.canNext()).toBeFalsy(); + promise.all([ + deployApp.stepper.getStepperForm().getText('cf'), + deployApp.stepper.getStepperForm().getText('org'), + deployApp.stepper.getStepperForm().getText('space') + ]).then(([cf, org, space]) => { + if (cf !== 'Cloud Foundry' && org !== 'Organization' && space !== 'Space') { + expect(deployApp.stepper.canNext()).toBeTruthy(); + } else { + expect(deployApp.stepper.canNext()).toBeFalsy(); + } + }); // Fill in form + deployApp.stepper.getStepperForm().fill({ 'cf': cfName }); deployApp.stepper.getStepperForm().fill({ 'org': orgName }); deployApp.stepper.getStepperForm().fill({ 'space': spaceName }); expect(deployApp.stepper.canNext()).toBeTruthy(); From f65fa1891de8432b298a887af055a704584bd732 Mon Sep 17 00:00:00 2001 From: Neil MacDougall Date: Mon, 13 Aug 2018 17:26:08 +0100 Subject: [PATCH 05/20] Output backend log if tests fail --- deploy/ci/travis/run-e2e-tests.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/deploy/ci/travis/run-e2e-tests.sh b/deploy/ci/travis/run-e2e-tests.sh index 35c236508f..d1fa6cb2fc 100644 --- a/deploy/ci/travis/run-e2e-tests.sh +++ b/deploy/ci/travis/run-e2e-tests.sh @@ -87,6 +87,13 @@ if [ "${TRAVIS_EVENT_TYPE}" != "pull_request" ]; then popd fi +# Output backend log if the tests failed +if [ "${RUN_TYPE}" == "quick" ]; then + if [ $RESULT -ne 0 ]; then + cat outputs/backend.log + fi +fi + # Check environment variable that will ignore E2E failures if [ -n "${STRATOS_ALLOW_E2E_FAILURES}" ]; then echo "Ignoring E2E test failures (if any) because STRATOS_ALLOW_E2E_FAILURES is set" From b76a6a9f6457e6a3f81c28b3c9641e23c16c11f3 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Mon, 13 Aug 2018 18:01:35 +0100 Subject: [PATCH 06/20] Ensure SQL statement works in both SQLite and MariaDB --- .../app-core/datastore/20180813110300_RemoveStaleTokens.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/app-core/datastore/20180813110300_RemoveStaleTokens.go b/src/backend/app-core/datastore/20180813110300_RemoveStaleTokens.go index 1cf5e372fd..9aa31beb04 100644 --- a/src/backend/app-core/datastore/20180813110300_RemoveStaleTokens.go +++ b/src/backend/app-core/datastore/20180813110300_RemoveStaleTokens.go @@ -9,7 +9,7 @@ import ( func init() { RegisterMigration(20180813110300, "RemoveStaleTokens", func(txn *sql.Tx, conf *goose.DBConf) error { - removeStaleTokens := "DELETE t FROM tokens t LEFT JOIN cnsis c ON c.guid=t.cnsi_guid WHERE c.guid IS NULL AND t.token_type='cnsi';" + removeStaleTokens := "DELETE FROM tokens WHERE token_type='cnsi' AND cnsi_guid NOT IN (SELECT guid FROM cnsis);" _, err := txn.Exec(removeStaleTokens) if err != nil { return err From b08dbf0d63ca50a4b6a1cec0e355db97a0ecf6ac Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Tue, 14 Aug 2018 10:02:16 +0100 Subject: [PATCH 07/20] Add debugging to help solve app deploy failure --- src/test-e2e/application/application-delete-e2e.spec.ts | 4 ++-- src/test-e2e/application/application-deploy-e2e.spec.ts | 6 +++++- src/test-e2e/application/application-summary.po.ts | 2 ++ src/test-e2e/e2e.ts | 4 ++-- src/test-e2e/po/page-header.po.ts | 2 +- src/test-e2e/po/page.po.ts | 2 +- 6 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/test-e2e/application/application-delete-e2e.spec.ts b/src/test-e2e/application/application-delete-e2e.spec.ts index e869a048a4..e1a3368966 100644 --- a/src/test-e2e/application/application-delete-e2e.spec.ts +++ b/src/test-e2e/application/application-delete-e2e.spec.ts @@ -26,7 +26,7 @@ describe('Application Delete', function () { .registerDefaultCloudFoundry() .connectAllEndpoints(ConsoleUserType.user) .connectAllEndpoints(ConsoleUserType.admin) - .getInfo(ConsoleUserType.admin) + .getInfo(ConsoleUserType.admin); applicationE2eHelper = new ApplicationE2eHelper(setup); cfHelper = new CFHelpers(setup); }); @@ -116,7 +116,7 @@ describe('Application Delete', function () { app = null; // Check that we have 1 less app - appWall.appList.getTotalResults().then(count =>expect(count).toBe(appCount - 1)); + appWall.appList.getTotalResults().then(count => expect(count).toBe(appCount - 1)); }); }); diff --git a/src/test-e2e/application/application-deploy-e2e.spec.ts b/src/test-e2e/application/application-deploy-e2e.spec.ts index f01ea8df33..5e34b33f72 100644 --- a/src/test-e2e/application/application-deploy-e2e.spec.ts +++ b/src/test-e2e/application/application-deploy-e2e.spec.ts @@ -1,7 +1,7 @@ import { browser, promise } from 'protractor'; import { ApplicationsPage } from '../applications/applications.po'; -import { e2e } from '../e2e'; +import { e2e, E2E } from '../e2e'; import { CFHelpers } from '../helpers/cf-helpers'; import { ConsoleUserType } from '../helpers/e2e-helpers'; import { SideNavigation, SideNavMenuItem } from '../po/side-nav.po'; @@ -108,10 +108,14 @@ describe('Application Deploy', function () { // Click next deployApp.stepper.next(); + (new E2E()).log(`Debug: Should be arriving at app summary`); + // Should be app summary ApplicationSummary.detect().then(appSummary => { + (new E2E()).log(`Debug: Created app summary obj`); appSummary.waitForPage(); appSummary.header.waitForTitleText('cf-quick-app'); + (new E2E()).log(`Debug: Have title`); applicationE2eHelper.cfHelper.deleteApp(appSummary.cfGuid, appSummary.appGuid); }); }); diff --git a/src/test-e2e/application/application-summary.po.ts b/src/test-e2e/application/application-summary.po.ts index 6b59b4f988..3f817f2302 100644 --- a/src/test-e2e/application/application-summary.po.ts +++ b/src/test-e2e/application/application-summary.po.ts @@ -1,6 +1,7 @@ import { browser, promise } from 'protractor'; import { Page } from '../po/page.po'; import { DeleteApplication } from './delete-app.po'; +import { E2E } from '../e2e'; export class ApplicationSummary extends Page { @@ -20,6 +21,7 @@ export class ApplicationSummary extends Page { expect(urlParts[3]).toBe('summary'); const cfGuid = urlParts[1]; const appGuid = urlParts[2]; + (new E2E()).log(`Creating App Summary object using cfGuid: '${cfGuid}' and appGuid: '${appGuid}'`); return new ApplicationSummary(cfGuid, appGuid); }); } diff --git a/src/test-e2e/e2e.ts b/src/test-e2e/e2e.ts index 781c329756..de6d514133 100644 --- a/src/test-e2e/e2e.ts +++ b/src/test-e2e/e2e.ts @@ -38,14 +38,14 @@ export class E2E { /** * Convenience for sleep */ - sleep(duration) { + sleep(duration: number) { browser.driver.sleep(duration); } /** * Log message in the control flow */ - log(log) { + log(log: string) { protractor.promise.controlFlow().execute(() => console.log(log)); } } diff --git a/src/test-e2e/po/page-header.po.ts b/src/test-e2e/po/page-header.po.ts index af47f75105..817664cd92 100644 --- a/src/test-e2e/po/page-header.po.ts +++ b/src/test-e2e/po/page-header.po.ts @@ -46,7 +46,7 @@ export class PageHeader extends Component { } waitForTitleText(text: string) { - browser.wait(this.until.textToBePresentInElement(this.getTitle(), text), 10000); + browser.wait(this.until.textToBePresentInElement(this.getTitle(), text), 10000, `Failed to wait for page header with text ${text}`); } logout(): promise.Promise { diff --git a/src/test-e2e/po/page.po.ts b/src/test-e2e/po/page.po.ts index 36809e9b0f..265b1b4338 100644 --- a/src/test-e2e/po/page.po.ts +++ b/src/test-e2e/po/page.po.ts @@ -48,7 +48,7 @@ export abstract class Page { waitForPage() { expect(this.navLink.startsWith('/')).toBeTruthy(); - browser.wait(until.urlIs(this.getUrl()), 20000); + browser.wait(until.urlIs(this.getUrl()), 20000, `Failed to wait for page with navlink '${this.navLink}'`); } waitForPageDataLoaded() { From 8d6ffb30186c6c568aaeafd9c196750412de66e8 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Tue, 14 Aug 2018 14:44:55 +0100 Subject: [PATCH 08/20] Three bug fixes... - (test) the incorrect space was fetched if there were multiple with same name - (test) on delete app it's route would not be deleted due to the app entity having an empty routes array - Beefed up check for space in application service --- .../applications/application.service.ts | 19 ++++++-- .../application-create-e2e.spec.ts | 5 +- .../application-delete-e2e.spec.ts | 9 ++-- .../application/application-e2e-helpers.ts | 47 ++++++++++++------- src/test-e2e/helpers/cf-helpers.ts | 11 ++--- 5 files changed, 59 insertions(+), 32 deletions(-) diff --git a/src/frontend/app/features/applications/application.service.ts b/src/frontend/app/features/applications/application.service.ts index 39f46b00d7..6db3cbc076 100644 --- a/src/frontend/app/features/applications/application.service.ts +++ b/src/frontend/app/features/applications/application.service.ts @@ -20,6 +20,7 @@ import { GetAppSummaryAction, } from '../../store/actions/app-metadata.actions'; import { GetApplication, UpdateApplication, UpdateExistingApplication } from '../../store/actions/application.actions'; +import { GetSpace } from '../../store/actions/space.actions'; import { AppState } from '../../store/app-state'; import { appEnvVarsSchemaKey, @@ -32,6 +33,7 @@ import { routeSchemaKey, serviceBindingSchemaKey, spaceSchemaKey, + spaceWithOrgKey, stackSchemaKey, } from '../../store/helpers/entity-factory'; import { createEntityRelationKey } from '../../store/helpers/entity-relations/entity-relations.types'; @@ -53,8 +55,6 @@ import { import { getRoute, isTCPRoute } from './routes/routes.helper'; - - export function createGetApplicationAction(guid: string, endpointGuid: string) { return new GetApplication( guid, @@ -184,7 +184,17 @@ export class ApplicationService { map(entityInfo => entityInfo.entity.entity), ); this.appSpace$ = moreWaiting$.pipe( first(), - switchMap(app => this.store.select(selectEntity(spaceSchemaKey, app.space_guid))), ); + switchMap(app => { + return this.entityServiceFactory.create>( + spaceSchemaKey, + entityFactory(spaceWithOrgKey), + app.space_guid, + new GetSpace(app.guid, app.cfGuid, [createEntityRelationKey(spaceSchemaKey, organizationSchemaKey)], true) + ).waitForEntity$.pipe( + map(entityInfo => entityInfo.entity) + ); + }) + ); this.appOrg$ = moreWaiting$.pipe( first(), switchMap(app => this.appSpace$.pipe( @@ -193,7 +203,8 @@ export class ApplicationService { return this.store.select(selectEntity(organizationSchemaKey, orgGuid)); }), filter(org => !!org) - )), ); + )) + ); this.isDeletingApp$ = this.appEntityService.isDeletingEntity$.pipe(publishReplay(1), refCount(), ); diff --git a/src/test-e2e/application/application-create-e2e.spec.ts b/src/test-e2e/application/application-create-e2e.spec.ts index 9acbe1d47f..8eb7452247 100644 --- a/src/test-e2e/application/application-create-e2e.spec.ts +++ b/src/test-e2e/application/application-create-e2e.spec.ts @@ -7,7 +7,7 @@ import { ApplicationSummary } from './application-summary.po'; import { CreateApplicationStepper } from './create-application-stepper.po'; -describe('Application Create', function () { +fdescribe('Application Create', function () { let nav: SideNavigation; let appWall: ApplicationsPage; @@ -69,8 +69,7 @@ describe('Application Create', function () { createAppStepper.waitUntilNotShown(); // Determine the app guid and confirm we're on the app summary page - const getCfCnsi = applicationE2eHelper.cfRequestHelper.getCfCnsi(); - const fetchApp = getCfCnsi.then(endpointModel => { + const fetchApp = applicationE2eHelper.cfRequestHelper.getCfCnsi().then(endpointModel => { cfGuid = endpointModel.guid; return applicationE2eHelper.fetchApp(cfGuid, testAppName); }); diff --git a/src/test-e2e/application/application-delete-e2e.spec.ts b/src/test-e2e/application/application-delete-e2e.spec.ts index e1a3368966..025dbc4d42 100644 --- a/src/test-e2e/application/application-delete-e2e.spec.ts +++ b/src/test-e2e/application/application-delete-e2e.spec.ts @@ -40,9 +40,12 @@ describe('Application Delete', function () { cfGuid = e2e.helper.getEndpointGuid(e2e.info, endpointName); const testTime = (new Date()).toISOString(); testAppName = ApplicationE2eHelper.createApplicationName(testTime); - return applicationE2eHelper.createApp(cfGuid, e2e.secrets.getDefaultCFEndpoint().testSpace, testAppName).then(appl => { - app = appl; - }); + return applicationE2eHelper.createApp( + cfGuid, + e2e.secrets.getDefaultCFEndpoint().testOrg, + e2e.secrets.getDefaultCFEndpoint().testSpace, + testAppName + ).then(appl => app = appl); }); afterAll(() => applicationE2eHelper.deleteApplication(cfGuid, app)); diff --git a/src/test-e2e/application/application-e2e-helpers.ts b/src/test-e2e/application/application-e2e-helpers.ts index a101fb6d94..ee4531d394 100644 --- a/src/test-e2e/application/application-e2e-helpers.ts +++ b/src/test-e2e/application/application-e2e-helpers.ts @@ -1,10 +1,11 @@ +import { browser, promise } from 'protractor'; + +import { IApp, IRoute } from '../../frontend/app/core/cf-api.types'; import { APIResource, CFResponse } from '../../frontend/app/store/types/api.types'; import { E2ESetup } from '../e2e'; +import { CFHelpers } from '../helpers/cf-helpers'; import { CFRequestHelpers } from '../helpers/cf-request-helpers'; import { E2EHelpers } from '../helpers/e2e-helpers'; -import { promise } from 'protractor'; -import { CFHelpers } from '../helpers/cf-helpers'; -import { browser } from 'protractor'; const customAppLabel = E2EHelpers.e2eItemPrefix + (process.env.CUSTOM_APP_LABEL || process.env.USER); @@ -33,7 +34,7 @@ export class ApplicationE2eHelper { ); } - deleteApplication = (cfGuid: string, app: APIResource): promise.Promise => { + deleteApplication = (cfGuid: string, app: APIResource): promise.Promise => { if (!cfGuid || !app) { return promise.fullyResolved({}); } @@ -48,10 +49,16 @@ export class ApplicationE2eHelper { }); // Delete route - const routes = app.entity.routes || []; - routes.forEach(route => { - promises.push(this.cfRequestHelper.sendCfDelete(cfGuid, 'routes/' + route.metadata.guid + '?q=recursive=true&async=false')); - }); + const routes: promise.Promise[]> = app.entity.routes && app.entity.routes.length ? + promise.fullyResolved(app.entity.routes) : + this.cfRequestHelper.sendCfGet(cfGuid, `apps/${app.metadata.guid}/routes`).then(res => res.resources); + + promises.push(routes.then(appRoutes => { + const routePromises = []; + return promise.all(appRoutes.map(route => + this.cfRequestHelper.sendCfDelete(cfGuid, 'routes/' + route.metadata.guid + '?q=recursive=true&async=false')) + ); + })); const deps = promise.all(promises).catch(err => { const errorString = `Failed to delete routes or services attached to an app`; @@ -68,17 +75,25 @@ export class ApplicationE2eHelper { }).catch(err => fail(`Failed to delete app or associated dependencies: ${err}`)); } - createApp(cfGuid: string, spaceName: string, appName: string) { - return browser.driver.wait(this.cfHelper.fetchSpace(cfGuid, spaceName).then(space => { - expect(space).not.toBeNull(); - return this.cfHelper.createApp(cfGuid, space.metadata.guid, appName).then(() => { - return this.fetchApp(cfGuid, appName).then(apps => { + createApp(cfGuid: string, orgName: string, spaceName: string, appName: string) { + return browser.driver.wait( + this.cfHelper.addOrgIfMissing(cfGuid, orgName) + .then(org => { + return this.cfHelper.fetchSpace(cfGuid, org.resources[0].metadata.guid, spaceName); + }) + .then(space => { + expect(space).not.toBeNull(); + return this.cfHelper.createApp(cfGuid, space.metadata.guid, appName); + }) + .then(() => { + return this.fetchApp(cfGuid, appName); + }) + .then(apps => { expect(apps.total_results).toBe(1); const app = apps.resources[0]; return app; - }); - }); - })); + }) + ); } } diff --git a/src/test-e2e/helpers/cf-helpers.ts b/src/test-e2e/helpers/cf-helpers.ts index da8f853313..971e6f8fd6 100644 --- a/src/test-e2e/helpers/cf-helpers.ts +++ b/src/test-e2e/helpers/cf-helpers.ts @@ -1,7 +1,6 @@ -import { CFRequestHelpers } from './cf-request-helpers'; import { E2ESetup } from '../e2e'; -import { promise } from 'protractor'; import { E2EConfigCloudFoundry } from '../e2e.types'; +import { CFRequestHelpers } from './cf-request-helpers'; export class CFHelpers { cfRequestHelper: CFRequestHelpers; @@ -25,7 +24,7 @@ export class CFHelpers { return users.find(user => user && user.entity && user.entity.username === name); } - addOrgIfMissing(cnsiGuid, orgName, adminGuid, userGuid) { + addOrgIfMissing(cnsiGuid, orgName, adminGuid?: string, userGuid?: string) { let added; return this.cfRequestHelper.sendCfGet(cnsiGuid, 'organizations?q=name IN ' + orgName).then(json => { if (json.total_results === 0) { @@ -34,7 +33,7 @@ export class CFHelpers { } return json; }).then(newOrg => { - if (!added) { + if (!added || !adminGuid || !userGuid) { // No need to mess around with permissions, it exists already. return newOrg; } @@ -122,8 +121,8 @@ export class CFHelpers { }); } - fetchSpace(cnsiGuid: string, spaceName: string) { - return this.cfRequestHelper.sendCfGet(cnsiGuid, 'spaces?q=name IN ' + spaceName).then(json => { + fetchSpace(cnsiGuid: string, orgGuid: string, spaceName: string) { + return this.cfRequestHelper.sendCfGet(cnsiGuid, 'spaces?q=name IN ' + spaceName + '&organization_guid=' + orgGuid).then(json => { if (json.total_results > 0) { const space = json.resources[0]; return space; From e84a7915fd50e7348565605db56074ee1174a3ea Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Tue, 14 Aug 2018 17:50:50 +0100 Subject: [PATCH 09/20] WIP - Refactor fetchSpace, fetchApp and deleteApp, ensure all work within a specific space - Refactor delete app, ensure it works efficiently in all scenarios - Still an issue - CF is not reporting an app's routes promtly - TODO: console.logs, remove sleep if possible, fdescribes --- .../application-create-e2e.spec.ts | 24 +-- .../application-delete-e2e.spec.ts | 9 +- .../application-deploy-e2e.spec.ts | 22 ++- .../application/application-e2e-helpers.ts | 173 ++++++++++++------ src/test-e2e/helpers/cf-helpers.ts | 55 +++--- src/test-e2e/helpers/cf-request-helpers.ts | 10 +- src/test-e2e/helpers/secrets-helpers.ts | 2 +- 7 files changed, 193 insertions(+), 102 deletions(-) diff --git a/src/test-e2e/application/application-create-e2e.spec.ts b/src/test-e2e/application/application-create-e2e.spec.ts index 8eb7452247..7c71445f46 100644 --- a/src/test-e2e/application/application-create-e2e.spec.ts +++ b/src/test-e2e/application/application-create-e2e.spec.ts @@ -1,3 +1,5 @@ +import { IApp } from '../../frontend/app/core/cf-api.types'; +import { APIResource } from '../../frontend/app/store/types/api.types'; import { ApplicationsPage } from '../applications/applications.po'; import { e2e } from '../e2e'; import { ConsoleUserType } from '../helpers/e2e-helpers'; @@ -6,13 +8,12 @@ import { ApplicationE2eHelper } from './application-e2e-helpers'; import { ApplicationSummary } from './application-summary.po'; import { CreateApplicationStepper } from './create-application-stepper.po'; - fdescribe('Application Create', function () { let nav: SideNavigation; let appWall: ApplicationsPage; let applicationE2eHelper: ApplicationE2eHelper; - let cfGuid, app; + let cfGuid, app: APIResource; beforeAll(() => { nav = new SideNavigation(); @@ -21,7 +22,8 @@ fdescribe('Application Create', function () { .clearAllEndpoints() .registerDefaultCloudFoundry() .connectAllEndpoints(ConsoleUserType.user) - .connectAllEndpoints(ConsoleUserType.admin); + .connectAllEndpoints(ConsoleUserType.admin) + .getInfo(); applicationE2eHelper = new ApplicationE2eHelper(setup); }); @@ -69,18 +71,16 @@ fdescribe('Application Create', function () { createAppStepper.waitUntilNotShown(); // Determine the app guid and confirm we're on the app summary page - const fetchApp = applicationE2eHelper.cfRequestHelper.getCfCnsi().then(endpointModel => { - cfGuid = endpointModel.guid; - return applicationE2eHelper.fetchApp(cfGuid, testAppName); - }); - const appFetched = fetchApp.then(response => { - expect(response.total_results).toBe(1); - app = response.resources[0]; - const appSummaryPage = new ApplicationSummary(cfGuid, app.metadata.guid, app.entity.name); + applicationE2eHelper.fetchAppInDefault(testAppName).then((res) => { + expect(res.app).not.toBe(null); + app = res.app; + cfGuid = res.cfGuid; + const appSummaryPage = new ApplicationSummary(res.cfGuid, app.metadata.guid, app.entity.name); appSummaryPage.waitForPage(); }); + }); - afterEach(() => applicationE2eHelper.deleteApplication(cfGuid, app)); + afterEach(() => app ? applicationE2eHelper.deleteApplication({ cfGuid, app }) : null); }); diff --git a/src/test-e2e/application/application-delete-e2e.spec.ts b/src/test-e2e/application/application-delete-e2e.spec.ts index 025dbc4d42..3220f0c7ed 100644 --- a/src/test-e2e/application/application-delete-e2e.spec.ts +++ b/src/test-e2e/application/application-delete-e2e.spec.ts @@ -9,7 +9,7 @@ import { CFHelpers } from '../helpers/cf-helpers'; import { ExpectedConditions } from 'protractor'; -describe('Application Delete', function () { +fdescribe('Application Delete', function () { let nav: SideNavigation; let appWall: ApplicationsPage; @@ -48,7 +48,11 @@ describe('Application Delete', function () { ).then(appl => app = appl); }); - afterAll(() => applicationE2eHelper.deleteApplication(cfGuid, app)); + afterAll(() => { + if (app) { + applicationE2eHelper.deleteApplication({ cfGuid, app }); + } + }); it('Should return to summary page after cancel', () => { const appSummaryPage = new ApplicationSummary(cfGuid, app.metadata.guid, app.entity.name); @@ -123,5 +127,4 @@ describe('Application Delete', function () { }); }); - }); diff --git a/src/test-e2e/application/application-deploy-e2e.spec.ts b/src/test-e2e/application/application-deploy-e2e.spec.ts index 5e34b33f72..41baf64f2e 100644 --- a/src/test-e2e/application/application-deploy-e2e.spec.ts +++ b/src/test-e2e/application/application-deploy-e2e.spec.ts @@ -17,7 +17,9 @@ const cfName = e2e.secrets.getDefaultCFEndpoint().name; const orgName = e2e.secrets.getDefaultCFEndpoint().testOrg; const spaceName = e2e.secrets.getDefaultCFEndpoint().testSpace; -describe('Application Deploy', function () { +const appName = 'cf-quick-app'; + +fdescribe('Application Deploy', function () { const testApp = e2e.secrets.getDefaultCFEndpoint().testDeployApp || 'nwmac/cf-quick-app'; @@ -111,17 +113,17 @@ describe('Application Deploy', function () { (new E2E()).log(`Debug: Should be arriving at app summary`); // Should be app summary - ApplicationSummary.detect().then(appSummary => { - (new E2E()).log(`Debug: Created app summary obj`); - appSummary.waitForPage(); - appSummary.header.waitForTitleText('cf-quick-app'); - (new E2E()).log(`Debug: Have title`); - applicationE2eHelper.cfHelper.deleteApp(appSummary.cfGuid, appSummary.appGuid); - }); + browser.wait(ApplicationSummary.detect() + .then(appSummary => { + (new E2E()).log(`Debug: Created app summary obj`); + appSummary.waitForPage(); + appSummary.header.waitForTitleText(appName); + (new E2E()).log(`Debug: Have title`); + return appSummary.cfGuid; + }) + .then(cfGuid => applicationE2eHelper.deleteApplication(null, { appName }))); }); }); - - }); diff --git a/src/test-e2e/application/application-e2e-helpers.ts b/src/test-e2e/application/application-e2e-helpers.ts index ee4531d394..c32cc0b7f9 100644 --- a/src/test-e2e/application/application-e2e-helpers.ts +++ b/src/test-e2e/application/application-e2e-helpers.ts @@ -1,8 +1,8 @@ import { browser, promise } from 'protractor'; -import { IApp, IRoute } from '../../frontend/app/core/cf-api.types'; +import { IApp, IRoute, ISpace } from '../../frontend/app/core/cf-api.types'; import { APIResource, CFResponse } from '../../frontend/app/store/types/api.types'; -import { E2ESetup } from '../e2e'; +import { E2ESetup, e2e } from '../e2e'; import { CFHelpers } from '../helpers/cf-helpers'; import { CFRequestHelpers } from '../helpers/cf-request-helpers'; import { E2EHelpers } from '../helpers/e2e-helpers'; @@ -27,71 +27,142 @@ export class ApplicationE2eHelper { */ static getHostName = (appName) => appName.replace(/[\.:-]/g, '_'); - fetchApp = (cfGuid: string, appName: string): promise.Promise => { - return this.cfRequestHelper.sendCfGet( - cfGuid, - 'apps?inline-relations-depth=1&include-relations=routes,service_bindings&q=name IN ' + appName - ); + fetchAppInDefault = ( + appName?: string, + appGuid?: string, + cfGuid?: string, + spaceGuid?: string + ): promise.Promise<{ cfGuid: string, app: APIResource }> => { + const cfGuidP: promise.Promise = cfGuid ? + promise.fullyResolved(cfGuid) : + this.cfRequestHelper.getCfCnsi().then(endpoint => endpoint.guid); + const spaceGuidP: promise.Promise = spaceGuid ? promise.fullyResolved(spaceGuid) : cfGuidP + .then(cfGuid1 => { + cfGuid = cfGuid1; + console.log(cfGuid, cfGuid1); + return this.cfHelper.fetchOrg(cfGuid, e2e.secrets.getDefaultCFEndpoint().testOrg) + .then(org => { + return this.cfHelper.fetchSpace(cfGuid, org.metadata.guid, e2e.secrets.getDefaultCFEndpoint().testSpace); + }); + }) + .then(space => space.metadata.guid); + const appP: promise.Promise> = promise.all([cfGuidP, spaceGuidP]).then(([cfGuid1, spaceGuid1]) => { + return appName ? this.fetchApp(cfGuid1, spaceGuid1, appName) : this.fetchAppByGuid(cfGuid1, appGuid); + }); + return appP.then(app => ({ cfGuid, app })); } - deleteApplication = (cfGuid: string, app: APIResource): promise.Promise => { - if (!cfGuid || !app) { - return promise.fullyResolved({}); - } - - const promises = []; - - // Delete service instance - const serviceBindings = app.entity.service_bindings || []; - serviceBindings.forEach(serviceBinding => { - const url = 'service_instances/' + serviceBinding.entity.service_instance_guid + '?recursive=true&async=false'; - promises.push(this.cfRequestHelper.sendCfDelete(cfGuid, url)); + fetchApp = (cfGuid: string, spaceGuid: string, appName: string): promise.Promise> => { + console.log('appName ', appName); + return this.cfHelper.baseFetchApp(cfGuid, spaceGuid, appName).then(json => { + console.log('app2: ', json); + if (json.total_results < 1) { + return null; + } else if (json.total_results === 1) { + return json.resources[0]; + } else { + throw new Error('There should only be one app, found multiple. App Name: ' + appName); + } }); + } - // Delete route - const routes: promise.Promise[]> = app.entity.routes && app.entity.routes.length ? - promise.fullyResolved(app.entity.routes) : - this.cfRequestHelper.sendCfGet(cfGuid, `apps/${app.metadata.guid}/routes`).then(res => res.resources); - - promises.push(routes.then(appRoutes => { - const routePromises = []; - return promise.all(appRoutes.map(route => - this.cfRequestHelper.sendCfDelete(cfGuid, 'routes/' + route.metadata.guid + '?q=recursive=true&async=false')) - ); - })); - - const deps = promise.all(promises).catch(err => { - const errorString = `Failed to delete routes or services attached to an app`; - console.log(`${errorString}: ${err}`); - return promise.rejected(errorString); - }); + fetchAppByGuid = (cfGuid: string, appGuid: string): promise.Promise> => { + return this.cfRequestHelper.sendCfGet>(cfGuid, 'apps/' + appGuid); + } - const cfRequestHelper = this.cfRequestHelper; + deleteApplication = ( + haveApp?: { + cfGuid: string, + app: APIResource + }, + needApp?: { + appName?: string, + appGuid?: string + } + ): promise.Promise => { + if (!haveApp && !needApp) { + e2e.log(`Skipping Deleting App...`); + return; + } - // Delete app - return deps.then(() => { - // console.log('Successfully delete deps: ', this.cfRequestHelper.sendCfDelete); - return cfRequestHelper.sendCfDelete(cfGuid, 'apps/' + app.metadata.guid); - }).catch(err => fail(`Failed to delete app or associated dependencies: ${err}`)); + let cfGuid = haveApp ? haveApp.cfGuid : null; + const appP: promise.Promise> = haveApp ? + this.fetchAppByGuid(haveApp.cfGuid, haveApp.app.metadata.guid) : + this.fetchAppInDefault(needApp.appName, needApp.appGuid).then(res => { + cfGuid = res.cfGuid; + return res.app; + }); + + e2e.log(`Deleting App...`); + + return appP + .then(app => { + e2e.log(`'${app.entity.name}: Found app to delete'`); + + const promises = []; + + // Delete service instance + const serviceBindings = app.entity.service_bindings || []; + serviceBindings.forEach(serviceBinding => { + const url = 'service_instances/' + serviceBinding.entity.service_instance_guid + '?recursive=true&async=false'; + promises.push(this.cfRequestHelper.sendCfDelete(cfGuid, url)); + }); + + // Delete route + let routes: promise.Promise[]>; + if (app.entity.routes && app.entity.routes.length) { + routes = promise.fullyResolved(app.entity.routes); + } else { + e2e.log('BEFORE'); + browser.sleep(5000); + e2e.log('AFter'); + routes = this.cfRequestHelper.sendCfGet(cfGuid, `apps/${app.metadata.guid}/routes`).then(res => { + console.log(app.entity.name + ': using request RESPONSE ', res.resources); + return res.resources; + }); + } + promises.push(routes.then(appRoutes => { + if (!appRoutes.length) { + e2e.log(`'${app.entity.name}: Deleting App Routes... None found'. `); + return promise.fullyResolved({}); + } + e2e.log(`'${app.entity.name}: Deleting App Routes... '${appRoutes.map(route => route.entity.host).join(',')}'. `); + return promise.all(appRoutes.map(route => + this.cfRequestHelper.sendCfDelete(cfGuid, 'routes/' + route.metadata.guid + '?q=recursive=true&async=false') + )); + })); + + const deps = promise.all(promises).catch(err => { + const errorString = `Failed to delete routes or services attached to an app`; + console.log(`${errorString}: ${err}`); + return promise.rejected(errorString); + }); + + const cfRequestHelper = this.cfRequestHelper; + + // Delete app + return deps.then(() => this.cfHelper.baseDeleteApp(cfGuid, app.metadata.guid)).then(() => { + e2e.log(`'${app.entity.name}': Successfully deleted.`); + }); + }) + .catch(err => fail(`Failed to delete app or associated dependencies: ${err}`)); } createApp(cfGuid: string, orgName: string, spaceName: string, appName: string) { return browser.driver.wait( this.cfHelper.addOrgIfMissing(cfGuid, orgName) .then(org => { - return this.cfHelper.fetchSpace(cfGuid, org.resources[0].metadata.guid, spaceName); + return this.cfHelper.fetchSpace(cfGuid, org.metadata.guid, spaceName); }) .then(space => { expect(space).not.toBeNull(); - return this.cfHelper.createApp(cfGuid, space.metadata.guid, appName); - }) - .then(() => { - return this.fetchApp(cfGuid, appName); + return promise.all([ + this.cfHelper.baseCreateApp(cfGuid, space.metadata.guid, appName), + promise.fullyResolved(space) + ]); }) - .then(apps => { - expect(apps.total_results).toBe(1); - const app = apps.resources[0]; - return app; + .then(([app, space]: [APIResource, APIResource]) => { + return this.fetchApp(cfGuid, space.metadata.guid, appName); }) ); } diff --git a/src/test-e2e/helpers/cf-helpers.ts b/src/test-e2e/helpers/cf-helpers.ts index 971e6f8fd6..aea18e77bf 100644 --- a/src/test-e2e/helpers/cf-helpers.ts +++ b/src/test-e2e/helpers/cf-helpers.ts @@ -1,3 +1,7 @@ +import { promise } from 'protractor'; + +import { IOrganization } from '../../frontend/app/core/cf-api.types'; +import { APIResource } from '../../frontend/app/store/types/api.types'; import { E2ESetup } from '../e2e'; import { E2EConfigCloudFoundry } from '../e2e.types'; import { CFRequestHelpers } from './cf-request-helpers'; @@ -9,7 +13,11 @@ export class CFHelpers { this.cfRequestHelper = new CFRequestHelpers(e2eSetup); } - addOrgIfMissingForEndpointUsers(guid: string, endpoint: E2EConfigCloudFoundry, testOrgName: string) { + addOrgIfMissingForEndpointUsers( + guid: string, + endpoint: E2EConfigCloudFoundry, + testOrgName: string + ): promise.Promise> { let testAdminUser, testUser; return this.fetchUsers(guid).then(users => { testUser = this.findUser(users, endpoint.creds.nonAdmin.username); @@ -24,21 +32,20 @@ export class CFHelpers { return users.find(user => user && user.entity && user.entity.username === name); } - addOrgIfMissing(cnsiGuid, orgName, adminGuid?: string, userGuid?: string) { + addOrgIfMissing(cnsiGuid, orgName, adminGuid?: string, userGuid?: string): promise.Promise> { let added; return this.cfRequestHelper.sendCfGet(cnsiGuid, 'organizations?q=name IN ' + orgName).then(json => { if (json.total_results === 0) { added = true; - return this.cfRequestHelper.sendCfPost(cnsiGuid, 'organizations', { name: orgName }); + return this.cfRequestHelper.sendCfPost>(cnsiGuid, 'organizations', { name: orgName }); } - return json; + return json.resources[0]; }).then(newOrg => { if (!added || !adminGuid || !userGuid) { // No need to mess around with permissions, it exists already. return newOrg; } - const org = newOrg.resources ? newOrg.resources[0] as any : newOrg; - const orgGuid = org.metadata.guid; + const orgGuid = newOrg.metadata.guid; const p1 = this.cfRequestHelper.sendCfPut(cnsiGuid, 'organizations/' + orgGuid + '/users/' + adminGuid); const p2 = this.cfRequestHelper.sendCfPut(cnsiGuid, 'organizations/' + orgGuid + '/users/' + userGuid); // Add user to org users @@ -73,20 +80,6 @@ export class CFHelpers { }); } - fetchApp(cnsiGuid, appName) { - return this.cfRequestHelper.sendCfGet(cnsiGuid, - 'apps?inline-relations-depth=1&include-relations=routes,service_bindings&q=name IN ' + appName) - .then(json => { - if (json.total_results < 1) { - return null; - } else if (json.total_results === 1) { - return json.resources[0]; - } else { - throw new Error('There should only be one app, found multiple. Add Name: ' + appName); - } - }); - } - fetchServiceExist(cnsiGuid, serviceName) { return this.cfRequestHelper.sendCfGet(cnsiGuid, 'service_instances?q=name IN ' + serviceName).then(json => { return json.resources; @@ -121,6 +114,16 @@ export class CFHelpers { }); } + fetchOrg(cnsiGuid: string, orgName: string) { + return this.cfRequestHelper.sendCfGet(cnsiGuid, 'organizations?q=name IN ' + orgName).then(json => { + if (json.total_results > 0) { + const space = json.resources[0]; + return space; + } + return null; + }); + } + fetchSpace(cnsiGuid: string, orgGuid: string, spaceName: string) { return this.cfRequestHelper.sendCfGet(cnsiGuid, 'spaces?q=name IN ' + spaceName + '&organization_guid=' + orgGuid).then(json => { if (json.total_results > 0) { @@ -131,11 +134,19 @@ export class CFHelpers { }); } - createApp(cnsiGuid: string, spaceGuid: string, appName: string) { + // For fully fleshed out fetch see application-e2e-helpers + baseFetchApp(cnsiGuid: string, spaceGuid: string, appName: string) { + return this.cfRequestHelper.sendCfGet(cnsiGuid, + `apps?inline-relations-depth=1&include-relations=routes,service_bindings&q=name IN ${appName},space_guid IN ${spaceGuid}`); + } + + // For fully fleshed our create see application-e2e-helpers + baseCreateApp(cnsiGuid: string, spaceGuid: string, appName: string) { return this.cfRequestHelper.sendCfPost(cnsiGuid, 'apps', { name: appName, space_guid: spaceGuid }); } - deleteApp(cnsiGuid: string, appGuid: string) { + // For fully fleshed out delete see application-e2e-helpers (includes route and service instance deletion) + baseDeleteApp(cnsiGuid: string, appGuid: string) { return this.cfRequestHelper.sendCfDelete(cnsiGuid, 'apps/' + appGuid); } diff --git a/src/test-e2e/helpers/cf-request-helpers.ts b/src/test-e2e/helpers/cf-request-helpers.ts index 3018fea8b9..7826bdf764 100644 --- a/src/test-e2e/helpers/cf-request-helpers.ts +++ b/src/test-e2e/helpers/cf-request-helpers.ts @@ -28,10 +28,14 @@ export class CFRequestHelpers extends RequestHelpers { }); } - sendCfGet = (cfGuid: string, url: string): promise.Promise => this.sendCfRequest(cfGuid, url, 'GET').then(JSON.parse); + sendCfGet(cfGuid: string, url: string): promise.Promise { + return this.sendCfRequest(cfGuid, url, 'GET').then(JSON.parse); + } + + sendCfPost(cfGuid: string, url: string, body: any): promise.Promise { + return this.sendCfRequest(cfGuid, url, 'POST', body).then(JSON.parse); + } - sendCfPost = (cfGuid: string, url: string, body: any): promise.Promise => - this.sendCfRequest(cfGuid, url, 'POST', body).then(JSON.parse) sendCfPut = (cfGuid: string, url: string, body?: any): promise.Promise => this.sendCfRequest(cfGuid, url, 'PUT', body).then(JSON.parse) diff --git a/src/test-e2e/helpers/secrets-helpers.ts b/src/test-e2e/helpers/secrets-helpers.ts index 8aeef59a01..2aa09666a7 100644 --- a/src/test-e2e/helpers/secrets-helpers.ts +++ b/src/test-e2e/helpers/secrets-helpers.ts @@ -51,7 +51,7 @@ export class SecretsHelpers { return this.secrets.endpoints || {}; } - // Get the configration for the default CF Endpoint + // Get the configuration for the default CF Endpoint getDefaultCFEndpoint(): E2EConfigCloudFoundry { if (this.secrets.endpoints.cf) { return this.secrets.endpoints.cf.find((ep) => ep.name === DEFAULT_CF_NAME); From b28b9dd41613a17d44c309f6ae47c0e3a5eaeadb Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Tue, 14 Aug 2018 17:52:16 +0100 Subject: [PATCH 10/20] Removed fdescribe for full travis run --- src/test-e2e/application/application-create-e2e.spec.ts | 2 +- src/test-e2e/application/application-delete-e2e.spec.ts | 2 +- src/test-e2e/application/application-deploy-e2e.spec.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test-e2e/application/application-create-e2e.spec.ts b/src/test-e2e/application/application-create-e2e.spec.ts index 7c71445f46..1a90333d20 100644 --- a/src/test-e2e/application/application-create-e2e.spec.ts +++ b/src/test-e2e/application/application-create-e2e.spec.ts @@ -8,7 +8,7 @@ import { ApplicationE2eHelper } from './application-e2e-helpers'; import { ApplicationSummary } from './application-summary.po'; import { CreateApplicationStepper } from './create-application-stepper.po'; -fdescribe('Application Create', function () { +describe('Application Create', function () { let nav: SideNavigation; let appWall: ApplicationsPage; diff --git a/src/test-e2e/application/application-delete-e2e.spec.ts b/src/test-e2e/application/application-delete-e2e.spec.ts index 3220f0c7ed..3f9f40b241 100644 --- a/src/test-e2e/application/application-delete-e2e.spec.ts +++ b/src/test-e2e/application/application-delete-e2e.spec.ts @@ -9,7 +9,7 @@ import { CFHelpers } from '../helpers/cf-helpers'; import { ExpectedConditions } from 'protractor'; -fdescribe('Application Delete', function () { +describe('Application Delete', function () { let nav: SideNavigation; let appWall: ApplicationsPage; diff --git a/src/test-e2e/application/application-deploy-e2e.spec.ts b/src/test-e2e/application/application-deploy-e2e.spec.ts index 41baf64f2e..99e3cd389b 100644 --- a/src/test-e2e/application/application-deploy-e2e.spec.ts +++ b/src/test-e2e/application/application-deploy-e2e.spec.ts @@ -19,7 +19,7 @@ const spaceName = e2e.secrets.getDefaultCFEndpoint().testSpace; const appName = 'cf-quick-app'; -fdescribe('Application Deploy', function () { +describe('Application Deploy', function () { const testApp = e2e.secrets.getDefaultCFEndpoint().testDeployApp || 'nwmac/cf-quick-app'; From 6db66d136c3232a6d9bde8af211b7b37bd655ae2 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Wed, 15 Aug 2018 12:20:22 +0100 Subject: [PATCH 11/20] Upfront fetch default cf, org and space to avoid timing out when cf goes slowly --- .../application-create-e2e.spec.ts | 17 ++- .../application-delete-e2e.spec.ts | 4 +- .../application-deploy-e2e.spec.ts | 2 +- .../application/application-e2e-helpers.ts | 121 ++++++++++++------ src/test-e2e/helpers/cf-helpers.ts | 6 +- 5 files changed, 106 insertions(+), 44 deletions(-) diff --git a/src/test-e2e/application/application-create-e2e.spec.ts b/src/test-e2e/application/application-create-e2e.spec.ts index 1a90333d20..18263f7599 100644 --- a/src/test-e2e/application/application-create-e2e.spec.ts +++ b/src/test-e2e/application/application-create-e2e.spec.ts @@ -1,3 +1,5 @@ +import { browser } from 'protractor'; + import { IApp } from '../../frontend/app/core/cf-api.types'; import { APIResource } from '../../frontend/app/store/types/api.types'; import { ApplicationsPage } from '../applications/applications.po'; @@ -27,6 +29,9 @@ describe('Application Create', function () { applicationE2eHelper = new ApplicationE2eHelper(setup); }); + // Fetch the default cf, org and space up front. This saves time later + beforeAll(() => applicationE2eHelper.updateDefaultCfOrgSpace()); + beforeEach(() => nav.goto(SideNavMenuItem.Applications)); it('Should create app', () => { @@ -71,16 +76,22 @@ describe('Application Create', function () { createAppStepper.waitUntilNotShown(); // Determine the app guid and confirm we're on the app summary page - applicationE2eHelper.fetchAppInDefault(testAppName).then((res) => { + browser.wait(applicationE2eHelper.fetchAppInDefaultOrgSpace(testAppName).then((res) => { expect(res.app).not.toBe(null); app = res.app; cfGuid = res.cfGuid; const appSummaryPage = new ApplicationSummary(res.cfGuid, app.metadata.guid, app.entity.name); appSummaryPage.waitForPage(); - }); + })); }); - afterEach(() => app ? applicationE2eHelper.deleteApplication({ cfGuid, app }) : null); + afterAll(() => { + expect(cfGuid).toBeDefined(); + expect(cfGuid).not.toBeNull(); + expect(app).toBeDefined(); + expect(app).not.toBeNull(); + return app ? applicationE2eHelper.deleteApplication({ cfGuid, app }) : null; + }); }); diff --git a/src/test-e2e/application/application-delete-e2e.spec.ts b/src/test-e2e/application/application-delete-e2e.spec.ts index 3f9f40b241..ac4edb4bb9 100644 --- a/src/test-e2e/application/application-delete-e2e.spec.ts +++ b/src/test-e2e/application/application-delete-e2e.spec.ts @@ -1,12 +1,10 @@ import { ApplicationsPage } from '../applications/applications.po'; import { e2e } from '../e2e'; +import { CFHelpers } from '../helpers/cf-helpers'; import { ConsoleUserType } from '../helpers/e2e-helpers'; import { SideNavigation, SideNavMenuItem } from '../po/side-nav.po'; import { ApplicationE2eHelper } from './application-e2e-helpers'; import { ApplicationSummary } from './application-summary.po'; -import { CreateApplicationStepper } from './create-application-stepper.po'; -import { CFHelpers } from '../helpers/cf-helpers'; -import { ExpectedConditions } from 'protractor'; describe('Application Delete', function () { diff --git a/src/test-e2e/application/application-deploy-e2e.spec.ts b/src/test-e2e/application/application-deploy-e2e.spec.ts index 99e3cd389b..60c9ef05e2 100644 --- a/src/test-e2e/application/application-deploy-e2e.spec.ts +++ b/src/test-e2e/application/application-deploy-e2e.spec.ts @@ -1,7 +1,7 @@ import { browser, promise } from 'protractor'; import { ApplicationsPage } from '../applications/applications.po'; -import { e2e, E2E } from '../e2e'; +import { E2E, e2e } from '../e2e'; import { CFHelpers } from '../helpers/cf-helpers'; import { ConsoleUserType } from '../helpers/e2e-helpers'; import { SideNavigation, SideNavMenuItem } from '../po/side-nav.po'; diff --git a/src/test-e2e/application/application-e2e-helpers.ts b/src/test-e2e/application/application-e2e-helpers.ts index c32cc0b7f9..42adb3bf35 100644 --- a/src/test-e2e/application/application-e2e-helpers.ts +++ b/src/test-e2e/application/application-e2e-helpers.ts @@ -1,14 +1,16 @@ import { browser, promise } from 'protractor'; import { IApp, IRoute, ISpace } from '../../frontend/app/core/cf-api.types'; -import { APIResource, CFResponse } from '../../frontend/app/store/types/api.types'; -import { E2ESetup, e2e } from '../e2e'; +import { APIResource } from '../../frontend/app/store/types/api.types'; +import { e2e, E2ESetup } from '../e2e'; import { CFHelpers } from '../helpers/cf-helpers'; import { CFRequestHelpers } from '../helpers/cf-request-helpers'; import { E2EHelpers } from '../helpers/e2e-helpers'; const customAppLabel = E2EHelpers.e2eItemPrefix + (process.env.CUSTOM_APP_LABEL || process.env.USER); +let cachedDefaultCfGuid, cachedDefaultOrgGuid, cachedDefaultSpaceGuid; + export class ApplicationE2eHelper { cfRequestHelper: CFRequestHelpers; @@ -20,6 +22,7 @@ export class ApplicationE2eHelper { } static createApplicationName = (isoTime?: string): string => E2EHelpers.createCustomName(customAppLabel, isoTime).toLowerCase(); + /** * Get default sanitized URL name for App * @param {string} appName Name of the app @@ -27,35 +30,64 @@ export class ApplicationE2eHelper { */ static getHostName = (appName) => appName.replace(/[\.:-]/g, '_'); - fetchAppInDefault = ( + updateDefaultCfOrgSpace = (): promise.Promise => { + // Fetch cf guid, org guid, or space guid from ... cache or jetstream + return this.fetchDefaultCfGuid(false) + .then(() => this.fetchDefaultOrgGuid(false)) + .then(() => this.fetchDefaultSpaceGuid(false)); + } + + fetchDefaultCfGuid = (fromCache = true): promise.Promise => { + return fromCache && cachedDefaultCfGuid ? + promise.fullyResolved(cachedDefaultCfGuid) : + this.cfRequestHelper.getCfCnsi().then(endpoint => { + cachedDefaultCfGuid = endpoint.guid; + return cachedDefaultCfGuid; + }); + } + + fetchDefaultOrgGuid = (fromCache = true): promise.Promise => { + return fromCache && cachedDefaultOrgGuid ? + promise.fullyResolved(cachedDefaultOrgGuid) : + this.fetchDefaultCfGuid(true).then(guid => this.cfHelper.fetchOrg(guid, e2e.secrets.getDefaultCFEndpoint().testOrg).then(org => { + cachedDefaultOrgGuid = org.metadata.guid; + return cachedDefaultOrgGuid; + })); + } + + fetchDefaultSpaceGuid = (fromCache = true): promise.Promise => { + return fromCache && cachedDefaultSpaceGuid ? + promise.fullyResolved(cachedDefaultSpaceGuid) : + this.fetchDefaultOrgGuid(true).then(orgGuid => + this.cfHelper.fetchSpace(cachedDefaultCfGuid, orgGuid, e2e.secrets.getDefaultCFEndpoint().testSpace) + ).then(space => { + cachedDefaultSpaceGuid = space.metadata.guid; + return cachedDefaultSpaceGuid; + }); + } + + fetchAppInDefaultOrgSpace = ( appName?: string, appGuid?: string, cfGuid?: string, spaceGuid?: string ): promise.Promise<{ cfGuid: string, app: APIResource }> => { - const cfGuidP: promise.Promise = cfGuid ? - promise.fullyResolved(cfGuid) : - this.cfRequestHelper.getCfCnsi().then(endpoint => endpoint.guid); - const spaceGuidP: promise.Promise = spaceGuid ? promise.fullyResolved(spaceGuid) : cfGuidP - .then(cfGuid1 => { - cfGuid = cfGuid1; - console.log(cfGuid, cfGuid1); - return this.cfHelper.fetchOrg(cfGuid, e2e.secrets.getDefaultCFEndpoint().testOrg) - .then(org => { - return this.cfHelper.fetchSpace(cfGuid, org.metadata.guid, e2e.secrets.getDefaultCFEndpoint().testSpace); - }); - }) - .then(space => space.metadata.guid); + + const cfGuidP: promise.Promise = cfGuid ? promise.fullyResolved(cfGuid) : this.fetchDefaultCfGuid(); + const spaceGuidP: promise.Promise = spaceGuid ? promise.fullyResolved(spaceGuid) : this.fetchDefaultSpaceGuid(); + const appP: promise.Promise> = promise.all([cfGuidP, spaceGuidP]).then(([cfGuid1, spaceGuid1]) => { return appName ? this.fetchApp(cfGuid1, spaceGuid1, appName) : this.fetchAppByGuid(cfGuid1, appGuid); }); - return appP.then(app => ({ cfGuid, app })); + + return appP.then(app => ({ cfGuid: cachedDefaultCfGuid, app })).catch(e => { + e2e.log('Failed to fetch application in default cf, org and space: ' + e); + throw e; + }); } fetchApp = (cfGuid: string, spaceGuid: string, appName: string): promise.Promise> => { - console.log('appName ', appName); return this.cfHelper.baseFetchApp(cfGuid, spaceGuid, appName).then(json => { - console.log('app2: ', json); if (json.total_results < 1) { return null; } else if (json.total_results === 1) { @@ -70,6 +102,26 @@ export class ApplicationE2eHelper { return this.cfRequestHelper.sendCfGet>(cfGuid, 'apps/' + appGuid); } + private chain( + currentValue: T, + nextChainFc: () => promise.Promise, + maxChain: number, + abortChainFc: (val: T) => boolean, + count = 0): promise.Promise { + if (count > maxChain || abortChainFc(currentValue)) { + return promise.fullyResolved(currentValue); + } + e2e.log('Chaining requests. Count: ' + count); + + return nextChainFc().then(res => { + if (abortChainFc(res)) { + return promise.fullyResolved(res); + } + browser.sleep(500); + return this.chain(res, nextChainFc, maxChain, abortChainFc, ++count); + }); + } + deleteApplication = ( haveApp?: { cfGuid: string, @@ -86,9 +138,10 @@ export class ApplicationE2eHelper { } let cfGuid = haveApp ? haveApp.cfGuid : null; + const appP: promise.Promise> = haveApp ? this.fetchAppByGuid(haveApp.cfGuid, haveApp.app.metadata.guid) : - this.fetchAppInDefault(needApp.appName, needApp.appGuid).then(res => { + this.fetchAppInDefaultOrgSpace(needApp.appName, needApp.appGuid).then(res => { cfGuid = res.cfGuid; return res.app; }); @@ -97,7 +150,7 @@ export class ApplicationE2eHelper { return appP .then(app => { - e2e.log(`'${app.entity.name}: Found app to delete'`); + e2e.log(`'${app.entity.name}': Found app to delete`); const promises = []; @@ -109,24 +162,20 @@ export class ApplicationE2eHelper { }); // Delete route - let routes: promise.Promise[]>; - if (app.entity.routes && app.entity.routes.length) { - routes = promise.fullyResolved(app.entity.routes); - } else { - e2e.log('BEFORE'); - browser.sleep(5000); - e2e.log('AFter'); - routes = this.cfRequestHelper.sendCfGet(cfGuid, `apps/${app.metadata.guid}/routes`).then(res => { - console.log(app.entity.name + ': using request RESPONSE ', res.resources); - return res.resources; - }); - } + // If we have zero routes, attempt 10 times to fetch a populated route list + const routes: promise.Promise[]> = this.chain[]>( + app.entity.routes, + () => this.cfHelper.fetchAppRoutes(cfGuid, app.metadata.guid), + 10, + (res) => !!res && !!res.length + ); + promises.push(routes.then(appRoutes => { - if (!appRoutes.length) { - e2e.log(`'${app.entity.name}: Deleting App Routes... None found'. `); + if (!appRoutes || !appRoutes.length) { + e2e.log(`'${app.entity.name}': Deleting App Routes... None found'. `); return promise.fullyResolved({}); } - e2e.log(`'${app.entity.name}: Deleting App Routes... '${appRoutes.map(route => route.entity.host).join(',')}'. `); + e2e.log(`'${app.entity.name}': Deleting App Routes... '${appRoutes.map(route => route.entity.host).join(',')}'. `); return promise.all(appRoutes.map(route => this.cfRequestHelper.sendCfDelete(cfGuid, 'routes/' + route.metadata.guid + '?q=recursive=true&async=false') )); diff --git a/src/test-e2e/helpers/cf-helpers.ts b/src/test-e2e/helpers/cf-helpers.ts index aea18e77bf..d40238ad36 100644 --- a/src/test-e2e/helpers/cf-helpers.ts +++ b/src/test-e2e/helpers/cf-helpers.ts @@ -1,6 +1,6 @@ import { promise } from 'protractor'; -import { IOrganization } from '../../frontend/app/core/cf-api.types'; +import { IOrganization, IRoute } from '../../frontend/app/core/cf-api.types'; import { APIResource } from '../../frontend/app/store/types/api.types'; import { E2ESetup } from '../e2e'; import { E2EConfigCloudFoundry } from '../e2e.types'; @@ -150,4 +150,8 @@ export class CFHelpers { return this.cfRequestHelper.sendCfDelete(cnsiGuid, 'apps/' + appGuid); } + fetchAppRoutes(cnsiGuid: string, appGuid: string): promise.Promise[]> { + return this.cfRequestHelper.sendCfGet(cnsiGuid, `apps/${appGuid}/routes`).then(res => res.resources); + } + } From e6c13f9543fb4cbdc562d31e690e05b419a41d3d Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Wed, 15 Aug 2018 12:55:01 +0100 Subject: [PATCH 12/20] Fix type failure in travis + other tidy ups --- src/test-e2e/application/application-deploy-e2e.spec.ts | 4 ---- src/test-e2e/application/application-e2e-helpers.ts | 2 +- src/test-e2e/application/application-summary.po.ts | 5 +++-- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/test-e2e/application/application-deploy-e2e.spec.ts b/src/test-e2e/application/application-deploy-e2e.spec.ts index 60c9ef05e2..3aa63d245d 100644 --- a/src/test-e2e/application/application-deploy-e2e.spec.ts +++ b/src/test-e2e/application/application-deploy-e2e.spec.ts @@ -110,15 +110,11 @@ describe('Application Deploy', function () { // Click next deployApp.stepper.next(); - (new E2E()).log(`Debug: Should be arriving at app summary`); - // Should be app summary browser.wait(ApplicationSummary.detect() .then(appSummary => { - (new E2E()).log(`Debug: Created app summary obj`); appSummary.waitForPage(); appSummary.header.waitForTitleText(appName); - (new E2E()).log(`Debug: Have title`); return appSummary.cfGuid; }) .then(cfGuid => applicationE2eHelper.deleteApplication(null, { appName }))); diff --git a/src/test-e2e/application/application-e2e-helpers.ts b/src/test-e2e/application/application-e2e-helpers.ts index 42adb3bf35..44d2513a74 100644 --- a/src/test-e2e/application/application-e2e-helpers.ts +++ b/src/test-e2e/application/application-e2e-helpers.ts @@ -118,7 +118,7 @@ export class ApplicationE2eHelper { return promise.fullyResolved(res); } browser.sleep(500); - return this.chain(res, nextChainFc, maxChain, abortChainFc, ++count); + return this.chain(res, nextChainFc, maxChain, abortChainFc, ++count); }); } diff --git a/src/test-e2e/application/application-summary.po.ts b/src/test-e2e/application/application-summary.po.ts index 3f817f2302..e793098cb7 100644 --- a/src/test-e2e/application/application-summary.po.ts +++ b/src/test-e2e/application/application-summary.po.ts @@ -1,7 +1,8 @@ import { browser, promise } from 'protractor'; + +import { e2e } from '../e2e'; import { Page } from '../po/page.po'; import { DeleteApplication } from './delete-app.po'; -import { E2E } from '../e2e'; export class ApplicationSummary extends Page { @@ -21,7 +22,7 @@ export class ApplicationSummary extends Page { expect(urlParts[3]).toBe('summary'); const cfGuid = urlParts[1]; const appGuid = urlParts[2]; - (new E2E()).log(`Creating App Summary object using cfGuid: '${cfGuid}' and appGuid: '${appGuid}'`); + e2e.log(`Creating App Summary object using cfGuid: '${cfGuid}' and appGuid: '${appGuid}'`); return new ApplicationSummary(cfGuid, appGuid); }); } From 21f1ecc1a53c84a7be03cf2ecd18b45110520da1 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Wed, 15 Aug 2018 14:04:12 +0100 Subject: [PATCH 13/20] Fix skip of create service instance bind app step --- .../application-create-e2e.spec.ts | 2 +- .../application/application-e2e-helpers.ts | 44 ++-------------- src/test-e2e/helpers/cf-helpers.ts | 52 +++++++++++++++++-- .../create-service-instance-e2e.spec.ts | 39 +++++++------- .../create-service-instance-stepper.po.ts | 4 ++ .../marketplace/services-helper-e2e.ts | 47 ++++++++--------- src/test-e2e/po/stepper.po.ts | 10 +++- 7 files changed, 106 insertions(+), 92 deletions(-) diff --git a/src/test-e2e/application/application-create-e2e.spec.ts b/src/test-e2e/application/application-create-e2e.spec.ts index 18263f7599..61a8af7133 100644 --- a/src/test-e2e/application/application-create-e2e.spec.ts +++ b/src/test-e2e/application/application-create-e2e.spec.ts @@ -30,7 +30,7 @@ describe('Application Create', function () { }); // Fetch the default cf, org and space up front. This saves time later - beforeAll(() => applicationE2eHelper.updateDefaultCfOrgSpace()); + beforeAll(() => applicationE2eHelper.cfHelper.updateDefaultCfOrgSpace()); beforeEach(() => nav.goto(SideNavMenuItem.Applications)); diff --git a/src/test-e2e/application/application-e2e-helpers.ts b/src/test-e2e/application/application-e2e-helpers.ts index 44d2513a74..e8ea59bbba 100644 --- a/src/test-e2e/application/application-e2e-helpers.ts +++ b/src/test-e2e/application/application-e2e-helpers.ts @@ -9,8 +9,6 @@ import { E2EHelpers } from '../helpers/e2e-helpers'; const customAppLabel = E2EHelpers.e2eItemPrefix + (process.env.CUSTOM_APP_LABEL || process.env.USER); -let cachedDefaultCfGuid, cachedDefaultOrgGuid, cachedDefaultSpaceGuid; - export class ApplicationE2eHelper { cfRequestHelper: CFRequestHelpers; @@ -30,42 +28,6 @@ export class ApplicationE2eHelper { */ static getHostName = (appName) => appName.replace(/[\.:-]/g, '_'); - updateDefaultCfOrgSpace = (): promise.Promise => { - // Fetch cf guid, org guid, or space guid from ... cache or jetstream - return this.fetchDefaultCfGuid(false) - .then(() => this.fetchDefaultOrgGuid(false)) - .then(() => this.fetchDefaultSpaceGuid(false)); - } - - fetchDefaultCfGuid = (fromCache = true): promise.Promise => { - return fromCache && cachedDefaultCfGuid ? - promise.fullyResolved(cachedDefaultCfGuid) : - this.cfRequestHelper.getCfCnsi().then(endpoint => { - cachedDefaultCfGuid = endpoint.guid; - return cachedDefaultCfGuid; - }); - } - - fetchDefaultOrgGuid = (fromCache = true): promise.Promise => { - return fromCache && cachedDefaultOrgGuid ? - promise.fullyResolved(cachedDefaultOrgGuid) : - this.fetchDefaultCfGuid(true).then(guid => this.cfHelper.fetchOrg(guid, e2e.secrets.getDefaultCFEndpoint().testOrg).then(org => { - cachedDefaultOrgGuid = org.metadata.guid; - return cachedDefaultOrgGuid; - })); - } - - fetchDefaultSpaceGuid = (fromCache = true): promise.Promise => { - return fromCache && cachedDefaultSpaceGuid ? - promise.fullyResolved(cachedDefaultSpaceGuid) : - this.fetchDefaultOrgGuid(true).then(orgGuid => - this.cfHelper.fetchSpace(cachedDefaultCfGuid, orgGuid, e2e.secrets.getDefaultCFEndpoint().testSpace) - ).then(space => { - cachedDefaultSpaceGuid = space.metadata.guid; - return cachedDefaultSpaceGuid; - }); - } - fetchAppInDefaultOrgSpace = ( appName?: string, appGuid?: string, @@ -73,14 +35,14 @@ export class ApplicationE2eHelper { spaceGuid?: string ): promise.Promise<{ cfGuid: string, app: APIResource }> => { - const cfGuidP: promise.Promise = cfGuid ? promise.fullyResolved(cfGuid) : this.fetchDefaultCfGuid(); - const spaceGuidP: promise.Promise = spaceGuid ? promise.fullyResolved(spaceGuid) : this.fetchDefaultSpaceGuid(); + const cfGuidP: promise.Promise = cfGuid ? promise.fullyResolved(cfGuid) : this.cfHelper.fetchDefaultCfGuid(); + const spaceGuidP: promise.Promise = spaceGuid ? promise.fullyResolved(spaceGuid) : this.cfHelper.fetchDefaultSpaceGuid(); const appP: promise.Promise> = promise.all([cfGuidP, spaceGuidP]).then(([cfGuid1, spaceGuid1]) => { return appName ? this.fetchApp(cfGuid1, spaceGuid1, appName) : this.fetchAppByGuid(cfGuid1, appGuid); }); - return appP.then(app => ({ cfGuid: cachedDefaultCfGuid, app })).catch(e => { + return appP.then(app => ({ cfGuid: this.cfHelper.cachedDefaultCfGuid, app })).catch(e => { e2e.log('Failed to fetch application in default cf, org and space: ' + e); throw e; }); diff --git a/src/test-e2e/helpers/cf-helpers.ts b/src/test-e2e/helpers/cf-helpers.ts index 2db96e0b48..e910ae8b4f 100644 --- a/src/test-e2e/helpers/cf-helpers.ts +++ b/src/test-e2e/helpers/cf-helpers.ts @@ -9,6 +9,9 @@ import { CFRequestHelpers } from './cf-request-helpers'; export class CFHelpers { cfRequestHelper: CFRequestHelpers; + cachedDefaultCfGuid: string; + cachedDefaultOrgGuid: string; + cachedDefaultSpaceGuid: string; constructor(public e2eSetup: E2ESetup) { this.cfRequestHelper = new CFRequestHelpers(e2eSetup); @@ -138,6 +141,14 @@ export class CFHelpers { }); } + fetchAppsCountInSpace(cnsiGuid: string, spaceGuid: string) { + console.log(cnsiGuid, spaceGuid); + return this.cfRequestHelper.sendCfGet(cnsiGuid, `spaces/${spaceGuid}/apps`).then(json => { + console.log(json.total_results); + return json.total_results; + }); + } + // For fully fleshed out fetch see application-e2e-helpers baseFetchApp(cnsiGuid: string, spaceGuid: string, appName: string) { return this.cfRequestHelper.sendCfGet(cnsiGuid, @@ -146,9 +157,7 @@ export class CFHelpers { // For fully fleshed our create see application-e2e-helpers baseCreateApp(cnsiGuid: string, spaceGuid: string, appName: string) { - return this.cfRequestHelper.sendCfGet(cnsiGuid, `spaces/${spaceGuid}/apps`).then(json => { - return json.total_results; - }); + return this.cfRequestHelper.sendCfPost(cnsiGuid, 'apps', { name: appName, space_guid: spaceGuid }); } // For fully fleshed out delete see application-e2e-helpers (includes route and service instance deletion) @@ -160,4 +169,41 @@ export class CFHelpers { return this.cfRequestHelper.sendCfGet(cnsiGuid, `apps/${appGuid}/routes`).then(res => res.resources); } + updateDefaultCfOrgSpace = (): promise.Promise => { + // Fetch cf guid, org guid, or space guid from ... cache or jetstream + return this.fetchDefaultCfGuid(false) + .then(() => this.fetchDefaultOrgGuid(false)) + .then(() => this.fetchDefaultSpaceGuid(false)); + } + + + fetchDefaultCfGuid = (fromCache = true): promise.Promise => { + return fromCache && this.cachedDefaultCfGuid ? + promise.fullyResolved(this.cachedDefaultCfGuid) : + this.cfRequestHelper.getCfGuid().then(guid => { + this.cachedDefaultCfGuid = guid; + return this.cachedDefaultCfGuid; + }); + } + + fetchDefaultOrgGuid = (fromCache = true): promise.Promise => { + return fromCache && this.cachedDefaultOrgGuid ? + promise.fullyResolved(this.cachedDefaultOrgGuid) : + this.fetchDefaultCfGuid(true).then(guid => this.fetchOrg(guid, e2e.secrets.getDefaultCFEndpoint().testOrg).then(org => { + this.cachedDefaultOrgGuid = org.metadata.guid; + return this.cachedDefaultOrgGuid; + })); + } + + fetchDefaultSpaceGuid = (fromCache = true): promise.Promise => { + return fromCache && this.cachedDefaultSpaceGuid ? + promise.fullyResolved(this.cachedDefaultSpaceGuid) : + this.fetchDefaultOrgGuid(true).then(orgGuid => + this.fetchSpace(this.cachedDefaultCfGuid, orgGuid, e2e.secrets.getDefaultCFEndpoint().testSpace) + ).then(space => { + this.cachedDefaultSpaceGuid = space.metadata.guid; + return this.cachedDefaultSpaceGuid; + }); + } + } diff --git a/src/test-e2e/marketplace/create-service-instance-e2e.spec.ts b/src/test-e2e/marketplace/create-service-instance-e2e.spec.ts index 751d3d3770..cba954e1e3 100644 --- a/src/test-e2e/marketplace/create-service-instance-e2e.spec.ts +++ b/src/test-e2e/marketplace/create-service-instance-e2e.spec.ts @@ -1,4 +1,4 @@ -import { browser, ElementFinder, promise } from 'protractor'; +import { ElementFinder, promise } from 'protractor'; import { e2e } from '../e2e'; import { ConsoleUserType } from '../helpers/e2e-helpers'; @@ -98,30 +98,29 @@ describe('Create Service Instance', () => { }); it('- should return user to Service summary when cancelled on service instance details', () => { - browser.wait(servicesHelperE2E.canBindAppStep() - .then(canBindApp => { - // Select CF/Org/Space - servicesHelperE2E.setCfOrgSpace(); - createServiceInstance.stepper.next(); + // Select CF/Org/Space + servicesHelperE2E.setCfOrgSpace(); + createServiceInstance.stepper.next(); - // Select Service - servicesHelperE2E.setServiceSelection(); - createServiceInstance.stepper.next(); + // Select Service + servicesHelperE2E.setServiceSelection(); + createServiceInstance.stepper.next(); - // Select Service Plan - servicesHelperE2E.setServicePlan(); - createServiceInstance.stepper.next(); + // Select Service Plan + servicesHelperE2E.setServicePlan(); + createServiceInstance.stepper.next(); - if (canBindApp) { - // Bind App - servicesHelperE2E.setBindApp(); - createServiceInstance.stepper.next(); - } + createServiceInstance.stepper.isBindAppStepDisabled().then(bindAppDisabled => { + if (!bindAppDisabled) { + // Bind App + servicesHelperE2E.setBindApp(); + createServiceInstance.stepper.next(); + } - createServiceInstance.stepper.cancel(); + createServiceInstance.stepper.cancel(); - servicesWall.isActivePage(); - })); + servicesWall.isActivePage(); + }); }); afterAll((done) => { diff --git a/src/test-e2e/marketplace/create-service-instance-stepper.po.ts b/src/test-e2e/marketplace/create-service-instance-stepper.po.ts index ff5593db01..b24ec0e037 100644 --- a/src/test-e2e/marketplace/create-service-instance-stepper.po.ts +++ b/src/test-e2e/marketplace/create-service-instance-stepper.po.ts @@ -37,4 +37,8 @@ export class CreateServiceInstanceStepper extends StepperComponent { return this.getStepperForm().fill({ [this.serviceNameFieldName]: serviceInstanceName }); } + isBindAppStepDisabled = () => { + return this.isStepDisabled('Bind App (Optional)'); + } + } diff --git a/src/test-e2e/marketplace/services-helper-e2e.ts b/src/test-e2e/marketplace/services-helper-e2e.ts index 510557049e..5456d398b1 100644 --- a/src/test-e2e/marketplace/services-helper-e2e.ts +++ b/src/test-e2e/marketplace/services-helper-e2e.ts @@ -43,41 +43,36 @@ export class ServicesHelperE2E { } createService = () => { - browser.wait(this.canBindAppStep() - .then(canBindApp => { - this.createServiceInstance.waitForPage(); + this.createServiceInstance.waitForPage(); - // Select CF/Org/Space - this.setCfOrgSpace(); - this.createServiceInstance.stepper.next(); + // Select CF/Org/Space + this.setCfOrgSpace(); + this.createServiceInstance.stepper.next(); - // Select Service - this.setServiceSelection(); - this.createServiceInstance.stepper.next(); + // Select Service + this.setServiceSelection(); + this.createServiceInstance.stepper.next(); - // Select Service Plan - this.setServicePlan(); - this.createServiceInstance.stepper.next(); + // Select Service Plan + this.setServicePlan(); + this.createServiceInstance.stepper.next(); - // Bind App - if (canBindApp) { - this.setBindApp(); - this.createServiceInstance.stepper.next(); - } + // Bind App + this.createServiceInstance.stepper.isBindAppStepDisabled().then(bindAppDisabled => { + if (!bindAppDisabled) { + this.setBindApp(); + this.createServiceInstance.stepper.next(); + } - this.setServiceInstanceDetail(); + this.setServiceInstanceDetail(); - this.createServiceInstance.stepper.next(); - }) - ); + this.createServiceInstance.stepper.next(); + }); } canBindAppStep = (): promise.Promise => { - const cf = e2e.secrets.getDefaultCFEndpoint(); - const endpointGuid = e2e.helper.getEndpointGuid(e2e.info, cf.name); - return this.cfHelper.fetchSpace(endpointGuid, cf.testSpace) - .then(space => space.metadata.guid) - .then(spaceGuid => this.cfHelper.fetchAppsCountInSpace(endpointGuid, spaceGuid)) + return this.cfHelper.fetchDefaultSpaceGuid(true) + .then(spaceGuid => this.cfHelper.fetchAppsCountInSpace(this.cfHelper.cachedDefaultCfGuid, spaceGuid)) .then(totalAppsInSpace => !!totalAppsInSpace); } diff --git a/src/test-e2e/po/stepper.po.ts b/src/test-e2e/po/stepper.po.ts index d97fbd9cce..ec11026843 100644 --- a/src/test-e2e/po/stepper.po.ts +++ b/src/test-e2e/po/stepper.po.ts @@ -74,10 +74,14 @@ export class StepperComponent extends Component { return browser.wait(until.textToBePresentInElement(lastActiveHeader, stepName), 5000); } + isStepDisabled(stepName: string): promise.Promise { + return this.getStep(stepName).element(by.css('app-dot-content span.disabled')).isPresent(); + } + getStepperForm = (): FormComponent => new FormComponent(this.locator.element(by.className('stepper-form'))); hasStep(name: string) { - return this.locator.element(by.cssContainingText('.steppers__header .steppers__header-text', name)).isPresent(); + return this.getStep(name).isPresent(); } getStepNames() { @@ -88,4 +92,8 @@ export class StepperComponent extends Component { return element(by.css('.steppers__header--active .steppers__header-text')).getText(); } + getStep(stepName) { + return this.locator.element(by.cssContainingText('.steppers__header', stepName)); + } + } From 76f00990c3dd4e4dd0a8273ab2bc9539adff17ad Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Wed, 15 Aug 2018 14:11:59 +0100 Subject: [PATCH 14/20] Potential fix for travis e2e compile error --- src/test-e2e/application/application-e2e-helpers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test-e2e/application/application-e2e-helpers.ts b/src/test-e2e/application/application-e2e-helpers.ts index e8ea59bbba..9830fccb83 100644 --- a/src/test-e2e/application/application-e2e-helpers.ts +++ b/src/test-e2e/application/application-e2e-helpers.ts @@ -77,7 +77,7 @@ export class ApplicationE2eHelper { return nextChainFc().then(res => { if (abortChainFc(res)) { - return promise.fullyResolved(res); + return promise.fullyResolved(res); } browser.sleep(500); return this.chain(res, nextChainFc, maxChain, abortChainFc, ++count); @@ -145,7 +145,7 @@ export class ApplicationE2eHelper { const deps = promise.all(promises).catch(err => { const errorString = `Failed to delete routes or services attached to an app`; - console.log(`${errorString}: ${err}`); + e2e.log(`${errorString}: ${err}`); return promise.rejected(errorString); }); From bfa18822691abc1953c9ffa0a8e7f89e4444d85f Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Wed, 15 Aug 2018 15:09:32 +0100 Subject: [PATCH 15/20] Fix incorrect guid when fetching space --- src/frontend/app/features/applications/application.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/app/features/applications/application.service.ts b/src/frontend/app/features/applications/application.service.ts index 6db3cbc076..feee6d5231 100644 --- a/src/frontend/app/features/applications/application.service.ts +++ b/src/frontend/app/features/applications/application.service.ts @@ -189,7 +189,7 @@ export class ApplicationService { spaceSchemaKey, entityFactory(spaceWithOrgKey), app.space_guid, - new GetSpace(app.guid, app.cfGuid, [createEntityRelationKey(spaceSchemaKey, organizationSchemaKey)], true) + new GetSpace(app.space_guid, app.cfGuid, [createEntityRelationKey(spaceSchemaKey, organizationSchemaKey)], true) ).waitForEntity$.pipe( map(entityInfo => entityInfo.entity) ); From d2834be3a95a775ec3e0cb4b59f60c2a56ab9448 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Wed, 15 Aug 2018 17:56:27 +0100 Subject: [PATCH 16/20] When fetching space's, don't attempt to fetch space's app's route's apps - Also tidy up space e2e tests --- .../services/cloud-foundry-space.service.ts | 1 - .../space-level/cf-space-level-e2e.spec.ts | 38 ++++++++++--------- .../space-level/cf-space-level-page.po.ts | 4 +- src/test-e2e/helpers/reset-helpers.ts | 2 +- src/test-e2e/po/component.po.ts | 2 +- src/test-e2e/po/page-subheader.po.ts | 2 +- 6 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/frontend/app/features/cloud-foundry/services/cloud-foundry-space.service.ts b/src/frontend/app/features/cloud-foundry/services/cloud-foundry-space.service.ts index 5f4d7911dd..61d304c945 100644 --- a/src/frontend/app/features/cloud-foundry/services/cloud-foundry-space.service.ts +++ b/src/frontend/app/features/cloud-foundry/services/cloud-foundry-space.service.ts @@ -109,7 +109,6 @@ export class CloudFoundrySpaceService { createEntityRelationKey(serviceInstancesSchemaKey, serviceBindingSchemaKey), createEntityRelationKey(serviceBindingSchemaKey, applicationSchemaKey), createEntityRelationKey(spaceSchemaKey, routeSchemaKey), - createEntityRelationKey(routeSchemaKey, applicationSchemaKey), ]; if (!isAdmin) { // We're only interested in fetching space roles via the space request for non-admins. This is the only way to guarantee the roles diff --git a/src/test-e2e/cloud-foundry/space-level/cf-space-level-e2e.spec.ts b/src/test-e2e/cloud-foundry/space-level/cf-space-level-e2e.spec.ts index 9b0dc55128..bcdf1d5043 100644 --- a/src/test-e2e/cloud-foundry/space-level/cf-space-level-e2e.spec.ts +++ b/src/test-e2e/cloud-foundry/space-level/cf-space-level-e2e.spec.ts @@ -1,18 +1,18 @@ import { browser } from 'protractor'; -import { ApplicationE2eHelper } from '../../application/application-e2e-helpers'; import { e2e, E2ESetup } from '../../e2e'; import { E2EConfigCloudFoundry } from '../../e2e.types'; +import { CFHelpers } from '../../helpers/cf-helpers'; import { ConsoleUserType } from '../../helpers/e2e-helpers'; import { CfSpaceLevelPage } from './cf-space-level-page.po'; -describe('CF - Space Level - ', () => { +describe('CF - Space Level -', () => { let spacePage: CfSpaceLevelPage; let e2eSetup: E2ESetup; let defaultCf: E2EConfigCloudFoundry; - let applicationE2eHelper: ApplicationE2eHelper; + let cfHelper: CFHelpers; function setup(user: ConsoleUserType) { e2eSetup = e2e.setup(ConsoleUserType.admin) @@ -22,7 +22,7 @@ describe('CF - Space Level - ', () => { .connectAllEndpoints(ConsoleUserType.user) .loginAs(user) .getInfo(); - applicationE2eHelper = new ApplicationE2eHelper(e2eSetup); + cfHelper = new CFHelpers(e2eSetup); } function testBreadcrumb() { @@ -45,43 +45,47 @@ describe('CF - Space Level - ', () => { function navToPage() { defaultCf = e2e.secrets.getDefaultCFEndpoint(); - const endpointGuid = e2e.helper.getEndpointGuid(e2e.info, defaultCf.name); - browser.wait(applicationE2eHelper.cfHelper.fetchSpace(endpointGuid, defaultCf.testSpace).then((space => { - spacePage = CfSpaceLevelPage.forEndpoint(endpointGuid, space.entity.organization_guid, space.metadata.guid); - spacePage.navigateTo(); - spacePage.waitForPageOrChildPage(); - spacePage.loadingIndicator.waitUntilNotShown(); - }))); + browser.wait( + cfHelper.fetchDefaultSpaceGuid(true) + .then(spaceGuid => { + spacePage = CfSpaceLevelPage.forEndpoint( + cfHelper.cachedDefaultCfGuid, + cfHelper.cachedDefaultOrgGuid, + cfHelper.cachedDefaultSpaceGuid + ); + return spacePage.navigateTo(); + }) + .then(() => spacePage.waitForPageOrChildPage()) + .then(() => spacePage.loadingIndicator.waitUntilNotShown()) + ); } - describe('As Admin', () => { + describe('As Admin -', () => { beforeEach(() => { setup(ConsoleUserType.admin); }); - describe('Basic Tests - ', () => { + describe('Basic Tests -', () => { beforeEach(navToPage); it('Breadcrumb', testBreadcrumb); it('Walk Tabs', testTabs); - }); }); - describe('As User', () => { + fdescribe('As User -', () => { beforeEach(() => { setup(ConsoleUserType.user); }); - describe('Basic Tests - ', () => { + describe('Basic Tests -', () => { beforeEach(navToPage); it('Breadcrumb', testBreadcrumb); it('Walk Tabs', testTabs); - }); }); diff --git a/src/test-e2e/cloud-foundry/space-level/cf-space-level-page.po.ts b/src/test-e2e/cloud-foundry/space-level/cf-space-level-page.po.ts index e07ce40795..718f4eb3c7 100644 --- a/src/test-e2e/cloud-foundry/space-level/cf-space-level-page.po.ts +++ b/src/test-e2e/cloud-foundry/space-level/cf-space-level-page.po.ts @@ -8,9 +8,7 @@ import { CFPage } from '../../po/cf-page.po'; export class CfSpaceLevelPage extends CFPage { static forEndpoint(guid: string, orgGuid: string, spaceGuid: string): CfSpaceLevelPage { - const page = new CfSpaceLevelPage(); - page.navLink = `/cloud-foundry/${guid}/organizations/${orgGuid}/spaces/${spaceGuid}`; - return page; + return new CfSpaceLevelPage(`/cloud-foundry/${guid}/organizations/${orgGuid}/spaces/${spaceGuid}`); } goToSummaryTab() { diff --git a/src/test-e2e/helpers/reset-helpers.ts b/src/test-e2e/helpers/reset-helpers.ts index a8a622e522..2c79f938c4 100644 --- a/src/test-e2e/helpers/reset-helpers.ts +++ b/src/test-e2e/helpers/reset-helpers.ts @@ -42,7 +42,7 @@ export class ResetsHelpers { } /** - * Get all of the registered Endpoints and comnnect all of them for which credentials + * Get all of the registered Endpoints and connect all of them for which credentials * have been configured */ connectEndpoint(req, endpointName: string, userType: ConsoleUserType = ConsoleUserType.admin) { diff --git a/src/test-e2e/po/component.po.ts b/src/test-e2e/po/component.po.ts index 811d879c07..0bdeed0a79 100644 --- a/src/test-e2e/po/component.po.ts +++ b/src/test-e2e/po/component.po.ts @@ -33,7 +33,7 @@ export class Component { } waitUntilNotShown(): promise.Promise { - return browser.wait(until.invisibilityOf(this.locator), 5000); + return browser.wait(until.invisibilityOf(this.locator), 20000); } protected hasClass(cls, element = this.locator): promise.Promise { diff --git a/src/test-e2e/po/page-subheader.po.ts b/src/test-e2e/po/page-subheader.po.ts index 4fac7dc808..4e94affbf4 100644 --- a/src/test-e2e/po/page-subheader.po.ts +++ b/src/test-e2e/po/page-subheader.po.ts @@ -51,7 +51,7 @@ export class PageSubHeaderComponent extends Component { if (!suffix.startsWith('/')) { suffix = '/' + suffix; } - browser.wait(until.urlContains(browser.baseUrl + baseUrl + suffix), 20000); + browser.wait(until.urlContains(browser.baseUrl + baseUrl + suffix), 20000, `Waiting for item '${name}'`); } getItemMap(): promise.Promise { From aaa8922745423cb38029bfce171c28854c3294db Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Wed, 15 Aug 2018 18:30:27 +0100 Subject: [PATCH 17/20] Another travis typescript wonderfail --- src/test-e2e/marketplace/services-helper-e2e.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/test-e2e/marketplace/services-helper-e2e.ts b/src/test-e2e/marketplace/services-helper-e2e.ts index 5456d398b1..d1ea11fc51 100644 --- a/src/test-e2e/marketplace/services-helper-e2e.ts +++ b/src/test-e2e/marketplace/services-helper-e2e.ts @@ -132,9 +132,7 @@ export class ServicesHelperE2E { if (serviceInstance) { return this.deleteServiceInstance(cfGuid, serviceInstance.metadata.guid); } - const p = promise.defer(); - p.fulfill(createEmptyCfResponse()); - return p; + return promise.fullyResolved(createEmptyCfResponse()); }); } From 1c6902c2b17011f29483c1b11fbdcf18600fb8e1 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Wed, 15 Aug 2018 19:09:23 +0100 Subject: [PATCH 18/20] Remove fdescribe, prodding travis --- .../cloud-foundry/space-level/cf-space-level-e2e.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test-e2e/cloud-foundry/space-level/cf-space-level-e2e.spec.ts b/src/test-e2e/cloud-foundry/space-level/cf-space-level-e2e.spec.ts index bcdf1d5043..98658e85fe 100644 --- a/src/test-e2e/cloud-foundry/space-level/cf-space-level-e2e.spec.ts +++ b/src/test-e2e/cloud-foundry/space-level/cf-space-level-e2e.spec.ts @@ -75,7 +75,7 @@ describe('CF - Space Level -', () => { }); - fdescribe('As User -', () => { + describe('As User -', () => { beforeEach(() => { setup(ConsoleUserType.user); }); From e8ed11fe6f3bac5198af8723e539d3c4161635e3 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Thu, 16 Aug 2018 14:40:31 +0100 Subject: [PATCH 19/20] Updates following review --- .../application/application-e2e-helpers.ts | 6 +++--- src/test-e2e/application/application-summary.po.ts | 2 -- src/test-e2e/helpers/cf-helpers.ts | 8 +++----- src/test-e2e/helpers/cf-request-helpers.ts | 14 ++++++++------ 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/test-e2e/application/application-e2e-helpers.ts b/src/test-e2e/application/application-e2e-helpers.ts index 9830fccb83..82df7b56e2 100644 --- a/src/test-e2e/application/application-e2e-helpers.ts +++ b/src/test-e2e/application/application-e2e-helpers.ts @@ -49,7 +49,7 @@ export class ApplicationE2eHelper { } fetchApp = (cfGuid: string, spaceGuid: string, appName: string): promise.Promise> => { - return this.cfHelper.baseFetchApp(cfGuid, spaceGuid, appName).then(json => { + return this.cfHelper.basicFetchApp(cfGuid, spaceGuid, appName).then(json => { if (json.total_results < 1) { return null; } else if (json.total_results === 1) { @@ -152,7 +152,7 @@ export class ApplicationE2eHelper { const cfRequestHelper = this.cfRequestHelper; // Delete app - return deps.then(() => this.cfHelper.baseDeleteApp(cfGuid, app.metadata.guid)).then(() => { + return deps.then(() => this.cfHelper.basicDeleteApp(cfGuid, app.metadata.guid)).then(() => { e2e.log(`'${app.entity.name}': Successfully deleted.`); }); }) @@ -168,7 +168,7 @@ export class ApplicationE2eHelper { .then(space => { expect(space).not.toBeNull(); return promise.all([ - this.cfHelper.baseCreateApp(cfGuid, space.metadata.guid, appName), + this.cfHelper.basicCreateApp(cfGuid, space.metadata.guid, appName), promise.fullyResolved(space) ]); }) diff --git a/src/test-e2e/application/application-summary.po.ts b/src/test-e2e/application/application-summary.po.ts index e793098cb7..6c9fe96b34 100644 --- a/src/test-e2e/application/application-summary.po.ts +++ b/src/test-e2e/application/application-summary.po.ts @@ -1,6 +1,5 @@ import { browser, promise } from 'protractor'; -import { e2e } from '../e2e'; import { Page } from '../po/page.po'; import { DeleteApplication } from './delete-app.po'; @@ -22,7 +21,6 @@ export class ApplicationSummary extends Page { expect(urlParts[3]).toBe('summary'); const cfGuid = urlParts[1]; const appGuid = urlParts[2]; - e2e.log(`Creating App Summary object using cfGuid: '${cfGuid}' and appGuid: '${appGuid}'`); return new ApplicationSummary(cfGuid, appGuid); }); } diff --git a/src/test-e2e/helpers/cf-helpers.ts b/src/test-e2e/helpers/cf-helpers.ts index e910ae8b4f..c162f02d58 100644 --- a/src/test-e2e/helpers/cf-helpers.ts +++ b/src/test-e2e/helpers/cf-helpers.ts @@ -142,26 +142,24 @@ export class CFHelpers { } fetchAppsCountInSpace(cnsiGuid: string, spaceGuid: string) { - console.log(cnsiGuid, spaceGuid); return this.cfRequestHelper.sendCfGet(cnsiGuid, `spaces/${spaceGuid}/apps`).then(json => { - console.log(json.total_results); return json.total_results; }); } // For fully fleshed out fetch see application-e2e-helpers - baseFetchApp(cnsiGuid: string, spaceGuid: string, appName: string) { + basicFetchApp(cnsiGuid: string, spaceGuid: string, appName: string) { return this.cfRequestHelper.sendCfGet(cnsiGuid, `apps?inline-relations-depth=1&include-relations=routes,service_bindings&q=name IN ${appName},space_guid IN ${spaceGuid}`); } // For fully fleshed our create see application-e2e-helpers - baseCreateApp(cnsiGuid: string, spaceGuid: string, appName: string) { + basicCreateApp(cnsiGuid: string, spaceGuid: string, appName: string) { return this.cfRequestHelper.sendCfPost(cnsiGuid, 'apps', { name: appName, space_guid: spaceGuid }); } // For fully fleshed out delete see application-e2e-helpers (includes route and service instance deletion) - baseDeleteApp(cnsiGuid: string, appGuid: string) { + basicDeleteApp(cnsiGuid: string, appGuid: string) { return this.cfRequestHelper.sendCfDelete(cnsiGuid, 'apps/' + appGuid); } diff --git a/src/test-e2e/helpers/cf-request-helpers.ts b/src/test-e2e/helpers/cf-request-helpers.ts index 170a364433..e7d227ec01 100644 --- a/src/test-e2e/helpers/cf-request-helpers.ts +++ b/src/test-e2e/helpers/cf-request-helpers.ts @@ -28,18 +28,20 @@ export class CFRequestHelpers extends RequestHelpers { }); } - sendCfGet(cfGuid: string, url: string): promise.Promise { + getCfGuid = (cfName?: string): promise.Promise => + this.getCfInfo(cfName).then((endpoint: EndpointModel) => endpoint ? endpoint.guid : null) + + sendCfGet(cfGuid: string, url: string): promise.Promise { return this.sendCfRequest(cfGuid, url, 'GET').then(JSON.parse); } - sendCfPost(cfGuid: string, url: string, body: any): promise.Promise { + sendCfPost(cfGuid: string, url: string, body: any): promise.Promise { return this.sendCfRequest(cfGuid, url, 'POST', body).then(JSON.parse); } - getCfGuid = (cfName?: string): promise.Promise => - this.getCfInfo(cfName).then((endpoint: EndpointModel) => endpoint ? endpoint.guid : null) - sendCfPut = (cfGuid: string, url: string, body?: any): promise.Promise => - this.sendCfRequest(cfGuid, url, 'PUT', body).then(JSON.parse) + sendCfPut(cfGuid: string, url: string, body?: any): promise.Promise { + return this.sendCfRequest(cfGuid, url, 'PUT', body).then(JSON.parse); + } sendCfDelete = (cfGuid: string, url: string): promise.Promise => this.sendCfRequest(cfGuid, url, 'DELETE'); From 2bddc71f4dd6a6afad8f19b954c1ac05583a6992 Mon Sep 17 00:00:00 2001 From: Richard Cox Date: Thu, 16 Aug 2018 15:53:43 +0100 Subject: [PATCH 20/20] Allow a longer time for all before, after, it and space in between This matches jasmine's defaultTimeoutInterval of 30000 Docs for allScriptsTimeout * The timeout in milliseconds for each script run on the browser. This * should be longer than the maximum time your application needs to * stabilize between tasks. --- protractor.conf.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protractor.conf.js b/protractor.conf.js index dc5824ff71..765a3fb99c 100644 --- a/protractor.conf.js +++ b/protractor.conf.js @@ -36,7 +36,7 @@ try { } exports.config = { - allScriptsTimeout: 11000, + allScriptsTimeout: 30000, specs: [ './src/test-e2e/**/*-e2e.spec.ts', ],