<file_sep># 1. Reordering Destinations

Date: 2019-04-09

## Status

Accepted 

## Context

A list of destinations should be reorderable, not fixed

## Decision

A trip is made up of a list of destinations. This list should be able to be reordered, on the main site or the mobile version of the site. Draggable would be the best, but a button for moving an extry up and down will also work.

## Consequences

It may not be best to write the trip to the database everytime a destination is reordered. Perhaps better to store the trip locally, and only write to the database once (with a 'Save' button)

 * Affects planned log functionality
 * Affects groups (others on trip)
 * Need to add JQuery UI or specialised Javascript to enable reordering
<file_sep># 1. Testing

Date: 2019-05-20

## Status

Accepted

## Context

In order to holistically test the core functionality of the website, a combination of unit testing, end-to-end testing, and manual testing is used. 

Unit tests are used on back-end models and database-related code in order to validate the functionality of each essential unit of the code (which, in most cases, are functions). 

On the front-end, various user actions are performed by automated testing software. During that process, key aspects relating to the front-end side of the website are tested.

High-level functionality is exclusively assessed and confirmed via manual user testing. This includes testing the following aspects of the website:

- Marker placement on maps
- Destinations being correctly added and drawn
- Trips being correctly written to and received from session storage

## Decision

The testing framework chosen for automated testing is Jest. This framework is used because: 

- It has a simple installation and configuration process for Node.js
- Due to its popularity as a javascript testing framework, it has a large developer-community which produces many articles, documents and forum threads (amongst many other sources of documentation and support)
- It has a wide variety of built-in assertion abilities (which means that there is no need for the installation of a third-party assertion library)

In order to simulate in-browser user-interactions with the website, Selenium WebDriver is used. Front-end testing is performed on the https://testawaywego.azurewebsites.net website since it is the website used for development. 

Ultimately, it was decided that all automated front-end user testing will be performed using Google Chrome as the browser. The reason for this is due to the fact that Google Chrome has the highest browser market share (more than 60%) globally - meaning that a majrity of the website's users will be using Google Chrome. 

At multiple stages throughout the development process, manual testing on other major browsers (i.e. FireFox, Safari and Microsoft Edge) was also performed in order to ensure the cross-browser compatibility of the website. Manual testing was also used to ensure that the website is mobile-friendly.

## Consequences

The major consequences of the choice to include automated front-end browser testing include:

 * Any tests involving front-end user testing have to be performed on a computer connected to the internet
 * The website will have to allow for "bots" to interact with it. This means that security measures (e.g. reCAPTCHA) against harmful bots  cannot be implemented on the site. 
 * Potential bugs in the website when viewed in other existing browsers (e.g. Opera, QQ, etc.) will not be detected in either automated or manual testing. 

 The major consequences of the choice to use manual user testing for high-level functionality include:
 
 * The developers save time since their time is not spent on writing complex automated tests for these high-level functionalities
 * As a trade-off, manual tests have to be repeatedly conducted on multiple browsers and devices in order to ensure functionality is maintained at several points in time throughout the development process 
<file_sep># 1. Maps API choice

Date: 2019-04-09

## Status

Accepted 

## Context

For the project, we need an API for a map: interface, search, marker placement, satellite or road map imagery. There are several options for maps, will be a primary mode of interacting with the site.

* Google maps
* OpenLayers
* TomTom
* MapBox
* HERE
* Mapfit

Main factor are cost, and ease of use (documentation for the API)

Google maps are highly customizable in style and appearance, and configerable for marker placement, information windows, and interface/controls.

## Decision

Upon examining the options, Google Maps was considered the most mature, easy-to-use and well-supported option. The API has excellent documentation and example code. The interface will be familiar to the majority of site users.

## Consequences

Google Maps has several modular APIs for Places, Routes, etc. A single API key will work for all of these.

 * Cost: for the project purpose we should not exceed the free threshold (views per day, click rate, $300 credit)
 * Limit scalability: If the project were a commercial project, Google's costs would severely limit the size the website could grow too without justifying through profitibility.
 * A 3th party map API requires a URL or Javascript page which exposes the API key and the Https request.
<file_sep>'use strict'

let express = require('express')
let app = express()
let mainRouter = express.Router()

let bodyParser = require('body-parser')
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({
  extended: true
}))

let authenticate = require('../models/authenticate')
let termsModel = require('../models/termsAndConditionsModel')
let tripManagerModel = require('../models/tripManagerModel')
let tripModel = require('../models/tripModel')
let mailManager = require('../models/email_manager')
let invitesModel = require('../models/invitesModel')
let logModel = require('../models/logModel')
let groupModel = require('../models/groupsModel')

// ------------
// URL Routing
// ------------
mainRouter.get('/', function (req, res) {
  res.status(200).sendFile('/index.html', { root: req.app.get('views') })
})

mainRouter.get('/terms_and_conditions', function (req, res) {
  res.status(200).sendFile('/terms_and_conditions.html', { root: req.app.get('views') })
})

mainRouter.get('/terms_and_conditions/data', function (req, res) {
  res.send(termsModel.getTermsAndCondtions())
})

mainRouter.get('/profile', function (req, res) {
  res.status(200).sendFile('profile.html', { root: req.app.get('views') })
})

mainRouter.get('/about', function (req, res) {
  res.status(200).sendFile('/about.html', { root: req.app.get('views') })
})

mainRouter.get('/register', function (req, res) {
  res.status(200).sendFile('/register.html', { root: req.app.get('views') })
})

mainRouter.get(['/sign-in', '/login', '/signin'], function (req, res) {
  res.status(200).sendFile('/sign-in.html', { root: req.app.get('views') })
})

mainRouter.get(['/trip', '/map'], function (req, res) {
  res.status(200).sendFile('/trip.html', { root: req.app.get('views') })
})

mainRouter.get('/hotels', function (req, res) {
  res.status(200).sendFile('/hotels.html', { root: req.app.get('views') })
})

mainRouter.get(['/trip-manager', '/trips'], function (req, res) {
  res.status(200).sendFile('/trip-manager.html', { root: req.app.get('views') })
})

// ----------------
// RESTFUL Routing
// ----------------
mainRouter.post('/trip/log', function (log, res) {
  res.status(202)
  logModel.createLog(log, res)
})

mainRouter.post('/trip-manager/log', function (tripId, res) {
  res.status(202)
  logModel.getLogs(tripId, res)
})

mainRouter.post('/trip/data', function (req, res) {
  res.status(202)
  tripModel.createDestination(req, res)
})

mainRouter.post('/trip-manager/data', function (req, res) {
  res.status(202)
  tripManagerModel.populateTripAndGroupTable(req, res)
})

mainRouter.post('/trip-manager/get-data', function (req, res) {
  res.status(202)
  tripManagerModel.getTrips(req, res)
})

mainRouter.post('/trip-manager-interface/data', function (req, res) {
  res.status(202)
  tripManagerModel.getDestinations(req, res)
})

mainRouter.post('/google-auth', (req, res) => {
  res.status(202)
  authenticate.googleUserAccountDatabaseConnection(req, res)
})

mainRouter.post('/auth', (req, res) => {
  res.status(202)
  authenticate.userAccountDatabaseConnection(req, res)
})

mainRouter.get('/email', function (req, res) {
  res.status(202)
  res.sendFile('email.html', { root: req.app.get('views') })
})

mainRouter.post('/groups', (req, res) => {
  res.status(202)
  groupModel.returnGroupUsers(req, res)
})

mainRouter.post('/invite', function (req, res) {
  mailManager.sendInvite(req.body.emailAddress, req.body.tripName, req.body.invitee)
  invitesModel.addInvite(res, req.body)
  res.sendStatus(200)
})

mainRouter.post('/invites/data', function (req, res) {
  res.status(202)
  invitesModel.getInvites(res, req.body.emailAddress)
})

mainRouter.post('/invites/data/accept', (req, res) => {
  res.status(202)
  invitesModel.handleInvites(req, res, true)
})

mainRouter.post('/invites/data/deny', (req, res) => {
  res.status(202)
  invitesModel.handleInvites(req, res, false)
})

// -----------------------------
// Error/Page Not Found Routing
// ------------------------------
mainRouter.get('*', function (req, res) {
  res.status(404)
  res.sendFile('/404.html', { root: req.app.get('views') })
  // res.status(404).send('404 Error: page not found')
})

module.exports = mainRouter
<file_sep>let check = window.sessionStorage.getItem('Hash')
// console.log(check)
if ((check === null) || (check === undefined)) {
  window.location = '/sign-in'
}

function updateProfile () {
  let usernameTag = document.getElementById('usernameTag')
  usernameTag.textContent = JSON.parse(window.sessionStorage.getItem('Name') + ' ')
  let emailTag = document.getElementById('emailTag')
  emailTag.textContent = JSON.parse(window.sessionStorage.getItem('Email') + ' ')
}

function clearTripFromSessionStorage () {
  window.sessionStorage.removeItem('trip')
}

function signOut () {
  // normal sign-out
  document.cookie = 'awaywegosession=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/'
  document.cookie = 'awaywegosession.sig=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/'
  window.sessionStorage.clear()
  window.localStorage.clear()
}

// Get User name for menu
$(document).ready(function () {
  updateProfile()
  $('#signOutButton').click(() => {
    let authInstance = gapi.auth2.getAuthInstance()
    authInstance.signOut().then(function () {
      signOut()
      console.log('User signed out.')
      window.location = '/'
    })
  })
})
<file_sep>'use strict'

