diff --git a/tests/base.conf.js b/tests/base.conf.js index c562780818f..972701017f1 100644 --- a/tests/base.conf.js +++ b/tests/base.conf.js @@ -39,6 +39,7 @@ class BaseConfig { } browser.driver.wait(setupSettings, 5 * 1000, 'Settings should be setup within 5 seconds'); + browser.driver.wait(utils.setUserContactDoc, 5 * 1000, 'User contact should be setup within 5 seconds'); browser.driver.wait(setupUser, 5 * 1000, 'User should be setup within 5 seconds'); browser.driver.sleep(1); // block until previous command has completed @@ -82,6 +83,7 @@ const setupSettings = () => { const setupUser = () => { return utils.getDoc('org.couchdb.user:' + auth.user) .then(doc => { + doc.contact_id = constants.USER_CONTACT_ID; doc.known = true; doc.language = 'en'; doc.roles = ['_admin']; diff --git a/tests/constants.js b/tests/constants.js index 7b259f26ac4..c5c5b61ac6d 100644 --- a/tests/constants.js +++ b/tests/constants.js @@ -12,6 +12,14 @@ module.exports = { // test database to avoid writing to the dev db DB_NAME: 'medic-test', - MAIN_DDOC_NAME: 'medic' + MAIN_DDOC_NAME: 'medic', + // tests create a document with this id to be referenced by org.couchdb.user contact_id + USER_CONTACT_ID: 'e2e_contact_test_id', + + DEFAULT_USER_CONTACT_DOC: { + _id: 'e2e_contact_test_id', + type: 'person', + reported_date: 1541679811408, + }, }; diff --git a/tests/e2e/forms/family-survey-form.specs.js b/tests/e2e/forms/family-survey-form.specs.js index 7cffd23fea0..5a22bedba82 100644 --- a/tests/e2e/forms/family-survey-form.specs.js +++ b/tests/e2e/forms/family-survey-form.specs.js @@ -5,13 +5,12 @@ const familyForm = require('../../page-objects/forms/family-survey-form.po'), userData = require('../../page-objects/forms/data/user.po.data'); describe('Family Survey form', () => { - const contactId = userData.contactId; - const docs = userData.docs; + const { userContactDoc, docs } = userData; beforeAll(done => { protractor.promise .all(docs.map(utils.saveDoc)) - .then(() => familyForm.configureForm(contactId, done)) + .then(() => familyForm.configureForm(userContactDoc, done)) .catch(done.fail); }); diff --git a/tests/e2e/forms/submit-delivery-form.specs.js b/tests/e2e/forms/submit-delivery-form.specs.js index 8949ca9d472..f24fdd8ce6b 100644 --- a/tests/e2e/forms/submit-delivery-form.specs.js +++ b/tests/e2e/forms/submit-delivery-form.specs.js @@ -6,15 +6,14 @@ const helper = require('../../helper'), userData = require('../../page-objects/forms/data/user.po.data'); describe('Submit Delivery Report', () => { - const contactId = userData.contactId; - const docs = userData.docs; + const { userContactDoc, docs } = userData; const noteToCHW = 'Good news, Jack! Jack () has delivered at the health facility. We will alert you when it is time to refer them for PNC. Please monitor them for danger signs. Thank you!'; beforeAll(done => { protractor.promise .all(docs.map(utils.saveDoc)) - .then(() => deliveryReport.configureForm(contactId, done)) + .then(() => deliveryReport.configureForm(userContactDoc, done)) .catch(done.fail); }); diff --git a/tests/e2e/forms/submit-photo-upload-form.spec.js b/tests/e2e/forms/submit-photo-upload-form.spec.js index cd3772d1c73..56b10b8292d 100644 --- a/tests/e2e/forms/submit-photo-upload-form.spec.js +++ b/tests/e2e/forms/submit-photo-upload-form.spec.js @@ -2,11 +2,11 @@ const helper = require('../../helper'), photoUpload = require('../../page-objects/forms/photo-upload.po'), common = require('../../page-objects/common/common.po'), utils = require('../../utils'), + constants = require('../../constants'), path = require('path'); -const contactId = 'some_contact_id'; -const doc = { - _id: contactId, +const userContactDoc = { + _id: constants.USER_CONTACT_ID, name: 'Jack', date_of_birth: '', phone: '+64274444444', @@ -21,9 +21,8 @@ const doc = { describe('Submit Photo Upload form', () => { beforeAll(done => { - utils - .saveDoc(doc) - .then(() => photoUpload.configureForm(contactId, done)) + Promise.resolve() + .then(() => photoUpload.configureForm(userContactDoc, done)) .catch(done.fail); }); diff --git a/tests/e2e/forms/submit-z-score-form.spec.js b/tests/e2e/forms/submit-z-score-form.spec.js index d8a2109136d..5fdb05c4747 100644 --- a/tests/e2e/forms/submit-z-score-form.spec.js +++ b/tests/e2e/forms/submit-z-score-form.spec.js @@ -1,10 +1,10 @@ const helper = require('../../helper'), ZScoreForm = require('../../page-objects/forms/z-score.po'), + constants = require('../../constants'), utils = require('../../utils'); -const contactId = 'some_contact_id'; -const doc = { - _id: contactId, +const userContactDoc = { + _id: constants.USER_CONTACT_ID, name: 'Jack', date_of_birth: '', phone: '+64274444444', @@ -19,9 +19,8 @@ const doc = { describe('Submit Z-Score form', () => { beforeAll(done => { - utils - .saveDoc(doc) - .then(() => ZScoreForm.configureForm(contactId, done)) + Promise.resolve() + .then(() => ZScoreForm.configureForm(userContactDoc, done)) .catch(done.fail); }); diff --git a/tests/e2e/submit-enketo-form.js b/tests/e2e/submit-enketo-form.js index 68cdeb519da..c75b222e7b9 100644 --- a/tests/e2e/submit-enketo-form.js +++ b/tests/e2e/submit-enketo-form.js @@ -1,5 +1,6 @@ const utils = require('../utils'), helper = require('../helper'), + constants = require('../constants'), commonElements = require('../page-objects/common/common.po.js'); describe('Submit Enketo form', () => { @@ -22,7 +23,7 @@ describe('Submit Enketo form', () => { `; - const contactId = '3b3d50d275280d2568cd36281d00348b'; + const contactId = constants.USER_CONTACT_ID; const docs = [ { @@ -55,38 +56,39 @@ describe('Submit Enketo form', () => { external_id: '', type: 'district_hospital', }, - { - _id: contactId, - name: 'Jack', - date_of_birth: '', - phone: '+64274444444', - alternate_phone: '', + ]; + + const userContactDoc = { + _id: contactId, + name: 'Jack', + date_of_birth: '', + phone: '+64274444444', + alternate_phone: '', + notes: '', + type: 'person', + reported_date: 1478469976421, + parent: { + _id: 'c49385b3594af7025ef097114104ef48', + reported_date: 1469578114543, notes: '', - type: 'person', - reported_date: 1478469976421, - parent: { - _id: 'c49385b3594af7025ef097114104ef48', - reported_date: 1469578114543, + contact: { + _id: contactId, + name: 'Jack', + date_of_birth: '', + phone: '+64274444444', + alternate_phone: '', notes: '', - contact: { - _id: contactId, - name: 'Jack', - date_of_birth: '', - phone: '+64274444444', - alternate_phone: '', - notes: '', - type: 'person', - reported_date: 1478469976421, - }, - name: 'Number three district', - external_id: '', - type: 'district_hospital', + type: 'person', + reported_date: 1478469976421, }, + name: 'Number three district', + external_id: '', + type: 'district_hospital', }, - ]; + }; beforeAll(done => { - utils.seedTestData(done, contactId, docs); + utils.seedTestData(done, userContactDoc, docs); }); afterEach(utils.afterEach); diff --git a/tests/page-objects/forms/data/user.po.data.js b/tests/page-objects/forms/data/user.po.data.js index 9751f43e1a6..721dfeb1aca 100644 --- a/tests/page-objects/forms/data/user.po.data.js +++ b/tests/page-objects/forms/data/user.po.data.js @@ -1,12 +1,39 @@ +const constants = require('../../../constants'); + module.exports = { - contactId: '3b3d50d275280d2568cd36281d00348b', - docs: [ - { + docs: [{ + _id: 'c49385b3594af7025ef097114104ef48', + reported_date: 1469578114543, + notes: '', + contact: { + _id: constants.USER_CONTACT_ID, + name: 'Jack', + date_of_birth: '', + phone: '+64274444444', + alternate_phone: '', + notes: '', + type: 'person', + reported_date: 1478469976421, + }, + name: 'Number three district', + external_id: '', + type: 'district_hospital', + }], + userContactDoc: { + _id: constants.USER_CONTACT_ID, + name: 'Jack', + date_of_birth: '', + phone: '+64274444444', + alternate_phone: '', + notes: '', + type: 'person', + reported_date: 1478469976421, + parent: { _id: 'c49385b3594af7025ef097114104ef48', reported_date: 1469578114543, notes: '', contact: { - _id: '3b3d50d275280d2568cd36281d00348b', + _id: constants.USER_CONTACT_ID, name: 'Jack', date_of_birth: '', phone: '+64274444444', @@ -19,33 +46,5 @@ module.exports = { external_id: '', type: 'district_hospital', }, - { - _id: '3b3d50d275280d2568cd36281d00348b', - name: 'Jack', - date_of_birth: '', - phone: '+64274444444', - alternate_phone: '', - notes: '', - type: 'person', - reported_date: 1478469976421, - parent: { - _id: 'c49385b3594af7025ef097114104ef48', - reported_date: 1469578114543, - notes: '', - contact: { - _id: '3b3d50d275280d2568cd36281d00348b', - name: 'Jack', - date_of_birth: '', - phone: '+64274444444', - alternate_phone: '', - notes: '', - type: 'person', - reported_date: 1478469976421, - }, - name: 'Number three district', - external_id: '', - type: 'district_hospital', - }, - }, - ], + }, }; diff --git a/tests/page-objects/forms/delivery-report.po.js b/tests/page-objects/forms/delivery-report.po.js index efcef6bb6de..c211768c6ff 100644 --- a/tests/page-objects/forms/delivery-report.po.js +++ b/tests/page-objects/forms/delivery-report.po.js @@ -25,8 +25,8 @@ const selectRadioButton = value => { }; module.exports = { - configureForm: (contactId, done) => { - utils.seedTestData(done, contactId, docs); + configureForm: (userContactDoc, done) => { + utils.seedTestData(done, userContactDoc, docs); }, //patient page diff --git a/tests/page-objects/forms/family-survey-form.po.js b/tests/page-objects/forms/family-survey-form.po.js index 0031c3811c4..fa857028ae3 100644 --- a/tests/page-objects/forms/family-survey-form.po.js +++ b/tests/page-objects/forms/family-survey-form.po.js @@ -19,8 +19,8 @@ const docs = [ ]; module.exports = { - configureForm: (contactId, done) => { - utils.seedTestData(done, contactId, docs); + configureForm: (userContactDoc, done) => { + utils.seedTestData(done, userContactDoc, docs); }, fillFamilySurvey: (pregnant, numberOfChildren) => { diff --git a/tests/page-objects/forms/photo-upload.po.js b/tests/page-objects/forms/photo-upload.po.js index 8be6f789dc8..bb093faefb7 100644 --- a/tests/page-objects/forms/photo-upload.po.js +++ b/tests/page-objects/forms/photo-upload.po.js @@ -44,8 +44,8 @@ const docs = [ }]; module.exports = { - configureForm: (contactId, done) => { - utils.seedTestData(done, contactId, docs); + configureForm: (userContactDoc, done) => { + utils.seedTestData(done, userContactDoc, docs); }, submit: () => { diff --git a/tests/page-objects/forms/z-score.po.js b/tests/page-objects/forms/z-score.po.js index 9fc8bc95ef8..04287b7e3e5 100644 --- a/tests/page-objects/forms/z-score.po.js +++ b/tests/page-objects/forms/z-score.po.js @@ -142,8 +142,8 @@ const clickAndGetValue = el => { }; module.exports = { - configureForm: (contactId, done) => { - utils.seedTestData(done, contactId, docs); + configureForm: (userContactDoc, done) => { + utils.seedTestData(done, userContactDoc, docs); }, load: () => { diff --git a/tests/utils.js b/tests/utils.js index 2bf09fe775b..b8e3333fff6 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -3,8 +3,7 @@ const _ = require('underscore'), constants = require('./constants'), http = require('http'), path = require('path'), - htmlScreenshotReporter = require('protractor-jasmine2-screenshot-reporter'), - userSettingsDocId = `org.couchdb.user:${auth.user}`; + htmlScreenshotReporter = require('protractor-jasmine2-screenshot-reporter'); const PouchDB = require('pouchdb-core'); PouchDB.plugin(require('pouchdb-adapter-http')); @@ -157,6 +156,7 @@ const deleteAll = (except = []) => { doc.type ), 'appcache', + constants.USER_CONTACT_ID, 'migration-log', 'resources', 'branding', @@ -237,6 +237,28 @@ const refreshToGetNewSettings = () => { }); }; +const setUserContactDoc = () => { + const { + DB_NAME: dbName, + USER_CONTACT_ID: docId, + DEFAULT_USER_CONTACT_DOC: defaultDoc + } = constants; + + return module.exports.getDoc(docId) + .catch(() => ({})) + .then(existing => { + const rev = _.pick(existing, '_rev'); + return _.extend(defaultDoc, rev); + }) + .then(newDoc => request({ + path: `/${dbName}/${docId}`, + body: JSON.stringify(newDoc), + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + })); +}; + + const revertDb = (except, ignoreRefresh) => { return revertSettings().then(needsRefresh => { return deleteAll(except).then(() => { @@ -244,7 +266,7 @@ const revertDb = (except, ignoreRefresh) => { if (!ignoreRefresh && needsRefresh) { return refreshToGetNewSettings(); } - }); + }).then(setUserContactDoc); }); }; @@ -379,6 +401,11 @@ module.exports = { */ deleteAllDocs: deleteAll, + /* + * Sets the document referenced by the user's org.couchdb.user document to a default value + */ + setUserContactDoc, + /** * Update settings and refresh if required * @@ -407,13 +434,15 @@ module.exports = { } }), - seedTestData: (done, contactId, documents) => { + seedTestData: (done, userContactDoc, documents) => { protractor.promise .all(documents.map(module.exports.saveDoc)) - .then(() => module.exports.getDoc(userSettingsDocId)) - .then(user => { - user.contact_id = contactId; - return module.exports.saveDoc(user); + .then(() => module.exports.getDoc(constants.USER_CONTACT_ID)) + .then(existingContactDoc => { + if (userContactDoc) { + _.extend(existingContactDoc, userContactDoc); + return module.exports.saveDoc(existingContactDoc); + } }) .then(done) .catch(done.fail); diff --git a/webapp/src/js/services/session.js b/webapp/src/js/services/session.js index 0b4e56f9eb7..a53c6a47034 100644 --- a/webapp/src/js/services/session.js +++ b/webapp/src/js/services/session.js @@ -19,8 +19,13 @@ var COOKIE_NAME = 'userCtx', 'ngInject'; + let userCtxCookieValue; var getUserCtx = function() { - return ipCookie(COOKIE_NAME); + if (!userCtxCookieValue) { + userCtxCookieValue = ipCookie(COOKIE_NAME); + } + + return userCtxCookieValue; }; var waitForAppCache = function(callback) { @@ -34,6 +39,7 @@ var COOKIE_NAME = 'userCtx', var navigateToLogin = function() { $log.warn('User must reauthenticate'); ipCookie.remove(COOKIE_NAME, { path: '/' }); + userCtxCookieValue = undefined; waitForAppCache(function() { $window.location.href = '/' + Location.dbName + '/login' + '?redirect=' + encodeURIComponent($window.location.href); diff --git a/webapp/src/js/services/user.js b/webapp/src/js/services/user.js index c4decc39995..b921bda2729 100644 --- a/webapp/src/js/services/user.js +++ b/webapp/src/js/services/user.js @@ -13,10 +13,7 @@ }); }; - // If the user has role district_admin, returns their facility_id. - // If the user is admin, return undefined. - // Else throw error. - inboxServices.factory('UserDistrict', + inboxServices.factory('UserSettings', function( $q, DB, @@ -24,45 +21,26 @@ ) { 'ngInject'; - return function() { - var userCtx = Session.userCtx(); - if (!userCtx || !userCtx.name) { - return $q.reject(new Error('Not logged in')); + const userDocId = function () { + const userCtx = Session.userCtx(); + if (userCtx) { + return 'org.couchdb.user:' + userCtx.name; } - if (Session.isOnlineOnly(userCtx)) { - return $q.resolve(); - } - return getWithRemoteFallback(DB, 'org.couchdb.user:' + userCtx.name) - .then(function(user) { - if (!user.facility_id) { - return $q.reject(new Error('No district assigned to district admin.')); - } - return user.facility_id; - }) - .then(function(facilityId) { - // ensure the facility exists - return getWithRemoteFallback(DB, facilityId) - .then(function() { - return facilityId; - }); - }); }; - } - ); - inboxServices.factory('UserSettings', - function( - $q, - DB, - Session - ) { - 'ngInject'; + let userDoc; return function() { - var userCtx = Session.userCtx(); - if (!userCtx) { + if (userDoc) { + return userDoc; + } + + const docId = userDocId(); + if (!docId) { return $q.reject(new Error('UserCtx not found')); } - return getWithRemoteFallback(DB, 'org.couchdb.user:' + userCtx.name); + + userDoc = getWithRemoteFallback(DB, docId); + return userDoc; }; } ); diff --git a/webapp/tests/karma/unit/controllers/messages.js b/webapp/tests/karma/unit/controllers/messages.js index b2d48c55508..c88c7d44543 100644 --- a/webapp/tests/karma/unit/controllers/messages.js +++ b/webapp/tests/karma/unit/controllers/messages.js @@ -3,8 +3,7 @@ describe('MessagesCtrl controller', () => { 'use strict'; let createController, - scope, - UserDistrict; + scope; beforeEach(module('inboxApp')); @@ -17,7 +16,6 @@ describe('MessagesCtrl controller', () => { scope.setSelected = obj => scope.selected = obj; scope.setLoadingContent = () => {}; scope.setLeftActionBar = sinon.stub(); - UserDistrict = callback => callback(); createController = () => { return $controller('MessagesCtrl', { '$scope': scope, diff --git a/webapp/tests/karma/unit/controllers/reports.js b/webapp/tests/karma/unit/controllers/reports.js index 286942f0939..e77ddff0e51 100644 --- a/webapp/tests/karma/unit/controllers/reports.js +++ b/webapp/tests/karma/unit/controllers/reports.js @@ -8,7 +8,6 @@ describe('ReportsCtrl controller', () => { get, post, LiveList, - UserDistrict, MarkRead, Search, Changes, @@ -36,9 +35,6 @@ describe('ReportsCtrl controller', () => { scope.setRightActionBar = sinon.stub(); scope.setLeftActionBar = sinon.stub(); scope.settingSelected = () => {}; - UserDistrict = () => { - return { then: () => {} }; - }; LiveList = { reports: { initialised: () => true, setSelected: sinon.stub(), @@ -88,7 +84,6 @@ describe('ReportsCtrl controller', () => { 'Settings': KarmaUtils.nullPromise(), 'Tour': () => {}, 'UpdateFacility': {}, - 'UserDistrict': UserDistrict, 'Verified': {} }); }; diff --git a/webapp/tests/karma/unit/services/user-district.js b/webapp/tests/karma/unit/services/user-district.js deleted file mode 100644 index edbdb8f8339..00000000000 --- a/webapp/tests/karma/unit/services/user-district.js +++ /dev/null @@ -1,164 +0,0 @@ -describe('UserDistrict service', function() { - - 'use strict'; - - var service, - user, - userCtx, - get, - isOnlineOnly; - - beforeEach(function() { - get = sinon.stub(); - isOnlineOnly = sinon.stub(); - module('inboxApp'); - module(function ($provide) { - $provide.factory('DB', KarmaUtils.mockDB({ get: get })); - $provide.value('$q', Q); // bypass $q so we don't have to digest - $provide.value('UserSettings', function(callback) { - callback(null, user); - }); - $provide.value('Session', { - userCtx: function() { - return userCtx; - }, - isOnlineOnly: isOnlineOnly - }); - }); - inject(function($injector) { - service = $injector.get('UserDistrict'); - }); - userCtx = null; - user = null; - }); - - afterEach(function() { - KarmaUtils.restore(get, isOnlineOnly); - }); - - it('returns nothing for db admin', function() { - userCtx = { - name: 'greg', - roles: ['_admin'] - }; - isOnlineOnly.returns(true); - - return service() - .then(function(actual) { - chai.expect(actual).to.equal(undefined); - }); - - }); - - it('returns nothing for national admin', function() { - - userCtx = { - name: 'greg', - roles: ['national_admin'] - }; - isOnlineOnly.returns(true); - - return service() - .then(function(actual) { - chai.expect(actual).to.equal(undefined); - }); - - }); - - it('returns district for district admin', function() { - - userCtx = { - name: 'jeff', - roles: ['district_admin'] - }; - isOnlineOnly.returns(false); - - user = { - name: 'jeff', - roles: ['district_admin'], - facility_id: 'x' - }; - - get.onCall(0).returns(Promise.resolve(user)); - get.onCall(1).returns(Promise.resolve({ type: 'district_hospital' })); - - return service() - .then(function(actual) { - chai.expect(actual).to.equal('x'); - chai.expect(get.callCount).to.equal(2); - chai.expect(get.args[0][0]).to.equal('org.couchdb.user:jeff'); - chai.expect(get.args[1][0]).to.equal('x'); - }); - - }); - - it('returns error for district admin without a facility_id', function() { - - userCtx = { - name: 'jeff', - roles: ['district_admin'] - }; - isOnlineOnly.returns(false); - - user = { - name: 'jeff', - roles: ['district_admin'] - }; - - get.onCall(0).returns(Promise.resolve(user)); - - return service() - .then(function() { - throw new Error('Expected error to be thrown'); - }) - .catch(function(err) { - chai.expect(err.message).to.equal('No district assigned to district admin.'); - }); - - }); - - it('returns error for district admin with a facility_id that doesn\'t exist', function() { - - userCtx = { - name: 'jeff', - roles: ['district_admin'] - }; - isOnlineOnly.returns(false); - - user = { - name: 'jeff', - roles: ['district_admin'], - facility_id: 'x' - }; - - var err404 = {status: 404, name: 'not_found', message: 'missing', error: true, reason: 'missing'}; - - get.onCall(0).returns(Promise.resolve(user)); - get.onCall(1).returns(Promise.reject(err404)); - get.onCall(2).returns(Promise.reject(err404)); - - return service() - .then(function() { - throw new Error('Expected error to be thrown'); - }) - .catch(function(err) { - chai.expect(err.status).to.equal(404); - }); - - }); - - it('returns error for not logged in', function() { - - userCtx = {}; - - return service() - .then(function() { - throw new Error('Expected error to be thrown'); - }) - .catch(function(err) { - chai.expect(err.message).to.equal('Not logged in'); - }); - - }); - -}); diff --git a/webapp/tests/karma/unit/services/user-settings.js b/webapp/tests/karma/unit/services/user-settings.js index 8b5291cf555..f5de4e75391 100644 --- a/webapp/tests/karma/unit/services/user-settings.js +++ b/webapp/tests/karma/unit/services/user-settings.js @@ -13,9 +13,7 @@ describe('UserSettings service', function() { module(function ($provide) { $provide.value('Session', { userCtx: userCtx }); $provide.value('$q', Q); // bypass $q so we don't have to digest - $provide.factory('DB', KarmaUtils.mockDB({ - get: get - })); + $provide.factory('DB', KarmaUtils.mockDB({ get })); }); inject(function($injector) { service = $injector.get('UserSettings'); @@ -50,6 +48,35 @@ describe('UserSettings service', function() { }); }); + it('is cached', function() { + userCtx.returns({ name: 'jack' }); + get.returns(Promise.resolve({ id: 'j' })); + return service() + .then(first => { + chai.expect(first.id).to.equal('j'); + chai.expect(get.callCount).to.equal(1); + return service(); + }) + .then(second => { + chai.expect(second.id).to.equal('j'); + chai.expect(get.callCount).to.equal(1); + }); + }); + + it('multiple concurrent calls result in single database lookup', function() { + userCtx.returns({ name: 'jack' }); + get.returns(Promise.resolve({ id: 'j' })); + const isExpected = doc => { + chai.expect(doc.id).to.equal('j'); + chai.expect(get.callCount).to.equal(1); + }; + + const firstPromise = service(); + service(); + service().then(isExpected); + firstPromise.then(isExpected); + }); + it('gets from remote db', function() { userCtx.returns({ name: 'jack' }); get