From 6a8fe2e4315cffb10bc83700ed41927ff4d8f16d Mon Sep 17 00:00:00 2001 From: Nick Phura Date: Thu, 5 Apr 2018 16:11:07 -0700 Subject: [PATCH] EM-676: Add functional test fixtures & automated local execution. --- Jenkinsfile.dev | 111 +++---- config/assets/functional.js | 88 ++++++ config/env/default.js | 12 +- config/env/development.js | 2 + config/env/functional.js | 67 ++++ config/env/test.js | 2 + config/lib/mongoose.js | 39 ++- config/lib/seed.js | 3 - config/seed-data/test-integration.js | 11 - functional-tests/build.gradle | 3 + .../groovy/fixtures/FixtureManager.groovy | 198 ++++++++++++ .../src/test/groovy/specs/LoginSpec.groovy | 19 +- .../groovy/specs/base/LoggedInSpec.groovy | 20 +- .../src/test/resources/GebConfig.groovy | 2 +- gruntfile.js | 293 ++++++++++++++---- package.json | 1 + 16 files changed, 720 insertions(+), 151 deletions(-) create mode 100644 config/assets/functional.js create mode 100644 config/env/functional.js delete mode 100644 config/seed-data/test-integration.js create mode 100644 functional-tests/src/test/groovy/fixtures/FixtureManager.groovy diff --git a/Jenkinsfile.dev b/Jenkinsfile.dev index 2d8b503b5..b3b5762ae 100644 --- a/Jenkinsfile.dev +++ b/Jenkinsfile.dev @@ -14,7 +14,7 @@ def notifySlack(text, url, channel, attachments) { attachments: attachments ]) def encodedReq = URLEncoder.encode(payload, "UTF-8") - sh("curl -s -S -X POST --data \'payload=${encodedReq}\' ${slackURL}") + sh("curl -s -S -X POST --data \'payload=${encodedReq}\' ${slackURL}") } /* @@ -30,7 +30,7 @@ def buildsSinceLastSuccess(previousBuild, build) { } /* - * Generates a string containing all the commit messages from + * Generates a string containing all the commit messages from * the builds in pastBuilds. */ @NonCPS @@ -103,7 +103,7 @@ podTemplate(label: 'generic-maven', name: 'generic-maven', serviceAccount: 'jenk } } } -} +} podTemplate(label: 'sonarqube-maven', name: 'sonarqube-maven', serviceAccount: 'jenkins', cloud: 'openshift', containers: [ containerTemplate( @@ -136,7 +136,7 @@ podTemplate(label: 'sonarqube-maven', name: 'sonarqube-maven', serviceAccount: ' } } } -} +} podTemplate(label: 'generic-maven', name: 'generic-maven', serviceAccount: 'jenkins', cloud: 'openshift', containers: [ containerTemplate( @@ -184,57 +184,58 @@ podTemplate(label: 'generic-maven', name: 'generic-maven', serviceAccount: 'jenk } } -podTemplate(label: 'bddstack', name: 'bddstack', serviceAccount: 'jenkins', cloud: 'openshift', containers: [ - containerTemplate( - name: 'jnlp', - image: '172.50.0.2:5000/openshift/jenkins-slave-bddstack', - resourceRequestCpu: '500m', - resourceLimitCpu: '1000m', - resourceRequestMemory: '1Gi', - resourceLimitMemory: '4Gi', - workingDir: '/home/jenkins', - command: '', - args: '${computer.jnlpmac} ${computer.name}', - envVars: [ - secretEnvVar(key: 'ADMINPW', secretName: 'esm-server-secrets', secretKey: 'dev-admin-pwd') - ] - ) -]) -{ - stage('FT on Dev') { - node('bddstack') { - //the checkout is mandatory, otherwise functional test would fail - echo "checking out source" - echo "Build: ${BUILD_ID}" - checkout scm - dir('functional-tests') { - try { - sh './gradlew chromeHeadlessTest' - } finally { - archiveArtifacts allowEmptyArchive: true, artifacts: 'build/reports/geb/**/*' - junit 'build/test-results/**/*.xml' - publishHTML (target: [ - allowMissing: false, - alwaysLinkToLastBuild: false, - keepAll: true, - reportDir: 'build/reports/spock', - reportFiles: 'index.html', - reportName: "BDD Spock Report" - ]) - publishHTML (target: [ - allowMissing: false, - alwaysLinkToLastBuild: false, - keepAll: true, - reportDir: 'build/reports/tests/chromeHeadlessTest', - reportFiles: 'index.html', - reportName: "Full Test Report" - ]) - perfReport compareBuildPrevious: true, excludeResponseTime: true, ignoreFailedBuilds: true, ignoreUnstableBuilds: true, modeEvaluation: true, modePerformancePerTestCase: true, percentiles: '0,50,90,100', relativeFailedThresholdNegative: 80.0, relativeFailedThresholdPositive: 20.0, relativeUnstableThresholdNegative: 50.0, relativeUnstableThresholdPositive: 50.0, sourceDataFiles: 'build/test-results/**/*.xml' - } - } - } - } -} +// TODO: activate and verify once the new bddstack jenkins slave image is available +// podTemplate(label: 'bddstack', name: 'bddstack', serviceAccount: 'jenkins', cloud: 'openshift', containers: [ +// containerTemplate( +// name: 'jnlp', +// image: '172.50.0.2:5000/openshift/jenkins-slave-bddstack', +// resourceRequestCpu: '500m', +// resourceLimitCpu: '1000m', +// resourceRequestMemory: '1Gi', +// resourceLimitMemory: '4Gi', +// workingDir: '/home/jenkins', +// command: '', +// args: '${computer.jnlpmac} ${computer.name}', +// envVars: [ +// secretEnvVar(key: 'ADMINPW', secretName: 'esm-server-secrets', secretKey: 'dev-admin-pwd') +// ] +// ) +// ]) +// { +// stage('FT on Dev') { +// node('bddstack') { +// //the checkout is mandatory, otherwise functional test would fail +// echo "checking out source" +// echo "Build: ${BUILD_ID}" +// checkout scm +// dir('functional-tests') { +// try { +// sh './gradlew chromeHeadlessTest' +// } finally { +// archiveArtifacts allowEmptyArchive: true, artifacts: 'build/reports/geb/**/*' +// junit 'build/test-results/**/*.xml' +// publishHTML (target: [ +// allowMissing: false, +// alwaysLinkToLastBuild: false, +// keepAll: true, +// reportDir: 'build/reports/spock', +// reportFiles: 'index.html', +// reportName: "BDD Spock Report" +// ]) +// publishHTML (target: [ +// allowMissing: false, +// alwaysLinkToLastBuild: false, +// keepAll: true, +// reportDir: 'build/reports/tests/chromeHeadlessTest', +// reportFiles: 'index.html', +// reportName: "Full Test Report" +// ]) +// perfReport compareBuildPrevious: true, excludeResponseTime: true, ignoreFailedBuilds: true, ignoreUnstableBuilds: true, modeEvaluation: true, modePerformancePerTestCase: true, percentiles: '0,50,90,100', relativeFailedThresholdNegative: 80.0, relativeFailedThresholdPositive: 20.0, relativeUnstableThresholdNegative: 50.0, relativeUnstableThresholdPositive: 50.0, sourceDataFiles: 'build/test-results/**/*.xml' +// } +// } +// } +// } +// } podTemplate(label: 'owasp-zap', name: 'owasp-zap', serviceAccount: 'jenkins', cloud: 'openshift', containers: [ containerTemplate( diff --git a/config/assets/functional.js b/config/assets/functional.js new file mode 100644 index 000000000..726f403cc --- /dev/null +++ b/config/assets/functional.js @@ -0,0 +1,88 @@ +'use strict'; + +module.exports = { + client: { + lib: { + css: [ + 'public/lib/bootstrap/dist/css/bootstrap.css', + 'public/lib/angular-toastr/dist/angular-toastr.css', + 'public/lib/angular-bootstrap/ui-bootstrap-csp.css', + 'public/assimilated/ng-table/dist/ng-table.min.css' + ], + js: [ + 'public/lib/es6-shim/es6-shim.min.js', + 'public/lib/angular/angular.js', + 'public/lib/angular-resource/angular-resource.js', + 'public/lib/angular-animate/angular-animate.js', + 'public/lib/angular-messages/angular-messages.js', + 'public/lib/angular-ui-router/release/angular-ui-router.js', + 'public/lib/angular-ui-utils/ui-utils.js', + 'public/lib/tinymce/tinymce.js', + 'public/lib/angular-ui-tinymce/src/tinymce.js', + 'public/lib/jquery/dist/jquery.min.js', + 'public/lib/bootstrap/dist/js/bootstrap.min.js', + 'public/lib/angular-bootstrap/ui-bootstrap.js', + 'public/lib/angular-bootstrap/ui-bootstrap-tpls.js', + 'public/lib/angular-file-upload/angular-file-upload.js', + 'public/lib/ng-file-upload/ng-file-upload.js', + 'public/lib/angular-sanitize/angular-sanitize.js', + 'public/lib/angular-toastr/dist/angular-toastr.js', + 'public/lib/angular-toastr/dist/angular-toastr.tpls.js', + 'public/lib/angular-bootstrap-confirm/dist/angular-bootstrap-confirm.min.js', + 'public/lib/lodash/lodash.min.js', + 'public/lib/angular-simple-logger/dist/angular-simple-logger.js', + 'public/lib/angular-google-maps/dist/angular-google-maps.min.js', + 'public/lib/d3/d3.min.js', + 'public/lib/angularD3/dist/angularD3.js', + 'public/assimilated/ng-table/dist/ng-table.js', + 'public/lib/moment/moment.js', + 'public/lib/moment-timezone/moment-timezone.js', + 'public/lib/angular-moment/angular-moment.js', + 'public/lib/angular-scroll/angular-scroll.min.js', + 'public/lib/angular-cookies/angular-cookies.min.js', + 'public/moment-timezone-data.js', + 'public/readable-range.js', + 'public/lib/pdfjs-dist/build/pdf.combined.js', + 'public/lib/ng-pdfviewer/ng-pdfviewer.js', + 'public/lib/ng-pdfviewer/pdf.js', + 'public/lib/ng-pdfviewer/compatibility.js' + ], + tests: ['public/lib/angular-mocks/angular-mocks.js'] + }, + css: ['modules/*/client/css/*.css'], + less: ['modules/*/client/less/*.less'], + sass: ['modules/*/client/scss/*.scss'], + js: [ + 'modules/core/client/app/config.js', + 'modules/core/client/app/init.js', + 'modules/*/client/*.js', + 'modules/*/client/**/*.js', + 'modules/*/processes/*/client/*.js', + 'modules/*/processes/*/client/**/*.js', + 'modules/*/controls/*/client/*.js', + 'modules/*/controls/*/client/**/*.js' + ], + views: [ + 'modules/*/client/views/**/*.html', + 'modules/*/processes/*/client/views/**/*.html', + 'modules/*/controls/*/client/views/**/*.html' + ], + templates: ['build/templates.js'] + }, + server: { + gruntConfig: 'gruntfile.js', + allJS: ['server.js', 'config/**/*.js', 'modules/*/server/**/*.js'], + models: [ + 'modules/*/server/models/**/*.js', + 'modules/*/processes/*/server/models/**/*.js' + ], + routes: [ + 'modules/!(core)/server/routes/**/*.js', + 'modules/core/server/routes/**/*.js' + ], + sockets: 'modules/*/server/sockets/**/*.js', + config: 'modules/*/server/config/*.js', + policies: 'modules/*/server/policies/*.js', + views: 'modules/*/server/views/*.html' + } +}; diff --git a/config/env/default.js b/config/env/default.js index 657d590fb..f78143730 100644 --- a/config/env/default.js +++ b/config/env/default.js @@ -8,13 +8,20 @@ module.exports = { googleAnalyticsTrackingID: process.env.GOOGLE_ANALYTICS_TRACKING_ID || 'GOOGLE_ANALYTICS_TRACKING_ID' }, port: process.env.PORT || 3000, + token: { + tokenQuery: process.env.TOKEN_QUERY || 'smgov_userguid', + tokenParams: process.env.TOKEN_PARAMS || undefined, + tokenField: process.env.TOKEN_FIELD || undefined, + tokenHeader: process.env.TOKEN_HEADER || 'smgov_userguid', + failedOnMissing: process.env.TOKEN_ON_MISSING || false + }, templateEngine: 'swig', // Session Cookie settings sessionCookie: { // session expiration is set by default to 24 hours maxAge: 24 * (60 * 60 * 1000), // httpOnly flag makes sure the cookie is only accessed - // through the HTTP protocol and not JS/browser + // through the HTTP protocol and not JS/browser httpOnly: true, // secure cookie should be turned to true to provide additional // layer of security so that the cookie is set only when working @@ -30,6 +37,3 @@ module.exports = { logo: 'modules/core/client/img/brand/eao-banner-img-lg.png', favicon: 'modules/core/client/img/brand/favicon.ico' }; - -console.log(module.exports.app); - diff --git a/config/env/development.js b/config/env/development.js index 0360be2be..fb6a01bed 100644 --- a/config/env/development.js +++ b/config/env/development.js @@ -72,3 +72,5 @@ module.exports = { livereload: true, seedDB: process.env.MONGO_SEED || true }; + +console.log(module.exports.app); diff --git a/config/env/functional.js b/config/env/functional.js new file mode 100644 index 000000000..d4db541dc --- /dev/null +++ b/config/env/functional.js @@ -0,0 +1,67 @@ +'use strict'; + +var defaultEnvConfig = require('./default'); + +module.exports = { + db: { + uri: process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://' + (process.env.DB_1_PORT_27017_TCP_ADDR || 'localhost') + '/' + (process.env.MONGODB_FUNC_DATABASE || 'esm-dev-func'), + acluri: process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://' + (process.env.DB_1_PORT_27017_TCP_ADDR || 'localhost') + '/esm-acl-test', + name: process.env.MONGODB_FUNC_DATABASE || 'esm-dev-func', + options: { + user: '', + pass: '' + }, + // Enable mongoose debug mode + debug: process.env.MONGODB_DEBUG || false + }, + port: process.env.FUNCTIONAL_PORT || 3001, + app: { + title: defaultEnvConfig.app.title + ' - Functional Test Environment' + }, + facebook: { + clientID: process.env.FACEBOOK_ID || 'APP_ID', + clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET', + callbackURL: '/api/auth/facebook/callback' + }, + twitter: { + clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY', + clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET', + callbackURL: '/api/auth/twitter/callback' + }, + google: { + clientID: process.env.GOOGLE_ID || 'APP_ID', + clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET', + callbackURL: '/api/auth/google/callback' + }, + linkedin: { + clientID: process.env.LINKEDIN_ID || 'APP_ID', + clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET', + callbackURL: '/api/auth/linkedin/callback' + }, + github: { + clientID: process.env.GITHUB_ID || 'APP_ID', + clientSecret: process.env.GITHUB_SECRET || 'APP_SECRET', + callbackURL: '/api/auth/github/callback' + }, + paypal: { + clientID: process.env.PAYPAL_ID || 'CLIENT_ID', + clientSecret: process.env.PAYPAL_SECRET || 'CLIENT_SECRET', + callbackURL: '/api/auth/paypal/callback', + sandbox: true + }, + mailer: { + from: process.env.MAILER_FROM || 'MAILER_FROM', + options: { + service: process.env.MAILER_SERVICE_PROVIDER || 'MAILER_SERVICE_PROVIDER', + auth: { + user: process.env.MAILER_EMAIL_ID || 'MAILER_EMAIL_ID', + pass: process.env.MAILER_PASSWORD || 'MAILER_PASSWORD' + } + } + }, + + seedDB: process.env.MONGO_SEED || true + +}; + +console.log(module.exports.app); diff --git a/config/env/test.js b/config/env/test.js index 14817d3bd..68b8612b1 100644 --- a/config/env/test.js +++ b/config/env/test.js @@ -60,3 +60,5 @@ module.exports = { }, seedDB: process.env.MONGO_SEED || true }; + +console.log(module.exports.app); diff --git a/config/lib/mongoose.js b/config/lib/mongoose.js index 8108b427f..c1ad61e3a 100644 --- a/config/lib/mongoose.js +++ b/config/lib/mongoose.js @@ -18,7 +18,7 @@ module.exports.loadModels = function () { }; // Initialize Mongoose -module.exports.connect = function (cb) { +module.exports.connect = function (callback) { var _this = this; var db = mongoose.connect(config.db.uri, config.db.options, function (err) { @@ -32,14 +32,45 @@ module.exports.connect = function (cb) { mongoose.set('debug', config.db.debug); // Call callback FN - if (cb) cb(db); + if (callback) callback(db); } }); }; -module.exports.disconnect = function (cb) { +module.exports.disconnect = function (callback) { mongoose.disconnect(function (err) { console.info(chalk.yellow('Disconnected from MongoDB.')); - cb(err); + callback(err); }); }; + +module.exports.dropDatabase = function(callback) { + console.log(chalk.white('Mongoose drop database...'));//eslint-disable-line + + var MongoClient = require('mongodb').MongoClient; + + console.log(chalk.white("Connecting to database server ..."));//eslint-disable-line + console.log(chalk.white("config.db.uri: ", config.db.uri));//eslint-disable-line + + MongoClient.connect(config.db.uri, function(err, client) { + if(err) { + console.error(err);//eslint-disable-line + return; + } + + console.log("Connected to database server ...");//eslint-disable-line + + var db = client.db(config.db.name); + + db.dropDatabase(function(err){ + if (err) { + throw err; + } + client.close(); + }); + + if(callback) { + callback(); + } + }); +} diff --git a/config/lib/seed.js b/config/lib/seed.js index 55125c825..eb1497692 100644 --- a/config/lib/seed.js +++ b/config/lib/seed.js @@ -73,9 +73,6 @@ var checkIntegration = function (name, override) { var seedingAsync = function() { console.log('begin asynchronous seeding...'); - checkIntegration('testme').then(function (f) { - require('../seed-data/test-integration')(f); - }); // ------------------------------------------------------------------------- // diff --git a/config/seed-data/test-integration.js b/config/seed-data/test-integration.js deleted file mode 100644 index 83421d22c..000000000 --- a/config/seed-data/test-integration.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -module.exports = function (cb) { - // - // blah blah blah do some work - // - cb (''); - cb ('this is the output'); - cb ('this is more output'); - cb ('this is the last output', true); -}; diff --git a/functional-tests/build.gradle b/functional-tests/build.gradle index b8cadc194..ede4b6f64 100644 --- a/functional-tests/build.gradle +++ b/functional-tests/build.gradle @@ -56,6 +56,9 @@ dependencies { } testCompile 'org.slf4j:slf4j-api:1.7.13' testCompile 'org.slf4j:slf4j-simple:1.7.13' + + testCompile 'org.codehaus.groovy:groovy-all:2.3.7' + testCompile 'org.mongodb:mongo-java-driver:3.6.3' } webdriverBinaries { diff --git a/functional-tests/src/test/groovy/fixtures/FixtureManager.groovy b/functional-tests/src/test/groovy/fixtures/FixtureManager.groovy new file mode 100644 index 000000000..2bc78105a --- /dev/null +++ b/functional-tests/src/test/groovy/fixtures/FixtureManager.groovy @@ -0,0 +1,198 @@ +package fixtures + +import groovy.json.JsonSlurper + +import com.mongodb.* + +class FixtureManager { + private static String fixtureDataFolderPath = new File("src/test/groovy/fixtures/data").absolutePath; + + private MongoClient client; + private DB db; + + // Default connection values + private String host = 'localhost'; + private int port = 27017; + private String databaseName = 'esm-dev-func'; + + // Stores fixture data that is inserted via this manager. + private List fixtureDataInserted = []; + + /** + * A data fixture manager. + * + * This manager handles connecting to the mongo database and contains helper methods for inserting and + * removing data fixtures. + * + * When finished with this manager, it is recommneded that you call FixtureManager#removeAll() which will + * remove any fixture data that was added by this instance of the manager. + */ + FixtureManager(final String host = null, final Integer port = null, final String databaseName = null) { + def env = System.getenv() + if(host) { + this.host = host; + } else if (env['MONGODB_FUNC_HOST']) { + this.host = env['MONGODB_FUNC_HOST']; + } + + if (port) { + this.port = port; + } else if (env['MONGODB_FUNC_PORT']) { + this.port = env['MONGODB_FUNC_PORT'] as int; + } + + if (databaseName) { + this.databaseName = databaseName; + } else if (env['MONGODB_FUNC_DATABASE'] ) { + this.databaseName = env['MONGODB_FUNC_DATABASE']; + } + + this.client = new MongoClient(this.host, this.port); + + this.db = client.getDB(this.databaseName); + } + + /** + * @return the database instance used by this manager. + */ + public getDB() { + return this.db; + } + + /** + * Gets the fixture and returns the json object. + * @param fixture file name (required). + * @return a List of the fixture data or null if no data file is found. + */ + public List getFixture(String fixtureName) { + if(!fixtureName.endsWith(".json")) { + fixtureName += ".json"; + } + + try { + def String fixtureFilePath = [fixtureDataFolderPath, fixtureName].join('/'); + def File fixtureFile = new File(fixtureFilePath); + return new JsonSlurper().parseText(fixtureFile.getText()); + } catch (FileNotFoundException e) { + println("Could not find fixture data file: ${fixtureFile.absolutePath}"); + return null; + } + } + + /** + * Inserts all fixture data contained within the matching data file, if one exists. + * @param fixtureName the name of the json data file. Specifying the ".json" file extension is optional. + */ + public insertFixture(final String fixtureName) { + insert(getFixture(fixtureName)); + } + + /** + * Inserts all fixture data contained within the passed fixtureData parameter. + * @param fixtureData a List of fixture data elements. + * + * Example: + * fixtureData = [ + * [ + * "collection": "someCollection", + * "records": [ + * [ + * "firstName": "jon", + * "lastName": "smith", + * "address": [ + * "street": "123 fake street", + * "postal": "A1A 1A1" + * ] + * ] + * ] + * ] + * ] + */ + public insertFixture(final List fixtureData) { + insert(fixtureData); + } + + /** + * Removes all fixture data contained within the matching data file, if one exists. + * @param fixtureName the name of the json data file. Specifying the ".json" file extension is optional. + */ + public removeFixture(final String fixtureName) { + remove(getFixture(fixtureName)); + } + + /** + * Removes the fixture data contained within the passed fixtureData parameter. + * @param fixtureData a List of fixture data elements. + * + * Example: + * fixtureData = [ + * [ + * "collection": "someCollection", + * "records": [ + * [ + * "firstName": "jon", + * "lastName": "smith", + * "address": [ + * "street": "123 fake street", + * "postal": "A1A 1A1" + * ] + * ] + * ] + * ] + * ] + */ + public removeFixture(final List fixtureData) { + remove(fixtureData); + } + + /** + * Removes all fixture data that was inserted via the FixtureManager#insertFixture methods and + * which has not already been removed via the FixtureManager#removeFixture methods. + */ + public removeAll() { + fixtureDataInserted.each { + item -> + item.records.each { + record -> + BasicDBObject basicObject = new BasicDBObject() + record.each { + key, value -> basicObject.put(key, value) + } + db.getCollection(item.collection).remove(basicObject, WriteConcern.SAFE) + } + } + fixtureDataInserted.clear(); + } + + private insert(final List fixtureData) { + fixtureData.each { + item -> + item.records.each { + record -> + BasicDBObject basicObject = new BasicDBObject() + record.each { + key, value -> basicObject.put(key, value) + } + db.getCollection(item.collection).insert(basicObject, WriteConcern.SAFE) + fixtureDataInserted.add([collection: item.collection, records: [record]]); + } + } + } + + private remove(final String fixtureData) { + fixtureData.each { + item -> + item.records.each { + record -> + BasicDBObject basicObject = new BasicDBObject() + record.each { + key, value -> basicObject.put(key, value) + } + db.getCollection(item.collection).remove(basicObject, WriteConcern.SAFE) + if(fixtureDataInserted.contains(item)) { + fixtureDataInserted.remove(item); + } + } + } + } +} diff --git a/functional-tests/src/test/groovy/specs/LoginSpec.groovy b/functional-tests/src/test/groovy/specs/LoginSpec.groovy index 019b3f3b0..f48f1f941 100644 --- a/functional-tests/src/test/groovy/specs/LoginSpec.groovy +++ b/functional-tests/src/test/groovy/specs/LoginSpec.groovy @@ -1,5 +1,7 @@ import geb.spock.GebReportingSpec +import fixtures.FixtureManager + import pages.app.LoginPage import pages.app.HomePage import pages.app.ForgotPasswordPage @@ -9,19 +11,32 @@ import modules.FooterModule import spock.lang.Unroll import spock.lang.Title import spock.lang.Stepwise +import spock.lang.Shared @Stepwise @Title("Functional tests for the Login page") class LoginSpec extends GebReportingSpec { + @Shared + FixtureManager fixtureManager = new FixtureManager(); + + def setupSpec() { + fixtureManager.insertFixture("adminUser"); + } + def setup() { - module(FooterModule).logout() + clearCookies(); + module(FooterModule).logout(); + } + + def cleanupSpec() { + fixtureManager.removeAll(); } def "Navigate Page from: LoginPage, log in as Admin, Assert Page: HomePage"() { given: "I start on the LoginPage and am not logged in" to LoginPage when: "I log in as Admin" - login("admin", System.getenv("ADMINPW")) + login("user-name", "password") then: "I am logged in and returned to the HomePage" at HomePage } diff --git a/functional-tests/src/test/groovy/specs/base/LoggedInSpec.groovy b/functional-tests/src/test/groovy/specs/base/LoggedInSpec.groovy index 3354a3b9e..1410965ec 100644 --- a/functional-tests/src/test/groovy/specs/base/LoggedInSpec.groovy +++ b/functional-tests/src/test/groovy/specs/base/LoggedInSpec.groovy @@ -2,29 +2,43 @@ package base import geb.spock.GebReportingSpec +import fixtures.FixtureManager + import pages.app.LoginPage import pages.app.HomePage import modules.FooterModule +import spock.lang.Shared + /** * Extend this class if the child spec requires the user to be logged in. */ abstract class LoggedInSpec extends GebReportingSpec { + @Shared + FixtureManager fixtureManager = new FixtureManager(); + /** * Runs once before all tests in a spec. */ def setupSpec() { + setupFixtures(); + to LoginPage - login("admin", System.getenv("ADMINPW")) + login("user-name", "password"); at HomePage } + def setupFixtures() { + fixtureManager.insertFixture("adminUser"); + } + /** * Runs once after all tests in a spec. */ def cleanupSpec() { - clearCookies() - module(FooterModule).logout() + clearCookies(); + module(FooterModule).logout(); + fixtureManager.removeAll(); } } diff --git a/functional-tests/src/test/resources/GebConfig.groovy b/functional-tests/src/test/resources/GebConfig.groovy index 3600801f8..2a279f201 100644 --- a/functional-tests/src/test/resources/GebConfig.groovy +++ b/functional-tests/src/test/resources/GebConfig.groovy @@ -99,7 +99,7 @@ baseNavigatorWaiting = true def env = System.getenv() baseUrl = env['BASEURL'] if (!baseUrl) { - baseUrl = "https://esm-master.pathfinder.gov.bc.ca/" + baseUrl = "http://localhost:3001/"; } println "BaseURL: ${baseUrl}" diff --git a/gruntfile.js b/gruntfile.js index e8dac1e6e..708236eae 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -7,21 +7,22 @@ var _ = require('lodash'), defaultAssets = require('./config/assets/default'), testAssets = require('./config/assets/test'), fs = require('fs'), - path = require('path'); + path = require('path'), + childProcess = require('child_process'); -module.exports = function (grunt) { +module.exports = function(grunt) { // Project Configuration - var LOGO = ""; + var LOGO = ''; if (process.env.ENVIRONMENT === 'MEM') { LOGO = 'modules/core/client/img/brand/mem-logo-inverted.png'; // EAO Logo } else { LOGO = 'modules/core/client/img/brand/eao-banner-img-lg.png'; // BC Logo } - var ENV = ""; + var ENV = ''; if (process.env.ENVIRONMENT) { ENV = process.env.ENVIRONMENT; } else { - ENV = "EAO"; + ENV = 'EAO'; } grunt.initConfig({ @@ -34,15 +35,18 @@ module.exports = function (grunt) { }, dist: { constants: { - 'ENV': ENV, - 'LOGO': LOGO + ENV: ENV, + LOGO: LOGO } } }, env: { - test: { + unit: { NODE_ENV: 'test' }, + functional: { + NODE_ENV: 'functional' + }, dev: { NODE_ENV: 'development' }, @@ -58,7 +62,10 @@ module.exports = function (grunt) { } }, serverJS: { - files: _.union(defaultAssets.server.gruntConfig, defaultAssets.server.allJS), + files: _.union( + defaultAssets.server.gruntConfig, + defaultAssets.server.allJS + ), tasks: ['eslint'], options: { livereload: true @@ -110,8 +117,8 @@ module.exports = function (grunt) { csslintrc: '.csslintrc', 'outline-none': false, 'fallback-colors': false, - 'bulletproof-font-face' : false, - 'shorthand' : false + 'bulletproof-font-face': false, + shorthand: false }, all: { src: defaultAssets.client.css @@ -143,26 +150,30 @@ module.exports = function (grunt) { }, sass: { dist: { - files: [{ - expand: true, - src: defaultAssets.client.sass, - ext: '.css', - rename: function (base, src) { - return src.replace('/scss/', '/css/'); + files: [ + { + expand: true, + src: defaultAssets.client.sass, + ext: '.css', + rename: function(base, src) { + return src.replace('/scss/', '/css/'); + } } - }] + ] } }, less: { dist: { - files: [{ - expand: true, - src: defaultAssets.client.less, - ext: '.css', - rename: function (base, src) { - return src.replace('/less/', '/css/'); + files: [ + { + expand: true, + src: defaultAssets.client.less, + ext: '.css', + rename: function(base, src) { + return src.replace('/less/', '/css/'); + } } - }] + ] } }, 'node-inspector': { @@ -174,7 +185,7 @@ module.exports = function (grunt) { 'save-live-edit': true, 'no-preload': true, 'stack-trace-limit': 50, - 'hidden': [] + hidden: [] } } }, @@ -192,7 +203,7 @@ module.exports = function (grunt) { coverage: true, require: 'test.js', coverageFolder: 'coverage', - reportFormats: ['cobertura','lcovonly'], + reportFormats: ['cobertura', 'lcovonly'], check: { lines: 40, statements: 40 @@ -221,7 +232,7 @@ module.exports = function (grunt) { localConfig: { src: 'config/env/local.example.js', dest: 'config/env/local.js', - filter: function () { + filter: function() { return !fs.existsSync('config/env/local.js'); } }, @@ -229,7 +240,7 @@ module.exports = function (grunt) { expand: true, cwd: 'node_modules/tiny-jsonrpc', src: '*', - dest: 'node_modules/spooky/node_modules/tiny-jsonrpc', + dest: 'node_modules/spooky/node_modules/tiny-jsonrpc' } } }); @@ -248,69 +259,215 @@ module.exports = function (grunt) { //grunt.loadNpmTasks('grunt-sass'); grunt.loadNpmTasks('grunt-ng-constant'); - - grunt.task.registerTask('buildconstants', 'Builds all the environment information and bakes it into a conf.js file.', function () { - grunt.task.run('ngconstant'); - }); + grunt.task.registerTask( + 'buildconstants', + 'Builds all the environment information and bakes it into a conf.js file.', + function() { + grunt.task.run('ngconstant'); + } + ); // Make sure upload directory exists - grunt.task.registerTask('mkdir:upload', 'Task that makes sure upload directory exists.', function () { - // Get the callback - var done = this.async(); + grunt.task.registerTask( + 'mkdir:upload', + 'Task that makes sure upload directory exists.', + function() { + // Get the callback + var done = this.async(); - grunt.file.mkdir(path.normalize(__dirname + '/modules/users/client/img/profile/uploads')); + grunt.file.mkdir( + path.normalize(__dirname + '/modules/users/client/img/profile/uploads') + ); - done(); - }); + done(); + } + ); // Connect to the MongoDB instance and load the models - grunt.task.registerTask('mongoose', 'Task that connects to the MongoDB instance and loads the application models.', function () { - // Get the callback - var done = this.async(); + grunt.task.registerTask( + 'mongoose', + 'Task that connects to the MongoDB instance and loads the application models.', + function() { + // Get the callback + var done = this.async(); - // Use mongoose configuration - var mongoose = require('./config/lib/mongoose.js'); + // Use mongoose configuration + var mongoose = require('./config/lib/mongoose.js'); - // Connect to database - mongoose.connect(function (db) { - done(); - }); - }); + // Connect to database + mongoose.connect(function(db) { + done(); + }); + } + ); - grunt.task.registerTask('server', 'Starting the server', function () { - // Get the callback - var done = this.async(); + /** + * Start the local functional test server. + */ + var e2e_server_process; + grunt.task.registerTask( + 'start_e2e_server', + 'Starting functional test server.', + function() { + var done = this.async(); + + e2e_server_process = childProcess.spawn('node', ['server.js'], { + env: process.env, + detached: true, + shell: false, + stdio: 'ignore' + }); - var path = require('path'); - var app = require(path.resolve('./config/lib/app')); - var server = app.start(function () { done(); - }); - }); + } + ); + + /** + * Run the functional tests against the local server/database. + */ + grunt.task.registerTask( + 'run_e2e_tests', + 'Running functional tests.', + function() { + var done = this.async(); + + /** + * To run in headful mode: + * ['chromeTest'] + * + * To run a single test: + * ['-DchromeTest.single=SomeSpecName', 'chromeTest'] + * + * To increase logging (including println): + * ['chromeHeadlessTest', '--info'] + */ + var test_process = childProcess.spawn( + process.platform == 'win32' ? 'gradlew.bat' : './gradlew', + ['chromeHeadlessTest'], + { + env: process.env, + cwd: path.join(process.cwd(), 'functional-tests'), + stdio: 'inherit' + } + ); + + test_process.on('exit', done); + } + ); + + /** + * Drop the local functional test database. + */ + grunt.task.registerTask( + 'drop_e2e_database', + 'Dropping functional test database.', + function() { + var done = this.async(); + + var mongoose = require('./config/lib/mongoose.js'); + mongoose.dropDatabase(function() { + done(); + }); + } + ); + + /** + * Stop the local functional test server. + */ + grunt.task.registerTask( + 'shutdown_e2e_server', + 'Stopping functional test server.', + function() { + if (e2e_server_process) { + e2e_server_process.kill('SIGINT'); + } + } + ); // Lint CSS and JavaScript files. grunt.registerTask('lint', ['sass', 'less', 'eslint', 'csslint']); grunt.registerTask('default', ['sass']); // Lint project files and minify them into two production files. - grunt.registerTask('build', ['env:dev', 'lint', 'ngAnnotate', 'uglify', 'cssmin', 'buildconstants']); - grunt.registerTask('buildprod', ['env:prod', 'lint', 'ngAnnotate', 'uglify', 'cssmin', 'buildconstants']); - grunt.registerTask('buildtest', ['env:test', 'lint', 'ngAnnotate', 'uglify', 'cssmin', 'buildconstants']); + grunt.registerTask('build', [ + 'env:dev', + 'lint', + 'ngAnnotate', + 'uglify', + 'cssmin', + 'buildconstants' + ]); + grunt.registerTask('buildprod', [ + 'env:prod', + 'lint', + 'ngAnnotate', + 'uglify', + 'cssmin', + 'buildconstants' + ]); + grunt.registerTask('buildtest', [ + 'env:unit', + 'lint', + 'ngAnnotate', + 'uglify', + 'cssmin', + 'buildconstants' + ]); // Run the project tests - NB: These are not maintained at the moment. - grunt.registerTask('test', 'env:test') - //grunt.registerTask('test', ['env:test', 'lint', 'mkdir:upload', 'copy:localConfig', 'copy:tinyjson', 'server', 'mochaTest', 'karma:unit']); - //grunt.registerTask('test:server', ['env:test', 'lint', 'server', 'mochaTest']); - //grunt.registerTask('test:client', ['env:test', 'lint', 'server', 'karma:unit']); + grunt.registerTask('test', 'env:unit'); + + // Run the end-to-end functional tests + grunt.registerTask('e2e', [ + 'build-e2e', + 'start_e2e_server', + 'run_e2e_tests', + 'drop_e2e_database', + 'shutdown_e2e_server' + ]); + grunt.registerTask('build-e2e', [ + 'env:functional', + 'lint', + 'ngAnnotate', + 'uglify', + 'cssmin', + 'buildconstants' + ]); + // Run project coverage - grunt.registerTask('coverage', ['env:test', 'lint', 'mocha_istanbul:coverage']); + grunt.registerTask('coverage', [ + 'env:unit', + 'lint', + 'mocha_istanbul:coverage' + ]); // Run the project in development mode - grunt.registerTask('default', ['env:dev', 'lint', 'mkdir:upload', 'copy:localConfig', 'copy:tinyjson', 'buildconstants']); + grunt.registerTask('default', [ + 'env:dev', + 'lint', + 'mkdir:upload', + 'copy:localConfig', + 'copy:tinyjson', + 'buildconstants' + ]); // Run the project in debug mode - grunt.registerTask('debug', ['env:dev', 'lint', 'mkdir:upload', 'copy:localConfig', 'copy:tinyjson', 'buildconstants']); + grunt.registerTask('debug', [ + 'env:dev', + 'lint', + 'mkdir:upload', + 'copy:localConfig', + 'copy:tinyjson', + 'buildconstants' + ]); // Run the project in production mode - grunt.registerTask('prod', ['buildprod', 'env:prod', 'mkdir:upload', 'copy:localConfig', 'copy:tinyjson', 'buildconstants']); + grunt.registerTask('prod', [ + 'buildprod', + 'env:prod', + 'mkdir:upload', + 'copy:localConfig', + 'copy:tinyjson', + 'buildconstants' + ]); }; diff --git a/package.json b/package.json index de82f2239..493508c6f 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "scripts": { "start": "grunt build && node server.js", "test": "grunt test", + "e2e": "grunt e2e", "build": "grunt buildprod", "postinstall": "git config --global url.https://.insteadOf git:// && bower install --config.interactive=false --allow-root && grunt buildprod" },