let countries = {
  'au': {
    center: { lat: -25.3, lng: 133.8 },
    zoom: 4
  },
  'br': {
    center: { lat: -14.2, lng: -51.9 },
    zoom: 4
  },
  'ca': {
    center: { lat: 62, lng: -110.0 },
    zoom: 4
  },
  'fr': {
    center: { lat: 46.2, lng: 2.2 },
    zoom: 5
  },
  'de': {
    center: { lat: 51.2, lng: 10.4 },
    zoom: 5
  },
  'gr': {
    center: { lat: 37.59, lng: 23.43 },
    zoom: 4
  },
  'mx': {
    center: { lat: 23.6, lng: -102.5 },
    zoom: 4
  },
  'nz': {
    center: { lat: -40.9, lng: 174.9 },
    zoom: 5
  },
  'it': {
    center: { lat: 41.9, lng: 12.6 },
    zoom: 5
  },
  'za': {
    center: { lat: -30.6, lng: 22.9 },
    zoom: 5
  },
  'es': {
    center: { lat: 40.5, lng: -3.7 },
    zoom: 5
  },
  'pt': {
    center: { lat: 39.4, lng: -8.2 },
    zoom: 6
  },
  'us': {
    center: { lat: 37.1, lng: -95.7 },
    zoom: 4
  },
  'uk': {
    center: { lat: 54.8, lng: -4.6 },
    zoom: 5
  }
}

let darkMapType =
[
  {
    'featureType': 'all',
    'elementType': 'all',
    'stylers': [
      {
        'visibility': 'on'
      }
    ]
  },
  {
    'featureType': 'all',
    'elementType': 'labels',
    'stylers': [
      {
        'visibility': 'off'
      },
      {
        'saturation': '-100'
      }
    ]
  },
  {
    'featureType': 'all',
    'elementType': 'labels.text.fill',
    'stylers': [
      {
        'saturation': 36
      },
      {
        'color': '#000000'
      },
      {
        'lightness': 40
      },
      {
        'visibility': 'off'
      }
    ]
  },
  {
    'featureType': 'all',
    'elementType': 'labels.text.stroke',
    'stylers': [
      {
        'visibility': 'off'
      },
      {
        'color': '#000000'
      },
      {
        'lightness': 16
      }
    ]
  },
  {
    'featureType': 'all',
    'elementType': 'labels.icon',
    'stylers': [
      {
        'visibility': 'off'
      }
    ]
  },
  {
    'featureType': 'administrative',
    'elementType': 'geometry.fill',
    'stylers': [
      {
        'color': '#000000'
      },
      {
        'lightness': 20
      }
    ]
  },
  {
    'featureType': 'administrative',
    'elementType': 'geometry.stroke',
    'stylers': [
      {
        'color': '#000000'
      },
      {
        'lightness': 17
      },
      {
        'weight': 1.2
      }
    ]
  },
  {
    'featureType': 'landscape',
    'elementType': 'geometry',
    'stylers': [
      {
        'color': '#000000'
      },
      {
        'lightness': 20
      }
    ]
  },
  {
    'featureType': 'landscape',
    'elementType': 'geometry.fill',
    'stylers': [
      {
        'color': '#4d6059'
      }
    ]
  },
  {
    'featureType': 'landscape',
    'elementType': 'geometry.stroke',
    'stylers': [
      {
        'color': '#4d6059'
      }
    ]
  },
  {
    'featureType': 'landscape.natural',
    'elementType': 'geometry.fill',
    'stylers': [
      {
        'color': '#4d6059'
      }
    ]
  },
  {
    'featureType': 'poi',
    'elementType': 'geometry',
    'stylers': [
      {
        'lightness': 21
      }
    ]
  },
  {
    'featureType': 'poi',
    'elementType': 'geometry.fill',
    'stylers': [
      {
        'color': '#4d6059'
      }
    ]
  },
  {
    'featureType': 'poi',
    'elementType': 'geometry.stroke',
    'stylers': [
      {
        'color': '#4d6059'
      }
    ]
  },
  {
    'featureType': 'road',
    'elementType': 'geometry',
    'stylers': [
      {
        'visibility': 'on'
      },
      {
        'color': '#7f8d89'
      }
    ]
  },
  {
    'featureType': 'road',
    'elementType': 'geometry.fill',
    'stylers': [
      {
        'color': '#7f8d89'
      }
    ]
  },
  {
    'featureType': 'road.highway',
    'elementType': 'geometry.fill',
    'stylers': [
      {
        'color': '#7f8d89'
      },
      {
        'lightness': 17
      }
    ]
  },
  {
    'featureType': 'road.highway',
    'elementType': 'geometry.stroke',
    'stylers': [
      {
        'color': '#7f8d89'
      },
      {
        'lightness': 29
      },
      {
        'weight': 0.2
      }
    ]
  },
  {
    'featureType': 'road.arterial',
    'elementType': 'geometry',
    'stylers': [
      {
        'color': '#000000'
      },
      {
        'lightness': 18
      }
    ]
  },
  {
    'featureType': 'road.arterial',
    'elementType': 'geometry.fill',
    'stylers': [
      {
        'color': '#7f8d89'
      }
    ]
  },
  {
    'featureType': 'road.arterial',
    'elementType': 'geometry.stroke',
    'stylers': [
      {
        'color': '#7f8d89'
      }
    ]
  },
  {
    'featureType': 'road.local',
    'elementType': 'geometry',
    'stylers': [
      {
        'color': '#000000'
      },
      {
        'lightness': 16
      }
    ]
  },
  {
    'featureType': 'road.local',
    'elementType': 'geometry.fill',
    'stylers': [
      {
        'color': '#7f8d89'
      }
    ]
  },
  {
    'featureType': 'road.local',
    'elementType': 'geometry.stroke',
    'stylers': [
      {
        'color': '#7f8d89'
      }
    ]
  },
  {
    'featureType': 'transit',
    'elementType': 'geometry',
    'stylers': [
      {
        'color': '#000000'
      },
      {
        'lightness': 19
      }
    ]
  },
  {
    'featureType': 'water',
    'elementType': 'all',
    'stylers': [
      {
        'color': '#2b3638'
      },
      {
        'visibility': 'on'
      }
    ]
  },
  {
    'featureType': 'water',
    'elementType': 'geometry',
    'stylers': [
      {
        'color': '#2b3638'
      },
      {
        'lightness': 17
      }
    ]
  },
  {
    'featureType': 'water',
    'elementType': 'geometry.fill',
    'stylers': [
      {
        'color': '#24282b'
      }
    ]
  },
  {
    'featureType': 'water',
    'elementType': 'geometry.stroke',
    'stylers': [
      {
        'color': '#24282b'
      }
    ]
  },
  {
    'featureType': 'water',
    'elementType': 'labels',
    'stylers': [
      {
        'visibility': 'off'
      }
    ]
  },
  {
    'featureType': 'water',
    'elementType': 'labels.text',
    'stylers': [
      {
        'visibility': 'off'
      }
    ]
  },
  {
    'featureType': 'water',
    'elementType': 'labels.text.fill',
    'stylers': [
      {
        'visibility': 'off'
      }
    ]
  },
  {
    'featureType': 'water',
    'elementType': 'labels.text.stroke',
    'stylers': [
      {
        'visibility': 'off'
      }
    ]
  },
  {
    'featureType': 'water',
    'elementType': 'labels.icon',
    'stylers': [
      {
        'visibility': 'off'
      }
    ]
  }
]

let silverMapType =
[
  {
    'elementType': 'geometry',
    'stylers': [
      {
        'color': '#f5f5f5'
      }
    ]
  },
  {
    'elementType': 'labels.icon',
    'stylers': [
      {
        'visibility': 'off'
      }
    ]
  },
  {
    'elementType': 'labels.text.fill',
    'stylers': [
      {
        'color': '#616161'
      }
    ]
  },
  {
    'elementType': 'labels.text.stroke',
    'stylers': [
      {
        'color': '#f5f5f5'
      }
    ]
  },
  {
    'featureType': 'administrative.country',
    'stylers': [
      {
        'weight': 8
      }
    ]
  },
  {
    'featureType': 'administrative.land_parcel',
    'elementType': 'labels.text.fill',
    'stylers': [
      {
        'color': '#bdbdbd'
      }
    ]
  },
  {
    'featureType': 'poi',
    'elementType': 'geometry',
    'stylers': [
      {
        'color': '#eeeeee'
      }
    ]
  },
  {
    'featureType': 'poi',
    'elementType': 'labels.text.fill',
    'stylers': [
      {
        'color': '#757575'
      }
    ]
  },
  {
    'featureType': 'poi.park',
    'elementType': 'geometry',
    'stylers': [
      {
        'color': '#e5e5e5'
      }
    ]
  },
  {
    'featureType': 'poi.park',
    'elementType': 'labels.text.fill',
    'stylers': [
      {
        'color': '#9e9e9e'
      }
    ]
  },
  {
    'featureType': 'road',
    'elementType': 'geometry',
    'stylers': [
      {
        'color': '#ffffff'
      }
    ]
  },
  {
    'featureType': 'road.arterial',
    'elementType': 'labels',
    'stylers': [
      {
        'visibility': 'off'
      }
    ]
  },
  {
    'featureType': 'road.arterial',
    'elementType': 'labels.text.fill',
    'stylers': [
      {
        'color': '#757575'
      }
    ]
  },
  {
    'featureType': 'road.highway',
    'elementType': 'geometry',
    'stylers': [
      {
        'color': '#dadada'
      }
    ]
  },
  {
    'featureType': 'road.highway',
    'elementType': 'labels',
    'stylers': [
      {
        'visibility': 'off'
      }
    ]
  },
  {
    'featureType': 'road.highway',
    'elementType': 'labels.text.fill',
    'stylers': [
      {
        'color': '#616161'
      }
    ]
  },
  {
    'featureType': 'road.local',
    'stylers': [
      {
        'visibility': 'off'
      }
    ]
  },
  {
    'featureType': 'road.local',
    'elementType': 'labels.text.fill',
    'stylers': [
      {
        'color': '#9e9e9e'
      }
    ]
  },
  {
    'featureType': 'transit.line',
    'elementType': 'geometry',
    'stylers': [
      {
        'color': '#e5e5e5'
      }
    ]
  },
  {
    'featureType': 'transit.station',
    'elementType': 'geometry',
    'stylers': [
      {
        'color': '#eeeeee'
      }
    ]
  },
  {
    'featureType': 'water',
    'elementType': 'geometry',
    'stylers': [
      {
        'color': '#c9c9c9'
      }
    ]
  },
  {
    'featureType': 'water',
    'elementType': 'labels.text.fill',
    'stylers': [
      {
        'color': '#9e9e9e'
      }
    ]
  }
]

<file_sep>'user strict'

document.addEventListener('DOMContentLoaded', function () {
  let lang
  if (document.querySelectorAll('#map').length > 0) {
    if (document.querySelector('html').lang) {
      lang = document.querySelector('html').lang
    } else {
      lang = 'en'
    }

    let jsFile = document.createElement('script')
    jsFile.type = 'text/javascript'
    jsFile.src = 'https://maps.googleapis.com/maps/api/js?key=<KEY>&libraries=places&callback=initMap&region=ZA&language=' + lang
    document.getElementsByTagName('head')[0].appendChild(jsFile)
  }
})<file_sep>let termsAndConditionsModel = require('../app/models/termsAndConditionsModel')
let fs = require('fs')
let path = require('path')
// import addTermsToSubsection from '../app/controllers/terms_and_conditions'


describe('testing the model that exports the correct terms and conditions', () => {
    test('test if the preamble stord in the model is correct', () => {
        let preamble = [
            'When you create an account with us, you must provide us information that is accurate, complete, and current at all times. Failure to do so constitutes a breach of the Terms, which may result in immediate termination of your account on our Service.',
            'You are responsible for safeguarding the password that you use to access the Service and for any activities or actions under your password, whether your password is with our Service or a third-party service.',
            'You agree not to disclose your password to any third party. You must notify us immediately upon becoming aware of any breach of security or unauthorized use of your account.'
        ]
        expect(termsAndConditionsModel.getTermsAndCondtions().preamble).toEqual(preamble)
    })

    test('test if the information stored about accounts is correct', () => {
        let accounts = [
            'When you create an account with us, you must provide us information that is accurate, complete, and current at all times. Failure to do so constitutes a breach of the Terms, which may result in immediate termination of your account on our Service',
            'You are responsible for safeguarding the password that you use to access the Service and for any activities or actions under your password, whether your password is with our Service or a third-party service',
            'You agree not to disclose your password to any third party. You must notify us immediately upon becoming aware of any breach of security or unauthorized use of your account'
        ]
        expect(termsAndConditionsModel.getTermsAndCondtions().accounts).toEqual(accounts)
    })

    test('test if the information stored about other sites is correct', () => {
        let otherSites = [
            'Our Service may contain links to third-party web sites or services that are not owned or controlled by Away We Go',
            'Away We Go has no control over, and assumes no responsibility for, the content, privacy policies, or practices of any third party web sites or services. You further acknowledge and agree that Away We Go shall not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with use of or reliance on any such content, goods or services available on or through any such web sites or services.',
            'We strongly advise you to read the terms and conditions and privacy policies of any third-party web sites or services that you visit.'
        ]
        expect(termsAndConditionsModel.getTermsAndCondtions().otherSites).toEqual(otherSites)
    })
})

// describe('testing that the controller renders the data from the model onto the HTML page', () => {
//     describe('testing that the addTermsToSubsection function adds the neccesary information to each HTML div', () =>{
//         let testFolderRegex = /test/gi
//         let rootDir = path.resolve(__dirname).replace(testFolderRegex, '')
//         let termsAndConditionsHTMLDir = path.join(rootDir, '/app/views/terms_and_conditions.html')
//         // Read in the HTML file
//         document.body.innerHTML = fs.readFileSync(termsAndConditionsHTMLDir)
    
//         // require('../app/controllers/terms_and_conditions')
//         const $ = require('jquery')
        
//         // let addTermsToSubsection = function (subsection, termsArray, divider) {
//         //     let parentItemType = ''
//         //     if (divider === 'li') {
//         //     parentItemType = 'ol'
//         //     } else {
//         //     parentItemType = 'p'
//         //     }
//         //     let parentItem = document.createElement(parentItemType)
//         //     termsArray.forEach(element => {
//         //     let item = document.createElement(divider)
//         //     let term = document.createTextNode(element)
//         //     item.appendChild(term)
//         //     parentItem.appendChild(item)
//         //     })
//         //     subsection.append(parentItem)
//         // }
            
//         test('preamble section is added to the div with id="Preamble"', () => {  // Get the directory of where the HTML file is for the T&Cs
//             // An initial space is added to account for the fact that the preamble div's first HTML DOM child is a <p> element
//             let preambleString = ' '
//             // Construct the preamble string from the sentences returned by the model
//             termsAndConditionsModel.getTermsAndCondtions().preamble.forEach((sentence) => {
//                 preambleString = preambleString + sentence
//             })

//             addTermsToSubsection($('#Preamble'), termsAndConditionsModel.getTermsAndCondtions().preamble, 'p')

//             expect($('#Preamble').text()).toEqual(preambleString) 
//         })
//     })   
// })


<file_sep>'use strict'
jest.mock('../app/models/db')
let tripModel = require('../app/models/tripModel')

class Trip {
  constructor(id, title, destinations, user) {
    this.id = id
    this.title = title
    this.destinationList = destinations
    this.user = user
  }
}

class Destination {
  constructor(id, lat, lng, placeId, place, name, ordering) {
    this.id = String(id)
    this.lat = Number(lat)
    this.lng = Number(lng)
    this.placeId = String(placeId)
    this.place = String(place)
    this.name = String(name)
    this.ordering = Number(ordering)
  }
}

describe('testing trip class data structures', () => {
  let testTrip = new Trip(12345, "My Trip", [], 'A1B2C3D4E5F6')
  test('creating a trip object succeeds', () => {
    expect(testTrip.id).not.toBeNull()
    expect(testTrip.title).not.toEqual('')
    expect(testTrip.destinationList).toEqual([])
    expect(testTrip.user).toEqual('A1B2C3D4E5F6')
  })
})

describe('testing destination class data structures', () => {
  let testLatLng = { lat: 0, lng: 0 }
  let testDestination = new Destination(12345, 0, 0, 'A123', "My Destination", "testName", 1)
  test('creating a destination object succeeds', () => {
    expect(testDestination.id).not.toBeNull()
    expect(testDestination.lat).toEqual(testLatLng.lat)
    expect(testDestination.lng).toEqual(testLatLng.lng)
    expect(testDestination.placeId).not.toEqual(undefined)
    expect(testDestination.place).not.toEqual('')
    expect(testDestination.place).not.toBeNull()
    expect(testDestination.name).not.toBeNull()
    expect(testDestination.ordering).toEqual(1)
  })
})

describe('testing createDestinationQueryString function', () => {
  let testTrip1 = new Trip(12345, "My Trip", [], 'A1B2C3D4E5F6')
  let testQS1 = `DELETE FROM destinations WHERE trip_id = 12345;`
  test('destination query string is correct when trip has no destinations', () => {
    expect(tripModel.createDestinationQueryString(testTrip1)).toEqual(testQS1)
  })

  let testDestination1 = new Destination(12345, 0, 0, 'A123', "My Destination", "testName", 1)
  let testTrip2 = new Trip(12345, "My Trip", [testDestination1], 'A1B2C3D4E5F6')
  let testQS2 = testQS1 + `INSERT INTO destinations VALUES ('12345',0,0,'A123','My Destination','testName',1,12345);`
  test('destination query string is correct when trip has one destination', () => {
    expect(tripModel.createDestinationQueryString(testTrip2)).toEqual(testQS2)
  })

  let testDestination2 = new Destination(12345, 0, 0, 'A123', "My Destination", "testName", 1)
  let testTrip3 = new Trip(12345, "My Trip", [testDestination1, testDestination2], 'A1B2C3D4E5F6')
  let testQS3 = testQS2 + `INSERT INTO destinations VALUES ('12345',0,0,'A123','My Destination','testName',1,12345);`
  test('destination query string is correct when trip has two destinations', () => {
    expect(tripModel.createDestinationQueryString(testTrip3)).toEqual(testQS3)
  })
})

describe('testing query functions', () => {
  test('testing createDestinationQuery', async () => {
    let testDestination = new Destination(12345, 0, 0, 'A123', "My Destination", "testName", 1)
    let testTrip = new Trip(12345, "My Trip", [testDestination], 'A1B2C3D4E5F6')
    let queryString = tripModel.createDestinationQueryString(testTrip)
    let createDestination = await tripModel.createDestinationQuery(queryString)
    expect(Object.keys(createDestination).length).toEqual(1)
    expect(createDestination.recordset).toEqual(undefined)
  })
})
<file_sep># 1. Login API Choice

Date: 2019-04-09

## Status

Accepted 

## Context

It would be convenient to use the Google Login API as an alternative method for users to login. This would provide a template for our own login details stored in the DB, as well as a quick way to get the Sprint 1 User story related to login completed ASAP. 

## Decision

Using a well known and widely known/supported login mechanism such as Google's OAuth2 will allow more rapid development of an appropriate security setup for the site. We will apply for an API key and start implementing the login/registration page through the Google Login API

## Consequences

We don;t want users to be required to have a gmail account. Therefore we _still_ have to build a normal login/password authentication setup, in addition to the google login, and with additiona checks to confirm the interoprability between the two registration/login methods. However, the speed in the first sprint, and leaning on established beats practice from a major and knowledgeable company such as Google means we can learn how to best structure the data stored/required for our own site login.

 * Additional testing on interoperability
 * The password and user details have a security context and must be labelled as such
<file_sep>'use strict'

let db = require('./db')
let SqlString = require('sqlstring')

function createLog (logInfo, res) {
  let log = logInfo.body
  let queryString = createLogQueryString(log)
  createLogQuery(queryString)
    .then(result => {
      res.send('Log table added to entries')
    })
    .catch(err => {
      console.log('populate log table error:', err)
    })
}

function createLogQueryString (log) {
  let queryString = ''
  for (let i = 0; i < log.length; i++) {
    queryString = queryString + SqlString.format('INSERT INTO log VALUES (?,?,?,?,?,?);',
      [log[i].id,
        log[i].userId,
        log[i].code,
        log[i].date,
        log[i].importance,
        log[i].tripId])
  }
  return queryString
}

function createLogQuery (queryString) {
  return db.pools
    .then(pool => {
      return pool.request()
        .query(queryString)
    })
}

function getLogs (req, res) {
  let tripId = req.body.tripId
  getLogQuery(tripId)
    .then(result => {
      res.send(result.recordset)
    })
    .catch(err => {
      console.log('Get log error:', err)
    })
}

function getLogQuery (tripId) {
  return db.pools
    .then(pool => {
      let dbrequest = pool.request()
      dbrequest.input('tripId', tripId)
      return dbrequest
        .query('SELECT id, userId, code, date, importance, trip_id, first_name, last_name FROM log JOIN users ON log.userid = users.hash WHERE trip_id = @tripId;')
    })
}

module.exports = {
  createLog: createLog,
  getLogs: getLogs,
  createLogQueryString: createLogQueryString,
  createLogQuery: createLogQuery,
  getLogQuery: getLogQuery
}
<file_sep>'use strict'

let express = require('express')
let path = require('path')
let app = express()
let mainRouter = require('./app/routes/mainRoutes')
let cookieSession = require('cookie-session')
let OAuthKeys = require('./app/models/keys.json')
require('dotenv').config()
let favicon = require('serve-favicon')
let bodyParser = require('body-parser')
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({
  extended: true
}))

app.use(favicon(path.join(__dirname, './app/public/', 'favicon.ico')))

app.set('views', path.join(__dirname, './app/views'))
app.use(express.static(path.join(__dirname, './app/public')))
app.use(express.static(path.join(__dirname, './app/controllers')))

app.use('/', mainRouter)

let port = process.env.PORT || 3000
app.listen(port)
console.log('Express server running on port', port)
<file_sep>'use strict'

$(document).ready(function () {
  $('#landingPageSignInButton').click(function () {
    window.location = '/sign-in'
  })

  $('#landingPageRegisterButton').click(function () {
    window.location = '/register'
  })

  $('#aboutButton').click(function () {
    window.location = '/about'
  })
})
<file_sep>'use strict'
jest.mock('../app/models/db')
let tripManagerModel = require('../app/models/tripManagerModel')


describe('testing getTripTitlesQueryString function', () => {
    let testTrip1 = { trip_id: 1234 }
    let testTripArray2 = [testTrip1]
    let testQS2 = `SELECT * FROM trips WHERE id IN ('1234');`
    test('trip query string is correct when one trip exists', () => {
        expect(tripManagerModel.getTripTitlesQueryString(testTripArray2)).toEqual(testQS2)
    })

    let testTrip2 = { trip_id: 5678 }
    let testTripArray3 = [testTrip1, testTrip2]
    let testQS3 = `SELECT * FROM trips WHERE id IN ('1234','5678');`
    test('trip query string is correct when more than one trip exists', () => {
        expect(tripManagerModel.getTripTitlesQueryString(testTripArray3)).toEqual(testQS3)
    })
})

describe('testing tripManagerModel query functions', () => {
    let tripInfo = {
        id: 0,
        title: 'My Trip',
        user: 'a1s2d3f4g5h6j7k8'
    }

    test('testing populateTripAndGroupTableQuery', async () => {
        let tableResult = await tripManagerModel.populateTripAndGroupTableQuery(tripInfo)
        expect(Object.keys(tableResult).length).toEqual(1)
        expect(tableResult.recordset).toEqual(undefined)
    })

    test('testing getTripsQuery', async () => {
        let user = 'z<PASSWORD>'
        let groups = await tripManagerModel.getTripsQuery(user)
        expect(groups.recordset[0].user_hash).toEqual('z1x2c3v4b5n6m7')
        expect(groups.recordset[1].user_hash).toEqual('z1x2c3v4b5n6m7')
        expect(groups.recordset[0].trip_id).toEqual('123456789')
        expect(groups.recordset[1].trip_id).toEqual('987654321')
    })

    test('testing getTripTitlesQuery', async () => {
        let testTrip1 = { trip_id: 1234 }
        let testTrip2 = { trip_id: 5678 }
        let testTripArray3 = [testTrip1, testTrip2]
        let titles = await tripManagerModel.getTripTitlesQuery(testTripArray3)
        expect(titles.recordset[0].id).toEqual('0')
        expect(titles.recordset[1].id).toEqual('1')
        expect(titles.recordset[0].title).toEqual('My Trip')
        expect(titles.recordset[1].title).toEqual('Your Trip')
    })

    test('testing getDestinationsQuery', async ()=>{
        let tripId = 1
        let trip = await tripManagerModel.getDestinationsQuery(tripId)
        expect(trip.recordset[0].id).toEqual('1')
        expect(trip.recordset[0].lat).toEqual('0')
        expect(trip.recordset[0].lng).toEqual('0')
        expect(trip.recordset[0].place_id).toEqual('place12345')
        expect(trip.recordset[0].place).toEqual('London')
        expect(trip.recordset[0].name).toEqual('Family visit')
        expect(trip.recordset[0].ordering).toEqual(1)
        expect(trip.recordset[0].trip_id).toEqual('1')
        expect(trip.recordset[1].id).toEqual('2')
        expect(trip.recordset[1].lat).toEqual('1')
        expect(trip.recordset[1].lng).toEqual('1')
        expect(trip.recordset[1].place_id).toEqual('place54321')
        expect(trip.recordset[1].place).toEqual('Manchester')
        expect(trip.recordset[1].name).toEqual('Football stadium visit')
        expect(trip.recordset[1].ordering).toEqual(2)
        expect(trip.recordset[1].trip_id).toEqual('1')
    })
})<file_sep>'use strict'

let db = require('./db')

/* Populate trip and groups table */
function populateTripAndGroupTable (trip, res) {
  let tripInfo = trip.body
  populateTripAndGroupTableQuery(tripInfo)
    .then(result => {
      // console.log('trips and groups population result ', result)
      res.send(tripInfo)
    })
    .catch(err => {
      console.log('populate trips table error:', err)
    })
}

function populateTripAndGroupTableQuery (tripInfo) {
  return db.pools
    .then(pool => {
      let dbrequest = pool.request()
      dbrequest.input('id', db.sql.Char, tripInfo.id)
      dbrequest.input('title', tripInfo.title)
      dbrequest.input('user', tripInfo.user)

      return dbrequest
        .query(`DELETE FROM trips WHERE id = @id; INSERT INTO trips VALUES(@id, @title); IF NOT EXISTS (SELECT * FROM groups WHERE user_hash = @user AND trip_id = @id) BEGIN INSERT INTO groups VALUES(@user, @id) END;`)
    })
}

/* Get Trips Query */

function getTrips (req, res) {
  let user = JSON.parse(req.body.userHash)
  getTripsQuery(user)
    .then(result => {
      // console.log('get trips result ', result)
      if (result.recordset.length !== 0) {
        getTripTitles(result.recordset, res)
      }
    })
    .catch(err => {
      console.log('Get trips error:', err)
    })
}

function getTripsQuery (user) {
  return db.pools
    .then(pool => {
      let dbrequest = pool.request()
      dbrequest.input('user', user)
      return dbrequest
        .query(`SELECT * FROM groups WHERE user_hash = @user;`)
    })
}

function getTripTitles (trips, res) {
  getTripTitlesQuery(trips)
    .then(result => {
      // console.log('get trip titles result ', result)
      if (trips.legnth !== 0) { res.send(result.recordset) } else {
        res.send('NoTripTitlesFound')
      }
    })
    .catch(err => {
      console.log('Get trip titles error:', err)
    })
}

function getTripTitlesQuery (trips) {
  return db.pools
    .then(pool => {
      if (trips.length !== 0) {
        let queryString = getTripTitlesQueryString(trips)
        return pool.request()
          .query(queryString)
      }
    })
}

function getTripTitlesQueryString (trips) {
  let queryString = `SELECT * FROM trips WHERE id IN (`
  for (let i = 0; i < trips.length; i++) {
    queryString = queryString + `'${trips[i].trip_id}',`
  }
  queryString = queryString.substring(0, queryString.length - 1)
  queryString = queryString + `);`
  return queryString
}

/* Get Destinations Query */

function getDestinations (req, res) {
  let tripId = req.body.tripId
  getDestinationsQuery(tripId)
    .then(result => {
      // console.log('get destinations result ', result.recordset)
      res.send(result.recordset)
    })
    .catch(err => {
      console.log('Get destinations error:', err)
    })
}

function getDestinationsQuery (tripId) {
  return db.pools
    .then(pool => {
      let dbrequest = pool.request()
      dbrequest.input('tripId', tripId)
      return dbrequest
        .query(`SELECT * FROM destinations WHERE trip_id = @tripId;`)
    })
}

module.exports = {
  populateTripAndGroupTable: populateTripAndGroupTable,
  getTrips: getTrips,
  getDestinations: getDestinations,
  getTripTitlesQueryString: getTripTitlesQueryString,
  populateTripAndGroupTableQuery: populateTripAndGroupTableQuery,
  getTripsQuery: getTripsQuery,
  getTripTitlesQuery: getTripTitlesQuery,
  getDestinationsQuery: getDestinationsQuery
}
<file_sep>'use strict'
/* DATABASE SET UP */
const mssql = require('mssql')
require('dotenv').config()
let config = {
  server: process.env.DB_SERVER,
  database: process.env.DB_NAME,
  user: process.env.DB_ADMIN,
  password: <PASSWORD>,
  port: Number(process.env.DB_PORT),
  options: {
    encrypt: true
  },
  pool: {
    max: 10,
    min: 0,
    idleTimeoutMillis: 30000
  }
}

/* Get a mssql connection instance */
let isConnected = true
let connectionError = null
let pools = new mssql.ConnectionPool(config)
  .connect()
  .then(pool => {
    console.log('Connected to DB')
    return pool
  })
  .catch(err => {
    isConnected = false
    connectionError = err
    console.log('connection error', err)
  });

/* Create Database Tables */

(function createUserTable () {
  pools.then((pool) => {
    return pool.request()
      // This is only a test query, change it to whatever you need
      .query(`IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='users' and xtype='U')
          CREATE TABLE users (
          first_name varchar(50),
          last_name varchar(50),
          email_address varchar(50) NOT NULL,
          image_url varchar(255),
          hash varchar(255) PRIMARY KEY NOT NULL
          )`)
  }).then(result => {
    // console.log('user table created', result)
  }).catch(err => {
    console.log('user table creation error', err)
  })
}
)();

(function createDestinationTable () {
  pools.then((pool) => {
    return pool.request()
      .query(`IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='destinations' and xtype='U')
          CREATE TABLE destinations (
          id varchar(255) PRIMARY KEY, 
          lat float,
          lng float,
          place_id varchar(255),
          place varchar(255),
          name varchar(50),
          ordering int,
          trip_id varchar(255)
          )`)
  }).then(result => {
  }).catch(err => {
    console.log('destinations table creation error', err)
  })
})();

(function createInvitesTable () {
  pools.then((pool) => {
    return pool.request()
      .query(`IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='invites' and xtype='U')
          CREATE TABLE invites (
          trip_id varchar(255), 
          email_address varchar(255)
          )`)
  }).then(result => {
    // console.log('invites table created', result)
  }).catch(err => {
    console.log('invites table creation error', err)
  })
})();

(function createTripTable () {
  pools.then((pool) => {
    return pool.request()
      .query(`IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='trips' and xtype='U')
          CREATE TABLE trips (
          id varchar(255) PRIMARY KEY, 
          title varchar(50)
          )`)
  }).then(result => {
    // console.log('trips table created', result)
  }).catch(err => {
    console.log('trips table creation error', err)
  })
}
)();

(function createGroupsTable () {
  pools.then((pool) => {
    return pool.request()
      .query(`IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='groups' and xtype='U')
          CREATE TABLE groups (
          user_hash varchar(255), 
          trip_id varchar(255)
          )`)
  }).then(result => {
    // console.log('groups table created', result)
  }).catch(err => {
    console.log('groups table creation error', err)
  })
}
)();

(function createLogTable () {
  pools.then((pool) => {
    return pool.request()
      .query(`IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='log' and xtype='U')
          CREATE TABLE log (
          id varchar(255) PRIMARY KEY, 
          userId varchar(255),
          code tinyint,
          date smalldatetime,
          importance bit,
          trip_id varchar(255)
          )`)
  }).then(result => {
  }).catch(err => {
    console.log('log table creation error', err)
  })
}
)()

module.exports = {
  sql: mssql,
  pools: pools,
  isConnected: isConnected,
  connectionError: connectionError
}
<file_sep>'use strict'

// Fake test so file can be included

test('Hello World: hello should greet the world', () => {
  let hello = 'world'
expect(hello).toEqual('world')
})

// Integration tests for the routing

/* You can get the status code alone using curl with:
curl -Li http://localhost:3000 -o /dev/null -w '%{http_code}\n' -s
*/

/* 
Integration testing for the routing would test each of the status codes 
for every possible defined route, and that a 404 is returned for all undefined routes
All data querie should return 202 (query recieved) or 201 (resurce created) for DB table creation requests
However, I could not configure the tests to work correctly, despite hours of attempts and several different 
techniques. Here I have left the main methods that I tried, and failed to get working.
*/

// var express = require('express')
// const app = express()
// var router = express.Router()

// const app = require('../index')
// const router = require('../app/routes/mainRoutes')
// const request = require('supertest')

// describe('test the root path', () => {
//   test('response should be status 200 (OK)', () => {
//     return request(router)
//     .get('/')
//     .expect(200)
//   })
// })

// describe('test the root path', () => {
//     test('response should be status 200 (OK)', () => {
//         return request(router)
//         .get("/").then(response => {
//             expect(response.statusCode).toBe(200)
//         })
//     })
// })

// An attempt with Jasmine for front end resolving on a running local instance or online:
// describe('test the root path', function() {
//   it('should route "/" to index', function() {
//     var controller = require('../app/routes/mainRoutes');
//     var orig_this = this;
//     var orig_load = require('module')._load;
//     var router = jasmine.createSpyObj('Router', ['get']);
//     var express = jasmine.createSpyObj('express', ['Router']);
//     express.Router.and.returnValues(router);
//     spyOn(require('module'), '_load').and.callFake(function() {
//       if (arguments[0] == 'express') {
//         return express;
//       } else {
//         return orig_load.apply(orig_this, arguments);
//       }
//     });
//     require('../app/routes/mainRoutes');
//     expect(router.get).toHaveBeenCalledWith('/', mainRouter);
//   });
// });

// Trying to setup a local instance (or online to the test or main server) to check responses...

// describe('loading express', function () {
//   let app, mainRouter
//   const request = require('supertest')
//   beforeEach(function () {
//     mainRouter = require('../app/routes/mainRoutes')
//     app = require('../index').listen(3000)
//   })
//   afterEach(function () {
//     app.close();
//   })
//   it('responds to /', function testSlash(done) {
//   request(mainRouter)
//     .get('/')
//     .expect(200, done);
//   })
//   it('404 everything else', function testPath(done) {
//     request(mainRouter)
//       .get('/foo/bar')
//       .expect(404, done);
//   })
// })<file_sep>const { Builder, By, Key, until } = require('selenium-webdriver')

require('selenium-webdriver/chrome')

require('selenium-webdriver/firefox')

require('chromedriver')

require('geckodriver')

const rootURL = 'https://testawaywego.azurewebsites.net/sign-in'

const d = new Builder().forBrowser('chrome').build()
const waitUntilTime = 20000
let driver, el, actual, expected
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 5

async function getElementById(id) {
    const el = await driver.wait(until.elementLocated(By.id(id)), waitUntilTime)
    return await driver.wait(until.elementIsVisible(el), waitUntilTime)
}
  
async function getElementByXPath(xpath) {
    const el = await driver.wait(until.elementLocated(By.xpath(xpath)), waitUntilTime)
    return await driver.wait(until.elementIsVisible(el), waitUntilTime)
}

test('waits for the driver to start', async () => {
    return d.then(_d => {
      driver = _d
    })
})
  
test('initialises the context', async () => {
    await driver.manage().window().setPosition(0, 0)
    await driver.manage().window().setSize(1280, 1024)
    await driver.get(rootURL)
})

describe('testing page elements on page load', () => {
    test('Email address placeholder is correct', async () => {
        el = await getElementById('inputEmail')
        actual = await el.getAttribute('placeholder')
        expected = 'Email address'
        expect(actual).toEqual(expected)
    })
    
    test('Password placeholder is correct', async () => {
        el = await getElementById('inputPassword')
        actual = await el.getAttribute('placeholder')
        expected = 'Password'
        expect(actual).toEqual(expected)
    })
    
    test('Remember password checkbox is not checked on page load', async () => {
        const el = await driver.wait(until.elementLocated(By.id('customCheck1')), waitUntilTime)
        actual = await el.isSelected()
        expected = false
        expect(actual).toEqual(expected)
    })
})

describe('test successful sign in', () => {
    test('User directed to trips page on successful sign in', async () => {
        let email = await getElementById('inputEmail')
        // email.clear()
        email.sendKeys('<EMAIL>')
        let password = await getElementById('inputPassword')
        // password.clear()
        password.sendKeys('r') // correct password for email
        let button = await getElementById('signInPageSignInButton')
        button.click()
    
        // wait for an element that is unique to the trip page to be found before getting the URL
        await getElementById('pac-input')
        let actual = await driver.getCurrentUrl()
        let expected = 'https://testawaywego.azurewebsites.net/trip'
        expect(actual).toEqual(expected)
    })
})

// describe('test failed sign in', () => {
//     test('User not directed to trips page when password is incorrect', async () => {
//         let correctEmail = await getElementById('inputEmail')
//         correctEmail.sendKeys('<EMAIL>')
//         let wrongPassword = await getElementById('inputPassword')
//         wrongPassword.sendKeys('<PASSWORD>') // wrong password for email
//         let button = await getElementById('signInPageSignInButton')
//         button.click()

//         // alert appears telling user that password is incorrect
//         let alert = await driver.switchTo().alert()
//         let actualAlert = alert.getText()
//         let expectedAlert = 'Your password is incorrect.'
//         expect(actualAlert).toEqual(expectedAlert)
//         alert.accept()
    
//         // page does not redirect to trips
//         let actual = await driver.getCurrentUrl()
//         let expected = 'https://awaywego.azurewebsites.net/sign-in'
//         expect(actual).toEqual(expected) 
//     })
// })

afterAll(() => {
    driver.quit()
});<file_sep># 1. Folder structure

Date: 2019-04-10
Modified: 2019-05-21 (Tyson)

## Status

Accepted

## Context

Having a fixed structure for a project has may advantages, limiting spread of files across multiple folders and contraining locations to known places. THere is an advantage is letting a folder strucute emerge oganically, but also a large risk, as things can break when low-level file locations change, necesitating logs of bug fixing and refactoring. Having a rigid initial structure canb lead to later restrictions, or imposed complexity.
## Decision

The following folder strucure is adopted:

    .
    ├── app
    │   ├── controllers
    │   ├── models
    │   ├── public
    │   │   ├── css
    │   │   ├── img
    │   │   └── js
    │   ├── routes
    │   └── views
    ├── docs
    │   ├── adr
    │   ├── misc
    │   ├── project_artifacts
    │   └── templates
    ├── node_modules
    ├── test
    └── local_only

**Update** Removed folders originally specified that were found to not be required during project development: 'log' and 'utility'

## Consequences

Will need to be disciplined in file locations and naming conventions. Will need to produce a naming convention document to accompany this folder structure ADR.

 * local-only will not be on repo
 * node_modules MUST not be on repo
 * utility -> back-end, maintenance task folder, not related to site or site-serving
 * docs must be maintained : directly to main branch, or updated in seperate branch?
<file_sep># 1. Sprint Planning and timeline

Date: 2019-04-09

## Status

Accepted 

## Context

16-25 April, Thabang is away, must work remotely. Major submission period over 29th April - 10th May, will impact productivity on software project.


## Decision

4 Sprints planned, consecutively. Will only start on 17th April, but have a "Sprint 0" from 9th to 16 April, with initial planning, research and folder structure creation, setup of Azure and Travis. Will not count towards actual number of sprints.

* Sprints will begin on Wednesdays, with a 1 hr review session in the morning.
* There will be a three hour sprint planning session in the afternooon each Wednesday.
* Release will be every Tuesday, by 8pm
* Product release window will be from 2pm - 8pm, with all pull requests done before 6pm to give time for any required last minute code review and testing

* Friday coding sessions together from 12pm - 4pm
* Standups via Whatsapp, or between lectures. Preferable to do in person but may not be possible.

Rebecca: Product Manager
Tyson: SCRUM Master

Theese roles will be alternated throughout the project each week.

## Consequences

Weekends, Tuesdays and Mondays are the most productive working days for all members of the project. Stand ups will happen in person whenever possible, but with diverse schedule of members, may have to use WhatsApp video chats instead.<file_sep>jest.mock('../app/models/db')
let authenticate = require('../app/models/authenticate')
const crypto = require('crypto')

describe('testing authentication', () => {
  let userInfo = {
    emailAddress: '<EMAIL>',
    password: '<PASSWORD>',
    userID: 12345678
  }

  const userHash = crypto.createHash('sha256')
  userHash.update(userInfo.emailAddress + userInfo.password)
  let userHashKey = userHash.digest('hex')

  const googleUserHash = crypto.createHash('sha256')
  googleUserHash.update(userInfo.userID + userInfo.password)
  let googleUserHashKey = googleUserHash.digest('hex')

  test('correct user hash key generated', () => {
    let user = authenticate.createHashKey(userInfo, false)
    expect(user.hash).toEqual(userHashKey)
  })

  test('correct google user hash key generated', () => {
    let googleUser = authenticate.createHashKey(userInfo, true)
    expect(googleUser.hash).toEqual(googleUserHashKey)
  })
})<file_sep>'use strict'
jest.mock('../app/models/db')
let logModel = require('../app/models/logModel')

describe('testing createLog', () => {
  let logItem = {
    id: '0',
    userId: 'random',
    code: '1',
    date: '2019-01-01',
    importance: 'True',
    tripId: 'random'
  }
  let log = [logItem]

  test('createLogQueryString is correct when creating a new log', () => {
    let queryString = logModel.createLogQueryString(log)
    let expectedQueryString = `INSERT INTO log VALUES ('0','random','1','2019-01-01','True','random');`
    expect(queryString).toEqual(expectedQueryString)
  })

  test('testing createLogQuery', async () => {
    let queryString = logModel.createLogQueryString(log)
    let response = await logModel.createLogQuery(queryString)
    expect(Object.keys(response).length).toEqual(1)
    expect(response.recordset).toEqual(undefined)
  })
})

describe('testing getLogs', () => {
  test('testing getLogQuery', async () => {
    let tripId = 0
    let logs = await logModel.getLogQuery(tripId)
    expect(logs.recordset[0].id).toEqual('0')
    expect(logs.recordset[0].userId).toEqual('random')
    expect(logs.recordset[0].code).toEqual('1')
    expect(logs.recordset[0].date).toEqual('2019-01-01')
    expect(logs.recordset[0].importance).toEqual('True')
    expect(logs.recordset[0].tripId).toEqual('random')
  })
})<file_sep># 1. Entering locations/destinations for a trip

Date: 2019-04-09

## Status

Accepted 

## Context

Destinations need to be entered into a trip somehow. The two most obvious choices seem to be by typing (some kind of auto-completion feature) or by clicking directly on a map, to set markers. These paradigms are the dominant ones in most existing APIs and site/map websites.

## Decision

We will aim to support both autocomplete AND clicking on the map. This would be the most convenient for users of the site. 

## Consequences

Two methods of adding destinations will mean two (or more) additional functions in code, more complexity and added maintenance and testing. The methods will both need to work on Mobile/Responsive well.

 * Vastly better UX
 * Additional UI work
 * Additional Testing
 * Additional Code
 * Additional code complexity
<file_sep>'use strict'

let db = require('./db')
let SqlString = require('sqlstring')

function createDestination (tripInfo, res) {
  let trip = tripInfo.body
  let queryString = createDestinationQueryString(trip)
  createDestinationQuery(queryString)
    .then(result => {
      // console.log('destination table population result ', result)
      res.send('DestinationTablePopulated')
    })
    .catch(err => {
      console.log('populate destination table error:', err)
    })
}

function createDestinationQueryString (trip) {
  let queryString = SqlString.format('DELETE FROM destinations WHERE trip_id = ?;', [trip.id])
  for (let i = 0; i < trip.destinationList.length; i++) {
    queryString = queryString + SqlString.format('INSERT INTO destinations VALUES (?,?,?,?,?,?,?,?);',
      [trip.destinationList[i].id,
        trip.destinationList[i].lat,
        trip.destinationList[i].lng,
        trip.destinationList[i].placeId,
        trip.destinationList[i].place,
        trip.destinationList[i].name,
        trip.destinationList[i].ordering,
        trip.id])
  }
  return queryString
}

function createDestinationQuery (queryString) {
  return db.pools
    .then(pool => {
      return pool.request()
        .query(queryString)
    })
}

module.exports = {
  createDestination: createDestination,
  createDestinationQueryString: createDestinationQueryString,
  createDestinationQuery: createDestinationQuery
}
<file_sep>Main Site:
[![Build Status](https://travis-ci.com/witseie-elen4010/2019-005-project.svg?token=<KEY>&branch=master)](https://travis-ci.com/witseie-elen4010/2019-005-project)
Test Site:
[![Build Status](https://travis-ci.com/witseie-elen4010/2019-005-project.svg?token=<KEY>&branch=development)](https://travis-ci.com/witseie-elen4010/2019-005-project)

Main Site: https://awaywego.azurewebsites.net <br>
Test Site: https://testawaywego.azurewebsites.net

# Away We Go # 
## A group travel trip planning web application.##
### ELEN4010 Group Project ###
###### <NAME> 1239448 ######
###### <NAME> 1066586 ######
###### <NAME> 1364103 #####
###### <NAME> 1056673 ######
---

Database:   <br>
      DB_SERVER=awaywegoserver.database.windows.net  <br>
      DB_NAME=AwayWeGoDatabase  <br>
      DB_ADMIN=softwareprojectadmin  <br>
      DB_PASSWORD=<PASSWORD>  <br>
      DB_PORT=1433  <br>

Test Database:  <br>
      DB_SERVER=testawaywegoserver.database.windows.net  <br>
      DB_NAME=TestAwayWeGoDatabase  <br>
      DB_ADMIN=softwareprojectadmin  <br>
      DB_PASSWORD=<PASSWORD>  <br>
      DB_PORT=1433  <br>

Travis: https://travis-ci.com/witseie-elen4010/2019-005-project <br>
Coveralls: https://coveralls.io/repos/github/witseie-elen4010/2019-005-project

EDIT (13 Nov 2019): All API Keys have been destroyed for all contributors from their respective Google Developer Console.
Note that both the Test and Main site described above have been shut down.
<file_sep>const { Builder, By, Key, until } = require('selenium-webdriver')

require('selenium-webdriver/chrome')

require('selenium-webdriver/firefox')

require('chromedriver')

require('geckodriver')

const rootURL = 'https://testawaywego.azurewebsites.net/sign-in'

const d = new Builder().forBrowser('chrome').build()
const waitUntilTime = 20000
let driver, el, actual, expected
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 5

async function getElementById(id) {
    const el = await driver.wait(until.elementLocated(By.id(id)), waitUntilTime)
    return await driver.wait(until.elementIsVisible(el), waitUntilTime)
}
  
async function getElementByXPath(xpath) {
    const el = await driver.wait(until.elementLocated(By.xpath(xpath)), waitUntilTime)
    return await driver.wait(until.elementIsVisible(el), waitUntilTime)
}

test('waits for the driver to start', async () => {
    return d.then(_d => {
      driver = _d
    })
})
  
test('initialises the context', async () => {
    await driver.manage().window().setPosition(0, 0)
    await driver.manage().window().setSize(1280, 1024)
    await driver.get(rootURL)
})

describe('testing map markers', () => {
    test('Marker can be found', async () => {
        // sign in to access trip page
        let email = await getElementById('inputEmail')
        email.sendKeys('<EMAIL>')
        let password = await getElementById('inputPassword')
        password.sendKeys('r') // correct password for email
        let button = await getElementById('signInPageSignInButton')
        button.click()


        // // A marker needs to exist on the map in order to be found. This can be done using 1 or 2:

        // // Option 1: create a marker on the map
        // location = await getElementById('pac-input')
        // location.sendKeys('Cape Town')
        // CT = await driver.wait(until.elementLocated(By.className('pac-container pac-logo')), waitUntilTime)
        // CT.click()

        // // Option 2: go to an existing trip on trip manager to load markers on trip page
        // driver.navigate().GoToUrl('https://testawaywego.azurewebsites.net/trip-manager')
        // title = await getElementById('Test trip')
        // title.click()
        // edit = await driver.wait(until.elementLocated(By.className('editTrip btn btn-sm btn-secondary')), waitUntilTime)
        // edit.click() // this redirects back to trip page with destinations loaded
        
        // // Find the marker on the map
        // map = await getElementById('map')
        // marker = await map.getAttribute('gmimap0')

        // // Check the attributes of the marker
        // actualShape = await marker.getAttribute('shape')
        // expectedShape = 'poly'
        // expect(actualShape).toEqual(expectedShape)

        // actualTitle = await el.getAttribute('title')
        // expectedTitle = ''
        // expect(actualTitle).toEqual(expectedTitle)

        // actualStyle = await el.getAttribute('style')
        // expectedStyle = 'cursor: pointer; touch-action: none;'
        // expect(actualStyle).toEqual(expectedStyle)
    })
})

afterAll(() => {
    driver.quit()
});<file_sep># 1. Title

Date: 2019-05-20

## Status

Accepted

## Context

In order to join a group, a potential member must be invited (it's not correct to add a person to a group without asking permission.) A person invited can either be a member of the website (have an account) or be a new user (no account registered yet). In order to cover both of these scenarios, and to avoid the website being a "walled garden" with a tiny set of users, and to encourage potential future growt, a mechanism to invite users could be an email, sent by an existing member, to any valid email address with an invitation to join. This could be in the form of a token with a payload, or more simply, an extra table in the DB, linking the invited person's email to a trip ID. 

## Decision

The mechanism of an external invitation with a specific link requires the ability to send an email (prefereably attractively  styled and clearly phrased, to avoid being rejected as unsolicited or junk email). The node module 'nodemailer' was selected as appropriate, for its wide support, mature development and ease of use, and 0 dependecies.

## Consequences

Nodemailer adds extra dependecies to node, increasing the number of modules that must be maintained and installed. This is a minor concern. The restrictions that a user must use the same email that they were invited with to login to the website in order to be included in the trip invite that was sent, is a more subtle problem, that can only be migitated by instructing the user in the original email to please use the same email address.

 * Invites must be visible as notifications, or on the Trip Manager page
 * Once rejected, or added, an invite must be removed from the 'invites' table to avoid annoying or confusing the user.
 * MIT license, means high compatibility with existing licenses on other node modules.<file_sep>let preamble = [
  'When you create an account with us, you must provide us information that is accurate, complete, and current at all times. Failure to do so constitutes a breach of the Terms, which may result in immediate termination of your account on our Service.',
  'You are responsible for safeguarding the password that you use to access the Service and for any activities or actions under your password, whether your password is with our Service or a third-party service.',
  'You agree not to disclose your password to any third party. You must notify us immediately upon becoming aware of any breach of security or unauthorized use of your account.'
]

let accounts = [
  'When you create an account with us, you must provide us information that is accurate, complete, and current at all times. Failure to do so constitutes a breach of the Terms, which may result in immediate termination of your account on our Service',
  'You are responsible for safeguarding the password that you use to access the Service and for any activities or actions under your password, whether your password is with our Service or a third-party service',
  'You agree not to disclose your password to any third party. You must notify us immediately upon becoming aware of any breach of security or unauthorized use of your account'
]

let otherSites = [
  'Our Service may contain links to third-party web sites or services that are not owned or controlled by Away We Go',
  'Away We Go has no control over, and assumes no responsibility for, the content, privacy policies, or practices of any third party web sites or services. You further acknowledge and agree that Away We Go shall not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with use of or reliance on any such content, goods or services available on or through any such web sites or services.',
  'We strongly advise you to read the terms and conditions and privacy policies of any third-party web sites or services that you visit.'
]

let getTermsAndCondtions = function () {
  let termsObj = { 'preamble': preamble,
    'accounts': accounts,
    'otherSites': otherSites
  }
  return termsObj
}

module.exports = {
  getTermsAndCondtions
}
<file_sep># 1. Title

Date: 2019-03-01

## Status

Accepted / Not Accepted

## Context

Explain the context

## Decision

What was decided

## Consequences

The consequences and outcomes of the decision

 * Consequence 1
 * Consequence 2<file_sep># Testing

This folder contains all the automated testing for the core functionality of the test. The testing framework used is Jest, and the end-end testing framework used is Selenium WebDriver. This file contains an overview of the tests performed in every test folder. 

## authenticate.test
This file tests:
- Generation of user hash keys
- Generation of Google user hash keys

## groups.test
This file unit-tests the groups model, which is responsible for generating database query strings for data relating to groups of individuals who edit a given trip. These tests include:
- Testing the validity of the SQL query strings passed to the database for retrieving group members according to user hashes
- Testing the validity of the SQL query strings passed to the database for retrieving group members according to the trip ID 

## invites.test
This file contains unit-tests for the invites model, which is responsible for generating database query strings for data relating to invites sent to email addresses. These tests include: 
- Adding invites to the invites table
- Getting invites for a particular user

## invitesFrontEnd.test
This file contains automated front-end testing for the invites modal on the website. This file tests: 
- The functionality of the "INVITE SOMEBODY TO JOIN THIS TRIP" button
- Front-end email format validation
- The validity of the confirmation message after an email is sent

## log.test
This file contains unit-tests for the log model, which is responsible for generating database query strings for data relating to logs about changes made to a trip. These tests include: 
- Creating logs
- Retrieving logs

## sign-in.test
This file contains automated front-end testing for the functionality of the sign-in page of the website. This file tests: 
- The necessary HTML DOM elements are displayed correctly for login
- The ability to successfully login once the correct user credentials are supplied

## termsAndConditions.test
This file contains unit-tests for the terms and conditions model, which is responsible for providing terms and conditions to be rendered on the webpage. These tests include: 
- Testing that the preamble returned is correct
- Testing that the terms and conditions relating to accounts is correct
- Testing that the terms and conditions relating to the other sites is correct

## termsAndConditionsFrontEnd.test
This file contains automated front-end testing that the terms and conditions from the model are rendered correctly on the on the website: These tests include: 
- Testing that the preamble returned is correct
- Testing that the terms and conditions relating to accounts is correct
- Testing that the terms and conditions relating to the other sites is correct

## trip-manager.test
This file contains unit-tests for the trip-manager model, which is responsible for generating the database query strings for data relating to the trips associated with a given user. These tests include: 
- Retrieving trip titles
- Adding trip titles
- Retrieving destinations that are part of a trip

## trip.test 
This file contains unit-tests for the trips model, which is responsible for generating the database query strings for the destinations table. These tests include: 
- Adding destinations
- Deleting destinations

## user.test
This file contains unit-tests for users model, which is responsible for generating the database query strings for the users table. These tests include: 
- Creating new users
- Finding existing users 

<file_sep>jest.mock('../app/models/db')
let userModel = require('../app/models/userModel')

describe('testing userModel', () => {
  test('test that user information gets deleted in fail to sign-in/register server responses', () => {
    let userInfo = {
      firstName: 'Some',
      lastName: 'Person',
      emailAddress: '<EMAIL>',
      password: '<PASSWORD>',
      userID: 12345678,
      image: 'http://some.url.com/image.jpg',
      hash: 'a1b2c3d4e5f6g7h8i9',
      userType: 'incorrectUser'
    }
    let expectServerInfoResponse = { userType: 'incorrectUser' }
    userModel.deleteUnnecessaryInfo(userInfo)
    expect(userInfo).toEqual(expectServerInfoResponse)
  })

  test('test findUserQuery', async () => {
    let email = '<EMAIL>'
    let user = await userModel.findUserQuery(email)
    expect(user.recordset[0].first_name).toEqual('Some')
    expect(user.recordset[0].last_name).toEqual('Person')
    expect(user.recordset[0].email_address).toEqual('<EMAIL>')
    expect(user.recordset[0].image_url).toEqual('http://image.com/image.jpg')
    expect(user.recordset[0].hash).toEqual('q1w2e3r4t5y6u7i8o9')
  })

  test('test createUserQuery', async () => {
    let userInfo = {
      firstName: 'Some',
      lastName: 'Person',
      emailAddress: '<EMAIL>',
      password: '<PASSWORD>',
      userID: 12345678,
      image: 'http://some.url.com/image.jpg',
      hash: 'a1b2c3d4e5f6g7h8i9',
      userType: 'incorrectUser'
    }
    let createUser = await userModel.createUserQuery(userInfo)
    expect(Object.keys(createUser).length).toEqual(1)
    expect(createUser.recordset).toEqual(undefined)
  })
})<file_sep>'use strict'

let db = require('./db')

function findUserQuery (email) {
  return db.pools
  // Run query
    .then((pool) => {
      let dbrequest = pool.request()
      dbrequest.input('email', email)
      return dbrequest.query(`SELECT * FROM users WHERE email_address = @email`)
    })
}

function findUser (userInfo, signin, res) {
  let info = userInfo
  let email = info.emailAddress
  findUserQuery(email)
    // both ID/Email match sought after in case of possible duplication of either ID/Email
    .then(result => {
      // if no match is found, it must be a new user
      if (result.recordset.length === 0) {
        info.userType = 'newUser'
        // if user doesn't exist and tries to sign in
        if (signin) {
          deleteUnnecessaryInfo(info)
          res.send(info)
        } else {
          createUser(info, res)
        }
      } else {
        info.userType = 'currentUser'
        // account that does exist and is trying to register
        if (!signin) {
          // console.log('trying to register')
          deleteUnnecessaryInfo(info)
          res.send(info)
        } else if (result.recordset[0].hash === info.hash) {
          // account that exists and is trying to sign in with the correct password
          // console.log('correct sign in')
          info.firstName = result.recordset[0].first_name
          info.lastName = result.recordset[0].last_name
          info.emailAddress = result.recordset[0].email_address
          info.hash = result.recordset[0].hash
          // console.log('lastly current', info)
          res.send(info)
        } else {
          // account that exists and is trying to sign in with the wrong password
          // console.log('incorrect sign in')
          info.userType = 'incorrectUser'
          deleteUnnecessaryInfo(info)
          res.send(info)
        }
      }
    })
    .catch(err => {
      console.log('find user error', err)
    })
}

function createUserQuery (info) {
  return db.pools
    // Run query
    .then((pool) => {
      let dbrequest = pool.request()
      dbrequest.input('firstName', info.firstName)
      dbrequest.input('lastName', info.lastName)
      dbrequest.input('emailAddress', info.emailAddress)
      dbrequest.input('image', info.image)
      dbrequest.input('hash', info.hash)
      return dbrequest.query(`INSERT INTO users VALUES(@firstName,@lastName,@emailAddress,@image,@hash)`)
    })
}

function createUser (userInfo, res) {
  let info = userInfo
  createUserQuery(info)
    // Send back the result
    .then(result => {
      res.send(info)
    })
    // If there's an error, return that with some description
    .catch(err => {
      console.log('create users error', err)
    })
}

function deleteUnnecessaryInfo (info) {
  delete info.emailAddress
  delete info.password
  delete info.hash
  delete info.image
  delete info.firstName
  delete info.lastName
  delete info.userID
}

module.exports = {
  findUser: findUser,
  deleteUnnecessaryInfo: deleteUnnecessaryInfo,
  findUserQuery: findUserQuery,
  createUserQuery: createUserQuery
}
<file_sep># 1. Use of GitHub's Project feature for a KanBan

Date: 2019-04-09

## Status

Accepted 

## Context

A SCRUM-based agile devlopment workflow would benefit from a central KANBAN board to keep track of userstories that have been written, are in progress, and are complete. This will help identify the sprint backlog, and the current focus of the sprint. Labels could be used to indicate size/priority/difficuly or value to the project, to help calculate the sprint velocity and determine what can get done inside a single sprint.

## Decision

Using the GitHib Project page with a single project for the repo, and using Issues labelled as User Stories, with columns for "To Do", "In progress", and "Completed".  We can leverage some of the automatic rules in Git to help automate some of the completetion of tasks ties to Milestones for each sprint:

https://github.com/witseie-elen4010/2019-005-project/projects/1

## Consequences

Manually Use story cards used so far will become redundant. Need to tie physical cards into the digital version in the interim. Will not maintain the physical cards beyond Sprint 1.

 * simplifies workflow
 * Directly ties git issues and User Stories to individual developers
 * Easy to modify an existing User story - need to maintain discipline to avoid retroactively modifying tasks
 * Acceptance tests can be specified as sub-issues, or as checkboxes on an issue.
<file_sep>'use strict'

const $ = window.$

export default function addTermsToSubsection (subsection, termsArray, divider) {
  let parentItemType = ''
  if (divider === 'li') {
    parentItemType = 'ol'
  } else {
    parentItemType = 'p'
  }
  let parentItem = document.createElement(parentItemType)
  termsArray.forEach(element => {
    let item = document.createElement(divider)
    let term = document.createTextNode(element)
    item.appendChild(term)
    parentItem.appendChild(item)
  })
  subsection.append(parentItem)
}

$(document).ready(() => {
  $.ajax({
    url: '/terms_and_conditions/data',
    type: 'GET',
    success: (data) => {
      addTermsToSubsection($('#Preamble'), data.preamble, 'p')
      addTermsToSubsection($('#Accounts'), data.accounts, 'li')
      addTermsToSubsection($('#OtherSites'), data.otherSites, 'li')
    }
  })
})
<file_sep>const { Builder, By, Key, until } = require('selenium-webdriver')

require('selenium-webdriver/chrome')

require('selenium-webdriver/firefox')

require('chromedriver')

require('geckodriver')

const rootURL = 'https://testawaywego.azurewebsites.net/sign-in'

const d = new Builder().forBrowser('chrome').build()
const waitUntilTime = 20000
let driver, el, actual, expected
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 5

async function getElementById(id) {
    const el = await driver.wait(until.elementLocated(By.id(id)), waitUntilTime)
    return await driver.wait(until.elementIsVisible(el), waitUntilTime)
}

async function getElementByClass(className) {
    const el = await driver.wait(until.elementLocated(By.className(className)), waitUntilTime)
    return await driver.wait(until.elementIsVisible(el), waitUntilTime)
}
  
async function getElementByXPath(xpath) {
    const el = await driver.wait(until.elementLocated(By.xpath(xpath)), waitUntilTime)
    return await driver.wait(until.elementIsVisible(el), waitUntilTime)
}

test('waits for the driver to start', async () => {
    return d.then(_d => {
      driver = _d
    })
})
  
test('initialises the context', async () => {
    await driver.manage().window().setPosition(0, 0)
    await driver.manage().window().setSize(1280, 1024)
    await driver.get(rootURL)
})


describe('test invites modal', () => {
    let validEmailAddress = '<EMAIL>'
    let invalidEmailAddress = 'invalidEmailAddress.com'
    
    test('Invites modal appears when "INVITE SOMEBODY TO JOIN THIS TRIP" is pressed', async () => {
        let email = await getElementById('inputEmail')
        email.clear()
        email.sendKeys('<EMAIL>')
        let password = await getElementById('inputPassword')
        password.clear()
        password.sendKeys('r') // correct password for email
        let button = await getElementById('signInPageSignInButton')
        button.click()

        // wait for an element that is unique to the trip page to be found before getting the URL
        await getElementById('pac-input')

        let inviteSomebodyButton = await getElementById('inviteEditorButton')
        inviteSomebodyButton.click()
        let invitesModal = await getElementByClass('modal')
        let modalVisibility = invitesModal.isDisplayed()
        expect(modalVisibility).toBeTruthy()
    })

    test('test if an a warning message appears after an invalid email address is entered', async () => {
        let emailAddressField = await getElementById('emailAddressField')
        emailAddressField.clear()
        emailAddressField.sendKeys(invalidEmailAddress)

        let inviteButton = await getElementById('inviteEmailAddressButton')
        inviteButton.click()

        let warningMessgae = await getElementById('inviteEmailAddressButton')
        warningMessageVisibility = await warningMessgae.isDisplayed()

        expect(warningMessageVisibility).toBeTruthy()
    })

    test('test if an a warning message appears after an invalid email address is entered', async () => {
        let emailAddressField = await getElementById('emailAddressField')
        emailAddressField.clear()
        emailAddressField.sendKeys('invalidEmailAddress.com')

        let inviteButton = await getElementById('inviteEmailAddressButton')
        inviteButton.click()

        let warningMessgae = await getElementById('inviteEmailAddressButton')
        warningMessageVisibility = await warningMessgae.isDisplayed()
        
        expect(warningMessageVisibility).toBeTruthy()
    })
    test('test if an a "group invite sent" message appears after a valid email address is entered', async () => {
        let emailAddressField = await getElementById('emailAddressField')
        emailAddressField.clear()
        emailAddressField.sendKeys(validEmailAddress)

        let inviteButton = await getElementById('inviteEmailAddressButton')
        inviteButton.click()

        let inviteSentMessage = await getElementById('inviteSendHeader')
        inviteSentMessageVisibility = await inviteSentMessage.isDisplayed()
        expect(inviteSentMessageVisibility).toBeTruthy()
    })
    test('test if the email addres in the "group invite sent" message is correct', async () => {
        let inviteSentAddress = await getElementById('InviteEmailAddress')
        inviteSentAddressText = await inviteSentAddress.getText()
        expect(inviteSentAddressText).toBe(validEmailAddress)
    })
})
afterAll(() => {
    driver.quit()
});<file_sep># 1. Trunk-Based Development

Date: 2019-04-24
Modified: 2019-05-02 (Tyson)
Modified: 2019-05-07 (Tyson)

## Status

Accepted

## Context

To perform Continual Integration and development, with weekly releases, it would be convenient and useful to have a testing branch as well. Accidental pull requests into the main branch may introduce features that have not been tested from the interfac/front-end. It is difficult to automate these front-end interface tests, and there may be factors not present in a localhost/express server that only become apparent in an online scanario.

The use of **master** branch as the release branch is useful, as 'master' is usually the most protected on GitHub, with the most warnings about deleting, modifying, etc. 

Code reviews ar essential from all developers, to become familiar with each other's code, and to learn about javascript, and web-development. THis way we all learn from each other, and also learn good review and communicaton practice.

## Decision

**master** will be the release branch

**development** will be the main development/test branch. This will also be made into the "default" branch for all pull requests, to avoid accidentaly PR into master

**feature** branches must be made off development, with unique names. All pull requests for completed features to be made into "development".

* All PRs must be reviewed by at least two developers to merge into "development"
* All PRs must be reviewed by at the three other developers to merge into "master"
* All PRs must pass all tests (Jest, Travis, and Coveralls) in order to be considered valid for a merge
* Stale reviews will be automatically dismissed if a new commit is pushed to the same branch 
* Accepted PRs for completed features (User Stories) should be deleted after sucessfully merging

## Consequences

This setup complexifies the deployment requirements, and a seperate test website will need to be setup in Azure.
Travis CI -> main site / GitHub CI -> directly to test site

**Update** Coveralls has a problem with repos that use "development" as their main branch: cannot see "master" at all.

**Update** Coveralls has resolved its error with not seeing "master" - logged issue and was fixed

**Update** Change in Admin rights on GitHut means we can no longer deploy directly to testawaywego from GitHub. Instead, Travis will use azure push features with branch rules to both sites (on: master / on: development) but lack of deployment slots means that we must use the same login/password between both test and main sites, with script-specified config for the sitename

**Update** The Travis azure deplyment is very flaky and doesn't complete the deployment predictably. Instead, a script has been written which directly issues a git force push command to send the repo from Travis to azure, if the tests complete sucessfully.


