diff --git a/.gitignore b/.gitignore index b8b6cad5141..4fcbc57e59d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ bower_components/ node_modules/ + +bundle/bundle.out.js + .idea/ *.iml my.env @@ -15,3 +18,5 @@ static/bower_components/ # istanbul output coverage/ + +npm-debug.log diff --git a/.travis.yml b/.travis.yml index 99660614597..562c8d9edbb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,8 @@ language: node_js +sudo: false node_js: - - "0.10" - - "0.11" -matrix: - allow_failures: - - node_js: "0.11" -services: - - mongodb -before_script: - - sleep 10 - - echo mongo mongo_travis -script: - - make travis + - "0.10" + - "0.12" +services: mongodb +script: make travis +after_script: make report diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dbac299d8e1..6169a7cbc4a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,19 @@ + + +**Table of Contents** + +- [Contributing to cgm-remote-monitor](#contributing-to-cgm-remote-monitor) + - [Design](#design) + - [Develop on `dev`](#develop-on-dev) + - [Style Guide](#style-guide) + - [Create a prototype](#create-a-prototype) + - [Submit a pull request](#submit-a-pull-request) + - [Comments and issues](#comments-and-issues) + - [Co-ordination](#co-ordination) + - [Other Dev Tips](#other-dev-tips) + + + # Contributing to cgm-remote-monitor @@ -12,15 +28,14 @@ [build-url]: https://travis-ci.org/nightscout/cgm-remote-monitor [dependency-img]: https://img.shields.io/david/nightscout/cgm-remote-monitor.svg [dependency-url]: https://david-dm.org/nightscout/cgm-remote-monitor -[coverage-img]: https://img.shields.io/coveralls/nightscout/cgm-remote-monitor/coverage.svg -[coverage-url]: https://coveralls.io/r/nightscout/cgm-remote-monitor?branch=dev +[coverage-img]: https://img.shields.io/coveralls/nightscout/cgm-remote-monitor/master.svg +[coverage-url]: https://coveralls.io/r/nightscout/cgm-remote-monitor?branch=master [gitter-img]: https://img.shields.io/badge/Gitter-Join%20Chat%20%E2%86%92-1dce73.svg [gitter-url]: https://gitter.im/nightscout/public [ready-img]: https://badge.waffle.io/nightscout/cgm-remote-monitor.svg?label=ready&title=Ready [waffle]: https://waffle.io/nightscout/cgm-remote-monitor [progress-img]: https://badge.waffle.io/nightscout/cgm-remote-monitor.svg?label=in+progress&title=In+Progress - ## Design Participate in the design process by creating an issue to discuss your @@ -31,6 +46,24 @@ design. We develop on the `dev` branch. You can get the dev branch checked out using `git checkout dev`. +## Style Guide + +Some simple rules, that will make it easier to maintain our codebase: + +* All indenting should use 2 space where possible (js, css, html, etc) +* A space before function parameters, such as: `function boom (name, callback) { }`, this makes searching for calls easier +* Name your callback functions, such as `boom('the name', function afterBoom ( result ) { }` +* Don't include author names in the header of your files, if you need to give credit to someone else do it in the commit comment. +* Use the comma first style, for example: + + ```javascript + var data = { + value: 'the value' + , detail: 'the details...' + , time: Date.now() + }; + ``` + ## Create a prototype Fork cgm-remote-monitor and create a branch. @@ -78,3 +111,13 @@ the version correctly. See sem-ver for versioning strategy. Every commit is tested by travis. We encourage adding tests to validate your design. We encourage discussing your use cases to help everyone get a better understanding of your design. + +## Other Dev Tips + +* Join the [Gitter chat][gitter-url] +* Get a local dev environment setup if you haven't already +* Try breaking up big features/improvements into small parts. It's much easier to accept small PR's +* Create tests for your new code, and for the old code too. We are aiming for a full test coverage. +* If your going to be working in old code that needs lots of reformatting consider doing the clean as a separate PR. +* If you can find others to help test your PR is will help get them merged in sooner. + diff --git a/Makefile b/Makefile index 15dc02705ff..2b60d26001b 100644 --- a/Makefile +++ b/Makefile @@ -1,25 +1,51 @@ +# Nightscout tests/builds/analysis TESTS=tests/*.js MONGO_CONNECTION?=mongodb://localhost/test_db CUSTOMCONNSTR_mongo_settings_collection?=test_settings CUSTOMCONNSTR_mongo_collection?=test_sgvs MONGO_SETTINGS=MONGO_CONNECTION=${MONGO_CONNECTION} \ - CUSTOMCONNSTR_mongo_collection=${CUSTOMCONNSTR_mongo_collection} \ - CUSTOMCONNSTR_mongo_settings_collection=${CUSTOMCONNSTR_mongo_settings_collection} + CUSTOMCONNSTR_mongo_collection=${CUSTOMCONNSTR_mongo_collection} + +# XXX.bewest: Mocha is an odd process, and since things are being +# wrapped and transformed, this odd path needs to be used, not the +# normal wrapper. When ./node_modules/.bin/mocha is used, no coverage +# information is generated. This happens because typical shell +# wrapper performs process management that mucks with the test +# coverage reporter's ability to instrument the tests correctly. +# Hard coding it to the local with our pinned version is bigger for +# initial installs, but ensures a consistent environment everywhere. +# On Travis, ./node_modules/.bin and other `nvm` and `npm` bundles are +# inserted into the default `$PATH` enviroinment, making pointing to +# the unwrapped mocha executable necessary. +MOCHA=./node_modules/mocha/bin/_mocha +# Pinned from dependency list. +ISTANBUL=./node_modules/.bin/istanbul +ANALYZED=./coverage/lcov.info +export CODACY_REPO_TOKEN=e29ae5cf671f4f918912d9864316207c all: test -travis-cov: - NODE_ENV=test \ - ${MONGO_SETTINGS} \ - istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -vvv -R tap ${TESTS} && \ - cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && \ - rm -rf ./coverage +coverage: + NODE_ENV=test ${MONGO_SETTINGS} \ + ${ISTANBUL} cover ${MOCHA} -- -R tap ${TESTS} + +report: + test -f ${ANALYZED} && \ + (npm install coveralls && cat ${ANALYZED} | \ + ./node_modules/.bin/coveralls) || echo "NO COVERAGE" + test -f ${ANALYZED} && \ + (npm install codecov.io && cat ${ANALYZED} | \ + ./node_modules/codecov.io/bin/codecov.io.js) || echo "NO COVERAGE" + test -f ${ANALYZED} && \ + (npm install codacy-coverage && cat ${ANALYZED} | \ + YOURPACKAGE_COVERAGE=1 ./node_modules/codacy-coverage/bin/codacy-coverage.js) || echo "NO COVERAGE" test: - ${MONGO_SETTINGS} \ - mocha --verbose -vvv -R tap ${TESTS} + ${MONGO_SETTINGS} ${MOCHA} -R tap ${TESTS} -travis: test travis-cov +travis: + NODE_ENV=test ${MONGO_SETTINGS} \ + ${ISTANBUL} cover ${MOCHA} --report lcovonly -- -R tap ${TESTS} -.PHONY: test +.PHONY: all coverage report test travis diff --git a/Procfile b/Procfile index 489b2700aca..32dd1c83adc 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: node server.js +web: ./node_modules/.bin/forever --minUptime 100 -c node server.js diff --git a/README.md b/README.md index e402869fd33..fff1d1180a6 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,12 @@ -cgm-remote-monitor (a.k.a. Nightscout) +Nightscout Web Monitor (a.k.a. cgm-remote-monitor) ====================================== +![nightscout horizontal](https://cloud.githubusercontent.com/assets/751143/8425633/93c94dc0-1ebc-11e5-99e7-71a8f464caac.png) + [![Build Status][build-img]][build-url] [![Dependency Status][dependency-img]][dependency-url] [![Coverage Status][coverage-img]][coverage-url] +[![Codacy Badge][codacy-img]][codacy-url] [![Gitter chat][gitter-img]][gitter-url] [![Stories in Ready][ready-img]][waffle] [![Stories in Progress][progress-img]][waffle] @@ -19,6 +22,8 @@ and blood glucose values are predicted 0.5 hours ahead using an autoregressive second order model. Alarms are generated for high and low values, which can be cleared by any watcher of the data. +# [#WeAreNotWaiting](https://twitter.com/hashtag/wearenotwaiting?src=hash&vertical=default&f=images) and [this](https://vimeo.com/109767890) is why. + Community maintained fork of the [original cgm-remote-monitor][original]. @@ -28,6 +33,8 @@ Community maintained fork of the [dependency-url]: https://david-dm.org/nightscout/cgm-remote-monitor [coverage-img]: https://img.shields.io/coveralls/nightscout/cgm-remote-monitor/master.svg [coverage-url]: https://coveralls.io/r/nightscout/cgm-remote-monitor?branch=master +[codacy-img]: https://www.codacy.com/project/badge/f79327216860472dad9afda07de39d3b +[codacy-url]: https://www.codacy.com/app/Nightscout/cgm-remote-monitor [gitter-img]: https://img.shields.io/badge/Gitter-Join%20Chat%20%E2%86%92-1dce73.svg [gitter-url]: https://gitter.im/nightscout/public [ready-img]: https://badge.waffle.io/nightscout/cgm-remote-monitor.svg?label=ready&title=Ready @@ -37,8 +44,38 @@ Community maintained fork of the [heroku-url]: https://heroku.com/deploy [original]: https://github.com/rnpenguin/cgm-remote-monitor -Install ---------------- + + +**Table of Contents** + +- [Install](#install) +- [Usage](#usage) + - [Updating my version?](#updating-my-version) + - [What is my mongo string?](#what-is-my-mongo-string) + - [Configure my uploader to match](#configure-my-uploader-to-match) + - [Nightscout API](#nightscout-api) + - [Example Queries](#example-queries) + - [Environment](#environment) + - [Required](#required) + - [Features/Labs](#featureslabs) + - [Alarms](#alarms) + - [Core](#core) + - [Predefined values for your browser settings (optional)](#predefined-values-for-your-browser-settings-optional) + - [Plugins](#plugins) + - [Default Plugins](#default-plugins) + - [Built-in/Example Plugins:](#built-inexample-plugins) + - [Extended Settings](#extended-settings) + - [Pushover](#pushover) + - [IFTTT Maker](#ifttt-maker) + - [Treatment Profile](#treatment-profile) + - [Setting environment variables](#setting-environment-variables) + - [Vagrant install](#vagrant-install) + - [More questions?](#more-questions) + - [License](#license) + + + +# Install Requirements: @@ -50,62 +87,95 @@ Clone this repo then install dependencies into the root of the project: $ npm install ``` -Usage ---------------- +#Usage The data being uploaded from the server to the client is from a -MongoDB server such as [mongolab][mongodb]. In order to access the -database, the appropriate credentials need to be filled into the -[JSON][json] file in the root directory. SGV data from the database -is assumed to have the following fields: date, sgv. Once all that is -ready, just host your web app on your service of choice. +MongoDB server such as [mongolab][mongodb]. [mongodb]: https://mongolab.com -[json]: https://github.com/rnpenguin/cgm-remote-monitor/blob/master/database_configuration.json [autoconfigure]: http://nightscout.github.io/pages/configure/ [mongostring]: http://nightscout.github.io/pages/mongostring/ [update-fork]: http://nightscout.github.io/pages/update-fork/ -### Updating my version? +## Updating my version? The easiest way to update your version of cgm-remote-monitor to our latest recommended version is to use the [update my fork tool][update-fork]. It even gives out stars if you are up to date. -### What is my mongo string? +## What is my mongo string? Try the [what is my mongo string tool][mongostring] to get a good idea of your mongo string. You can copy and paste the text in the gray box into your `MONGO_CONNECTION` environment variable. -### Configure my uploader to match +## Configure my uploader to match Use the [autoconfigure tool][autoconfigure] to sync an uploader to your config. -### Environment +## Nightscout API + +The Nightscout API enables direct access to your DData without the need for direct Mongo access. +You can find CGM data in `/api/v1/entries`, Care Portal Treatments in `/api/v1/treatments`, and Treatment Profiles in `/api/v1/profile`. +The server status and settings are available from `/api/v1/status.json`. + +By default the `/entries` and `/treatments` APIs limit results to the the most recent 10 values from the last 2 days. +You can get many more results, by using the `count`, `date`, `dateString`, and `created_at` parameters, depending on the type of data you're looking for. + +#### Example Queries + +(replace `http://localhost:1337` with your base url, YOUR-SITE) + + * 100's: `http://localhost:1337/api/v1/entries.json?find[sgv]=100` + * BGs between 2 days: `http://localhost:1337/api/v1/entries/sgv.json?find[dateString][$gte]=2015-08-28&find[dateString][$lte]=2015-08-30` + * Juice Box corrections in a year: `http://localhost:1337/api/v1/treatments.json?count=1000&find[carbs]=15&find[eventType]=Carb+Correction&find[created_at][$gte]=2015` + * Boluses over 2U: `http://localhost:1337/api/v1/treatments.json?find[insulin][$gte]=2` + +The API is Swagger enabled, so you can generate client code to make working with the API easy. +To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs.html or review [swagger.yaml](swagger.yaml). + + +## Environment `VARIABLE` (default) - description -#### Required +### Required * `MONGO_CONNECTION` - Your mongo uri, for example: `mongodb://sally:sallypass@ds099999.mongolab.com:99999/nightscout` + * `DISPLAY_UNITS` (`mg/dl`) - Choices: `mg/dl` and `mmol`. Setting to `mmol` puts the entire server into `mmol` mode by default, no further settings needed. + * `BASE_URL` - Used for building links to your sites api, ie pushover callbacks, usually the URL of your Nightscout site you may want https instead of http -#### Features/Labs +### Features/Labs - * `ENABLE` - Used to enable optional features, currently supports: `careportal` + * `ENABLE` - Used to enable optional features, expects a space delimited list, such as: `careportal rawbg iob`, see [plugins](#plugins) below + * `DISABLE` - Used to disable default features, expects a space delimited list, such as: `direction upbat`, see [plugins](#plugins) below * `API_SECRET` - A secret passphrase that must be at least 12 characters long, required to enable `POST` and `PUT`; also required for the Care Portal + * `TREATMENTS_AUTH` (`off`) - possible values `on` or `off`. When on device must be authenticated by entering `API_SECRET` to create treatments + + +### Alarms + + These alarm setting effect all delivery methods (browser, pushover, maker, etc), some settings can be overridden per client (web browser) + + * `ALARM_TYPES` (`simple` if any `BG_`* ENV's are set, otherwise `predict`) - currently 2 alarm types are supported, and can be used independently or combined. The `simple` alarm type only compares the current BG to `BG_` thresholds above, the `predict` alarm type uses highly tuned formula that forecasts where the BG is going based on it's trend. `predict` **DOES NOT** currently use any of the `BG_`* ENV's * `BG_HIGH` (`260`) - must be set using mg/dl units; the high BG outside the target range that is considered urgent * `BG_TARGET_TOP` (`180`) - must be set using mg/dl units; the top of the target range, also used to draw the line on the chart * `BG_TARGET_BOTTOM` (`80`) - must be set using mg/dl units; the bottom of the target range, also used to draw the line on the chart * `BG_LOW` (`55`) - must be set using mg/dl units; the low BG outside the target range that is considered urgent - * `ALARM_TYPES` (`simple` if any `BG_`* ENV's are set, otherwise `predict`) - currently 2 alarm types are supported, and can be used independently or combined. The `simple` alarm type only compares the current BG to `BG_` thresholds above, the `predict` alarm type uses highly tuned formula that forecasts where the BG is going based on it's trend. `predict` **DOES NOT** currently use any of the `BG_`* ENV's - * `PUSHOVER_API_TOKEN` - Used to enable pushover notifications for Care Portal treatments, this token is specific to the application you create from in [Pushover](https://pushover.net/) - * `PUSHOVER_USER_KEY` - Your Pushover user key, can be found in the top left of the [Pushover](https://pushover.net/) site + * `ALARM_URGENT_HIGH` (`on`) - possible values `on` or `off` + * `ALARM_URGENT_HIGH_MINS` (`30 60 90 120`) - Number of minutes to snooze urgent high alarms, space separated for options in browser, first used for pushover + * `ALARM_HIGH` (`on`) - possible values `on` or `off` + * `ALARM_HIGH_MINS` (`30 60 90 120`) - Number of minutes to snooze high alarms, space separated for options in browser, first used for pushover + * `ALARM_LOW` (`on`) - possible values `on` or `off` + * `ALARM_LOW_MINS` (`15 30 45 60`) - Number of minutes to snooze low alarms, space separated for options in browser, first used for pushover + * `ALARM_URGENT_LOW` (`on`) - possible values `on` or `off` + * `ALARM_URGENT_LOW_MINS` (`15 30 45`) - Number of minutes to snooze urgent low alarms, space separated for options in browser, first used for pushover + * `ALARM_URGENT_MINS` (`30 60 90 120`) - Number of minutes to snooze urgent alarms (that aren't tagged as high or low), space separated for options in browser, first used for pushover + * `ALARM_WARN_MINS` (`30 60 90 120`) - Number of minutes to snooze warning alarms (that aren't tagged as high or low), space separated for options in browser, first used for pushover -#### Core +### Core - * `DISPLAY_UNITS` (`mg/dl`) - Choices: `mg/dl` and `mmol`. Setting to `mmol` puts the entire server into `mmol` mode by default, no further settings needed. * `MONGO_COLLECTION` (`entries`) - The collection used to store SGV, MBG, and CAL records from your CGM device * `MONGO_TREATMENTS_COLLECTION` (`treatments`) -The collection used to store treatments entered in the Care Portal, see the `ENABLE` env var above * `MONGO_DEVICESTATUS_COLLECTION`(`devicestatus`) - The collection used to store device status information such as uploader battery @@ -113,12 +183,161 @@ Use the [autoconfigure tool][autoconfigure] to sync an uploader to your config. * `SSL_KEY` - Path to your ssl key file, so that ssl(https) can be enabled directly in node.js * `SSL_CERT` - Path to your ssl cert file, so that ssl(https) can be enabled directly in node.js * `SSL_CA` - Path to your ssl ca file, so that ssl(https) can be enabled directly in node.js + * `HEARTBEAT` (`60`) - Number of seconds to wait in between database checks + + +### Predefined values for your browser settings (optional) + * `TIME_FORMAT` (`12`)- possible values `12` or `24` + * `NIGHT_MODE` (`off`) - possible values `on` or `off` + * `SHOW_RAWBG` (`never`) - possible values `always`, `never` or `noise` + * `CUSTOM_TITLE` (`Nightscout`) - Usually name of T1 + * `THEME` (`default`) - possible values `default` or `colors` + * `ALARM_TIMEAGO_WARN` (`on`) - possible values `on` or `off` + * `ALARM_TIMEAGO_WARN_MINS` (`15`) - minutes since the last reading to trigger a warning + * `ALARM_TIMEAGO_URGENT` (`on`) - possible values `on` or `off` + * `ALARM_TIMEAGO_URGENT_MINS` (`30`) - minutes since the last reading to trigger a urgent alarm + * `SHOW_PLUGINS` - enabled plugins that should have their visualizations shown, defaults to all enabled + * `LANGUAGE` (`en`) - language of Nighscout. If not available english is used + +### Plugins + + Plugins are used extend the way information is displayed, how notifications are sent, alarms are triggered, and more. + + The built-in/example plugins that are available by default are listed below. The plugins may still need to be enabled by adding to the `ENABLE` environment variable. + +#### Default Plugins + + These can be disabled by setting the `DISABLE` env var, for example `DISABLE="direction upbat"` + + * `delta` (BG Delta) - Calculates and displays the change between the last 2 BG values. + * `direction` (BG Direction) - Displays the trend direction. + * `upbat` (Uploader Battery) - Displays the most recent battery status from the uploader phone. + * `errorcodes` (CGM Error Codes) - Generates alarms for CGM codes `9` (hourglass) and `10` (???). + * Use [extended settings](#extended-settings) to adjust what errorcodes trigger notifications and alarms: + * `ERRORCODES_INFO` (`1 2 3 4 5 6 7 8`) - By default the needs calibration (blood drop) and other codes below 9 generate an info level notification, set to a space separate list of number or `off` to disable + * `ERRORCODES_WARN` (`off`) - By default there are no warning configured, set to a space separate list of numbers or `off` to disable + * `ERRORCODES_URGENT` (`9 10`) - By default the hourglass and ??? generate an urgent alarm, set to a space separate list of numbers or `off` to disable + * `ar2` ([Forcasting using AR2 algorithm](https://github.com/nightscout/nightscout.github.io/wiki/Forecasting)) - Generates alarms based on forecasted values. + * Enabled by default if no thresholds are set **OR** `ALARM_TYPES` includes `predict`. + * Use [extended settings](#extended-settings) to adjust AR2 behavior: + * `AR2_USE_RAW` (`false`) - to forecast using `rawbg` values when standard values don't trigger an alarm. + * `AR2_CONE_FACTOR` (`2`) - to adjust size of cone, use `0` for a single line. + * `simplealarms` (Simple BG Alarms) - Uses `BG_HIGH`, `BG_TARGET_TOP`, `BG_TARGET_BOTTOM`, `BG_LOW` thresholds to generate alarms. + * Enabled by default if 1 of these thresholds is set **OR** `ALARM_TYPES` includes `simple`. + +#### Built-in/Example Plugins: + + * `rawbg` (Raw BG) - Calculates BG using sensor and calibration records from and displays an alternate BG values and noise levels. + * `iob` (Insulin-on-Board) - Adds the IOB pill visualization in the client and calculates values that used by other plugins. Uses treatments with insulin doses and the `dia` and `sens` fields from the [treatment profile](#treatment-profile). + * `cob` (Carbs-on-Board) - Adds the COB pill visualization in the client and calculates values that used by other plugins. Uses treatments with carb doses and the `carbs_hr`, `carbratio`, and `sens` fields from the [treatment profile](#treatment-profile). + * `bwp` (Bolus Wizard Preview) - This plugin in intended for the purpose of automatically snoozing alarms when the CGM indicates high blood sugar but there is also insulin on board (IOB) and secondly, alerting to user that it might be beneficial to measure the blood sugar using a glucometer and dosing insulin as calculated by the pump or instructed by trained medicare professionals. ***The values provided by the plugin are provided as a reference based on CGM data and insulin sensitivity you have configured, and are not intended to be used as a reference for bolus calculation.*** The plugin calculates the bolus amount when above your target, generates alarms when you should consider checking and bolusing, and snoozes alarms when there is enough IOB to cover a high BG. Uses the results of the `iob` plugin and `sens`, `target_high`, and `target_low` fields from the [treatment profile](#treatment-profile). Defaults that can be adjusted with [extended setting](#extended-settings) + * `BWP_WARN` (`0.50`) - If `BWP` is > `BWP_WARN` a warning alarm will be triggered. + * `BWP_URGENT` (`1.00`) - If `BWP` is > `BWP_URGENT` an urgent alarm will be triggered. + * `BWP_SNOOZE_MINS` (`10`) - minutes to snooze when there is enough IOB to cover a high BG. + * `BWP_SNOOZE` - (`0.10`) If BG is higher then the `target_high` and `BWP` < `BWP_SNOOZE` alarms will be snoozed for `BWP_SNOOZE_MINS`. + * `cage` (Cannula Age) - Calculates the number of hours since the last `Site Change` treatment that was recorded. + * `CAGE_ENABLE_ALERTS` (`false`) - Set to `true` to enable notifications to remind you of upcoming cannula change. + * `CAGE_INFO` (`44`) - If time since last `Site Change` matches `CAGE_INFO`, user will be warned of upcoming cannula change + * `CAGE_WARN` (`48`) - If time since last `Site Change` matches `CAGE_WARN`, user will be alarmed to to change the cannula + * `CAGE_URGENT` (`72`) - If time since last `Site Change` matches `CAGE_URGENT`, user will be issued a persistent warning of overdue change. + * `treatmentnotify` (Treatment Notifications) - Generates notifications when a treatment has been entered and snoozes alarms minutes after a treatment. Default snooze is 10 minutes, and can be set using the `TREATMENTNOTIFY_SNOOZE_MINS` [extended setting](#extended-settings). + * `basal` (Basal Profile) - Adds the Basal pill visualization to display the basal rate for the current time. Also enables the `bwp` plugin to calculate correction temp basal suggestions. Uses the `basal` field from the [treatment profile](#treatment-profile). + * `bridge` (Share2Nightscout bridge) - Glucose reading directly from the Share service, uses these extended settings: + * `BRIDGE_USER_NAME` - Your user name for the Share service. + * `BRIDGE_PASSWORD` - Your password for the Share service. + * `BRIDGE_INTERVAL` (`150000` *2.5 minutes*) - The time to wait between each update. + * `BRIDGE_MAX_COUNT` (`1`) - The maximum number of records to fetch per update. + * `BRIDGE_FIRST_FETCH_COUNT` (`3`) - Changes max count during the very first update only. + * `BRIDGE_MAX_FAILURES` (`3`) - How many failures before giving up. + * `BRIDGE_MINUTES` (`1400`) - The time window to search for new data per update (default is one day in minutes). + * `mmconnect` (MiniMed Connect bridge) - Transfer real-time MiniMed Connect data from the Medtronic CareLink server into Nightscout ([read more](https://github.com/mddub/minimed-connect-to-nightscout)) + * `MMCONNECT_USER_NAME` - Your user name for CareLink Connect. + * `MMCONNECT_PASSWORD` - Your password for CareLink Connect. + * `MMCONNECT_INTERVAL` (`60000` *1 minute*) - Number of milliseconds to wait between requests to the CareLink server. + * `MMCONNECT_MAX_RETRY_DURATION` (`32`) - Maximum number of total seconds to spend retrying failed requests before giving up. + * `MMCONNECT_SGV_LIMIT` (`24`) - Maximum number of recent sensor glucose values to send to Nightscout on each request. + * `MMCONNECT_VERBOSE` - Set this to any truthy value to log CareLink request information to the console. + + Also see [Pushover](#pushover) and [IFTTT Maker](#ifttt-maker). + + +#### Extended Settings + Some plugins support additional configuration using extra environment variables. These are prefixed with the name of the plugin and a `_`. For example setting `MYPLUGIN_EXAMPLE_VALUE=1234` would make `extendedSettings.exampleValue` available to the `MYPLUGIN` plugin. + + Plugins only have access to their own extended settings, all the extended settings of client plugins will be sent to the browser. + +#### Pushover + In addition to the normal web based alarms, there is also support for [Pushover](https://pushover.net/) based alarms and notifications. + + To get started install the Pushover application on your iOS or Android device and create an account. + + Using that account login to [Pushover](https://pushover.net/), in the top left you’ll see your User Key, you’ll need this plus an application API Token/Key to complete this setup. + + You’ll need to [Create a Pushover Application](https://pushover.net/apps/build). You only need to set the Application name, you can ignore all the other settings, but setting an Icon is a nice touch. Maybe you'd like to use [this one](https://raw.githubusercontent.com/nightscout/cgm-remote-monitor/master/static/images/large.png)? + + Pushover is configured using the following Environment Variables: + + * `ENABLE` - `pushover` should be added to the list of plugin, for example: `ENABLE="pushover"`. + * `PUSHOVER_API_TOKEN` - Used to enable pushover notifications, this token is specific to the application you create from in [Pushover](https://pushover.net/), ***[additional pushover information](#pushover)*** below. + * `PUSHOVER_USER_KEY` - Your Pushover user key, can be found in the top left of the [Pushover](https://pushover.net/) site, this can also be a pushover delivery group key to send to a group rather than just a single user. This also supports a space delimited list of keys. To disable `INFO` level pushes set this to `off`. + * `PUSHOVER_ALARM_KEY` - An optional Pushover user/group key, will be used for system wide alarms (level > `WARN`). If not defined this will fallback to `PUSHOVER_USER_KEY`. A possible use for this is sending important messages and alarms to a CWD that you don't want to send all notification too. This also support a space delimited list of keys. To disable Alarm pushes set this to `off`. + * `PUSHOVER_ANNOUNCEMENT_KEY` - An optional Pushover user/group key, will be used for system wide user generated announcements. If not defined this will fallback to `PUSHOVER_USER_KEY` or `PUSHOVER_ALARM_KEY`. This also support a space delimited list of keys. To disable Announcement pushes set this to `off`. + * `BASE_URL` - Used for pushover callbacks, usually the URL of your Nightscout site, use https when possible. + * `API_SECRET` - Used for signing the pushover callback request for acknowledgments. + + If you never want to get info level notifications (treatments) use `PUSHOVER_USER_KEY="off"` + If you never want to get an alarm via pushover use `PUSHOVER_ALARM_KEY="off"` + If you never want to get an announcement via pushover use `PUSHOVER_ANNOUNCEMENT_KEY="off"` + + If only `PUSHOVER_USER_KEY` is set it will be used for all info notifications, alarms, and announcements + + For testing/development try [localtunnel](http://localtunnel.me/). + +#### IFTTT Maker + In addition to the normal web based alarms, and pushover, there is also integration for [IFTTT Maker](https://ifttt.com/maker). + + With Maker you are able to integrate with all the other [IFTTT Channels](https://ifttt.com/channels). For example you can send a tweet when there is an alarm, change the color of hue light, send an email, send and sms, and so much more. + + 1. Setup IFTTT account: [login](https://ifttt.com/login) or [create an account](https://ifttt.com/join) + 2. Find your secret key on the [maker page](https://ifttt.com/maker) + 3. Configure Nightscout by setting these environment variables: + * `ENABLE` - `maker` should be added to the list of plugin, for example: `ENABLE="maker"`. + * `MAKER_KEY` - Set this to your secret key that you located in step 2, for example: `MAKER_KEY="abcMyExampleabc123defjt1DeNSiftttmak-XQb69p"` This also support a space delimited list of keys. + * `MAKER_ANNOUNCEMENT_KEY` - An optional Maker key, will be used for system wide user generated announcements. If not defined this will fallback to `MAKER_KEY`. A possible use for this is sending important messages and alarms to a CWD that you don't want to send all notification too. This also support a space delimited list of keys. + 4. [Create a recipe](https://ifttt.com/myrecipes/personal/new) or see [more detailed instructions](lib/plugins/maker-setup.md#create-a-recipe) + + Plugins can create custom events, but all events sent to maker will be prefixed with `ns-`. The core events are: + * `ns-event` - This event is sent to the maker service for all alarms and notifications. This is good catch all event for general logging. + * `ns-allclear` - This event is sent to the maker service when an alarm has been ack'd or when the server starts up without triggering any alarms. For example, you could use this event to turn a light to green. + * `ns-info` - Plugins that generate notifications at the info level will cause this event to also be triggered. It will be sent in addition to `ns-event`. + * `ns-warning` - Alarms at the warning level with cause this event to also be triggered. It will be sent in addition to `ns-event`. + * `ns-urgent` - Alarms at the urgent level with cause this event to also be triggered. It will be sent in addition to `ns-event`. + * see the [full list of events](lib/plugins/maker-setup.md#events) + + +### Treatment Profile + Some of the [plugins](#plugins) make use of a treatment profile that can be edited using the Profile Editor, see the link in the Settings drawer on your site. + + Treatment Profile Fields: + + * `timezone` (Time Zone) - time zone local to the patient. *Should be set.* + * `units` (Profile Units) - blood glucose units used in the profile, either "mgdl" or "mmol" + * `dia` (Insulin duration) - value should be the duration of insulin action to use in calculating how much insulin is left active. Defaults to 3 hours. + * `carbs_hr` (Carbs per Hour) - The number of carbs that are processed per hour, for more information see [#DIYPS](http://diyps.org/2014/05/29/determining-your-carbohydrate-absorption-rate-diyps-lessons-learned/). + * `carbratio` (Carb Ratio) - grams per unit of insulin. + * `sens` (Insulin sensitivity) How much one unit of insulin will normally lower blood glucose. + * `basal` The basal rate set on the pump. + * `target_high` - Upper target for correction boluses. + * `target_low` - Lower target for correction boluses. + + Some example profiles are [here](example-profiles.md). ## Setting environment variables Easy to emulate on the commandline: ```bash - echo 'MONGO_CONNECTION="mongodb://sally:sallypass@ds099999.mongolab.com:99999/nightscout"' >> my.env + echo 'MONGO_CONNECTION=mongodb://sally:sallypass@ds099999.mongolab.com:99999/nightscout' >> my.env + echo 'MONGO_COLLECTION=entries' >> my.env ``` From now on you can run using diff --git a/Release.md b/Release.md deleted file mode 100644 index 8e9ba512d0d..00000000000 --- a/Release.md +++ /dev/null @@ -1,15 +0,0 @@ - -v0.4.1 / 2014-09-12 -================== - - * quick hack to prevent mbg records from crashing pebble - * add script to prep release branch - * tweak toolbar and button size/placement - * Merge pull request #128 from nightscout/wip/mbg - * Merge pull request #166 from nightscout/wip/id-rev - * Merge pull request #165 from nightscout/hotfix/pebble-sgv-string - * convert sgv to string - * add ability to easily id git rev-parse HEAD - * Merge branch 'release/0.4.0' into dev - * hack: only consider 'grey' sgv records - * Added searching for MBG data from mongo query. diff --git a/app.js b/app.js new file mode 100644 index 00000000000..fa807b81a5d --- /dev/null +++ b/app.js @@ -0,0 +1,55 @@ + +var express = require('express'); +var compression = require('compression'); +function create (env, ctx) { + /////////////////////////////////////////////////// + // api and json object variables + /////////////////////////////////////////////////// + var api = require('./lib/api/')(env, ctx); + + var app = express(); + var appInfo = env.name + ' ' + env.version; + app.set('title', appInfo); + app.enable('trust proxy'); // Allows req.secure test on heroku https connections. + + app.use(compression({filter: function shouldCompress(req, res) { + //TODO: return false here if we find a condition where we don't want to compress + // fallback to standard filter function + return compression.filter(req, res); + }})); + + //if (env.api_secret) { + // console.log("API_SECRET", env.api_secret); + //} + app.use('/api/v1', api); + + + // pebble data + app.get('/pebble', ctx.pebble); + + // expose swagger.yaml + app.get('/swagger.yaml', function (req, res) { + res.sendFile(__dirname + '/swagger.yaml'); + }); + + //app.get('/package.json', software); + + // define static server + //TODO: JC - changed cache to 1 hour from 30d ays to bypass cache hell until we have a real solution + var staticFiles = express.static(env.static_files, {maxAge: 60 * 60 * 1000}); + + // serve the static content + app.use(staticFiles); + + var bundle = require('./bundle')(); + app.use(bundle); + + // Handle errors with express's errorhandler, to display more readable error messages. + var errorhandler = require('errorhandler'); + //if (process.env.NODE_ENV === 'development') { + app.use(errorhandler()); + //} + return app; +} +module.exports = create; + diff --git a/app.json b/app.json index 77a1a308cd0..c5a6417eed5 100644 --- a/app.json +++ b/app.json @@ -8,17 +8,27 @@ "required": true }, "API_SECRET": { - "description": "REQUIRED: User generated password used for REST API and optional features (12 character minimum).", + "description": "REQUIRED: A secret passphrase that must be at least 12 characters long, required to enable POST and PUT; also required for the Care Portal", "value": "", "required": true }, + "DISPLAY_UNITS": { + "description": "Choices: mg/dl and mmol. Setting to mmol puts the entire server into mmol mode by default, no further settings needed.", + "value": "", + "required": false + }, "ENABLE": { - "description": "Space delimited list of optional features to enable, such as 'careportal'.", + "description": "Used to enable optional features, expects a space delimited list, such as: careportal rawbg iob, see https://github.com/nightscout/cgm-remote-monitor/blob/master/README.md for more info", + "value": "", + "required": false + }, + "DISABLE": { + "description": "Used to disable default features, expects a space delimited list, such as: direction upbat, see https://github.com/nightscout/cgm-remote-monitor/blob/master/README.md for more info", "value": "", "required": false }, "ALARM_TYPES": { - "description": "Nightscout alarm behavior control. Default null value implies 'predict'. For adjustable alarm thresholds (set below), set to 'simple'.", + "description": "Alarm behavior currently 2 alarm types are supported simple and predict, and can be used independently or combined. The simple alarm type only compares the current BG to BG_ thresholds above, the predict alarm type uses highly tuned formula that forecasts where the BG is going based on it's trend. predict DOES NOT currently use any of the BG_* ENV's", "value": "", "required": false }, @@ -51,9 +61,110 @@ "description": "Pushover user key, required for Pushover notifications. Leave blank if not using Pushover.", "value": "", "required": false + }, + "PUSHOVER_ANNOUNCEMENT_KEY": { + "description": "An optional Pushover user/group key, will be used for system wide user generated announcements. If not defined this will fallback to `PUSHOVER_USER_KEY`. A possible use for this is sending important messages and alarms to a CWD that you don't want to send all notification too. This also support a space delimited list of keys. Leave blank if not using Pushover", + "value": "", + "required": false + }, + "CUSTOM_TITLE": { + "description": "Customize the name of the website, usually the name of T1.", + "value": "", + "required": false + }, + "THEME": { + "description": "Possible values default or colors", + "value": "", + "required": false + }, + "SHOW_RAWBG": { + "description": "Possible values always, never or noise", + "value": "", + "required": false + }, + "BRIDGE_USER_NAME": { + "description": "Share bridge - Your user name for the Share service. ENSURE bridge is in ENABLE if you want to use the share bridge", + "value": "", + "required": false + }, + "BRIDGE_PASSWORD": { + "description": "Share bridge - Your password for the Share service. ENSURE bridge is in ENABLE if you want to use the share bridge", + "value": "", + "required": false + }, + "TIME_FORMAT": { + "description": "Browser default time mode valid settings are 12 or 24", + "value": "12", + "required": false + }, + "NIGHT_MODE": { + "description": "Browser defaults to night mode valid settings are on or off", + "value": "off", + "required": false + }, + "SHOW_RAWBG": { + "description": "Browser default raw display mode vaild settings are always never or noise", + "value": "never", + "required": false + }, + "THEME": { + "description": "Browser default theme setting vaild settings are default or colors", + "value": "default", + "required": false + }, + "ALARM_URGENT_HIGH": { + "description": "Browser default urgent high alarm enabled vaild settings are on or off", + "value": "on", + "required": false + }, + "ALARM_HIGH": { + "description": "Browser default high alarm enabled vaild settings are on or off", + "value": "on", + "required": false + }, + "ALARM_LOW": { + "description": "Browser default low alarm enabled vaild settings are on or off", + "value": "on", + "required": false + }, + "ALARM_URGENT_LOW": { + "description": "Browser default urgent low alarm enabled vaild settings are on or off", + "value": "on", + "required": false + }, + "ALARM_TIMEAGO_WARN": { + "description": "Browser default warn after time of last data exceeds ALARM_TIMEAGO_WARN_MINS alarm enabled vaild settings are on or off", + "value": "on", + "required": false + }, + "ALARM_TIMEAGO_WARN_MINS": { + "description": "Browser default minutes since the last reading to trigger a warning", + "value": "15", + "required": false + }, + "ALARM_TIMEAGO_URGENT": { + "description": "Browser default urgent warning after time of last data exceeds ALARM_TIMEAGO_URGENT_MINS alarm enabled vaild settings are on or off", + "value": "on", + "required": false + }, + "ALARM_TIMEAGO_URGENT_MINS": { + "description": "Browser default minutes since last reading to trigger an urgent alarm", + "value": "30", + "required": false + }, + "MAKER_KEY": { + "description": "Maker Key - Set this to your secret key Note for additional info see https://github.com/nightscout/cgm-remote-monitor/blob/dev/README.md#ifttt-maker , maker should be added to enable if you want to use maker, Leave blank if not using maker", + "value": "", + "required": false + }, + "MAKER_ANNOUNCEMENT_KEY": { + "description": "Maker Announcement Key - Set this to your secret key for announcements Note for additional info see https://github.com/nightscout/cgm-remote-monitor/blob/dev/README.md#ifttt-maker , maker should be added to enable if you want to use maker Leave blank if not using maker", + "value": "", + "required": false } }, "addons": [ - "mongolab:sandbox" + "mongolab:sandbox", + "papertrail" ] } diff --git a/bin/post-sgv.sh b/bin/post-sgv.sh new file mode 100755 index 00000000000..9b48e5dfcc9 --- /dev/null +++ b/bin/post-sgv.sh @@ -0,0 +1,9 @@ +#!/bin/sh +# "date": "1413782506964" + +curl -H "Content-Type: application/json" -H "api-secret: $API_SECRET" -XPOST 'http://localhost:1337/api/v1/entries/' -d '{ + "sgv": 100, + "type": "sgv", + "direction": "Flat", + "date": "1415950912800" +}' diff --git a/bower.json b/bower.json index 406470963b2..0511964f203 100644 --- a/bower.json +++ b/bower.json @@ -1,14 +1,13 @@ { "name": "nightscout", - "version": "0.6.2", + "version": "0.8.1", "dependencies": { - "angularjs": "1.3.0-beta.19", - "bootstrap": "~3.2.0", - "d3": "3.4.3", "jquery": "2.1.0", "jQuery-Storage-API": "~1.7.2", "tipsy-jmalonzo": "~1.0.1", - "jsSHA": "~1.5.0" + "jquery-ui": "~1.11.3", + "jquery-flot": "0.8.3", + "swagger-ui": "~2.1.2" }, "resolutions": { "jquery": "2.1.0" diff --git a/bundle/bundle.source.js b/bundle/bundle.source.js new file mode 100644 index 00000000000..d78ecb3c868 --- /dev/null +++ b/bundle/bundle.source.js @@ -0,0 +1,19 @@ +(function () { + + window._ = require('lodash'); + window.$ = window.jQuery = require('jquery'); + window.moment = require('moment-timezone'); + window.Nightscout = window.Nightscout || {}; + + window.Nightscout = { + client: require('../lib/client') + , units: require('../lib/units')() + , plugins: require('../lib/plugins/')().registerClientDefaults() + , report_plugins: require('../lib/report_plugins/')() + , admin_plugins: require('../lib/admin_plugins/')() + }; + + console.info('Nightscout bundle ready'); + +})(); + diff --git a/bundle/index.js b/bundle/index.js new file mode 100644 index 00000000000..4a8cd1563f2 --- /dev/null +++ b/bundle/index.js @@ -0,0 +1,17 @@ +'use strict'; + +var browserify_express = require('browserify-express'); + +function bundle() { + return browserify_express({ + entry: __dirname + '/bundle.source.js', + watch: __dirname + '/../lib/', + mount: '/public/js/bundle.js', + verbose: true, + //minify: true, + bundle_opts: { debug: true }, // enable inline sourcemap on js files + write_file: __dirname + '/bundle.out.js' + }); +} + +module.exports = bundle; \ No newline at end of file diff --git a/deploy.sh b/deploy.sh index 4bc630de4ab..29fbdc49d2b 100755 --- a/deploy.sh +++ b/deploy.sh @@ -99,6 +99,7 @@ selectNodeVersion () { # ---------- echo Handling node.js deployment. +echo "\"$SCM_COMMIT_ID\"" > $DEPLOYMENT_SOURCE/scm-commit-id.json # 1. KuduSync if [[ "$IN_PLACE_DEPLOYMENT" -ne "1" ]]; then diff --git a/env.js b/env.js index 0218d69632a..9d1388b6435 100644 --- a/env.js +++ b/env.js @@ -1,151 +1,169 @@ 'use strict'; -var env = { }; +var _ = require('lodash'); +var fs = require('fs'); var crypto = require('crypto'); var consts = require('./lib/constants'); -var fs = require('fs'); + +var env = { + settings: require('./lib/settings')() +}; + // Module to constrain all config and environment parsing to one spot. +// See the function config ( ) { - /* - * First inspect a bunch of environment variables: - * * PORT - serve http on this port - * * MONGO_CONNECTION, CUSTOMCONNSTR_mongo - mongodb://... uri - * * CUSTOMCONNSTR_mongo_collection - name of mongo collection with "sgv" documents - * * CUSTOMCONNSTR_mongo_settings_collection - name of mongo collection to store configurable settings - * * API_SECRET - if defined, this passphrase is fed to a sha1 hash digest, the hex output is used to create a single-use token for API authorization - * * NIGHTSCOUT_STATIC_FILES - the "base directory" to use for serving - * static files over http. Default value is the included `static` - * directory. + * See README.md for info about all the supported ENV VARs */ - var software = require('./package.json'); - var git = require('git-rev'); - - if (readENV('SCM_GIT_EMAIL') == 'windowsazure' && readENV('ScmType') == 'GitHub') { - git.cwd('/home/site/repository'); - } - if (readENV('SCM_COMMIT_ID')) { - env.head = readENV('SCM_COMMIT_ID'); - } else { - git.short(function record_git_head (head) { - console.log("GIT HEAD", head); - env.head = head; - }); - } - env.version = software.version; - env.name = software.name; - env.DISPLAY_UNITS = readENV('DISPLAY_UNITS', 'mg/dl'); env.PORT = readENV('PORT', 1337); - env.mongo = readENV('MONGO_CONNECTION') || readENV('MONGO') || readENV('MONGOLAB_URI'); - env.mongo_collection = readENV('MONGO_COLLECTION', 'entries'); - env.settings_collection = readENV('MONGO_SETTINGS_COLLECTION', 'settings'); - env.treatments_collection = readENV('MONGO_TREATMENTS_COLLECTION', 'treatments'); - env.devicestatus_collection = readENV('MONGO_DEVICESTATUS_COLLECTION', 'devicestatus'); + env.static_files = readENV('NIGHTSCOUT_STATIC_FILES', __dirname + '/static/'); - env.enable = readENV('ENABLE'); + setSSL(); + setAPISecret(); + setVersion(); + setMongo(); + updateSettings(); + + // require authorization for entering treatments + env.treatments_auth = readENV('TREATMENTS_AUTH',false); + + return env; +} + +function setSSL() { env.SSL_KEY = readENV('SSL_KEY'); env.SSL_CERT = readENV('SSL_CERT'); env.SSL_CA = readENV('SSL_CA'); env.ssl = false; if (env.SSL_KEY && env.SSL_CERT) { env.ssl = { - key: fs.readFileSync(env.SSL_KEY) - , cert: fs.readFileSync(env.SSL_CERT) + key: fs.readFileSync(env.SSL_KEY), cert: fs.readFileSync(env.SSL_CERT) }; if (env.SSL_CA) { env.ca = fs.readFileSync(env.SSL_CA); } } +} - var shasum = crypto.createHash('sha1'); - - ///////////////////////////////////////////////////////////////// - // A little ugly, but we don't want to read the secret into a var - ///////////////////////////////////////////////////////////////// +// A little ugly, but we don't want to read the secret into a var +function setAPISecret() { var useSecret = (readENV('API_SECRET') && readENV('API_SECRET').length > 0); + //TODO: should we clear API_SECRET from process env? env.api_secret = null; // if a passphrase was provided, get the hex digest to mint a single token if (useSecret) { if (readENV('API_SECRET').length < consts.MIN_PASSPHRASE_LENGTH) { - var msg = ["API_SECRET should be at least", consts.MIN_PASSPHRASE_LENGTH, "characters"]; - var err = new Error(msg.join(' ')); - // console.error(err); - throw err; - process.exit(1); + var msg = ['API_SECRET should be at least', consts.MIN_PASSPHRASE_LENGTH, 'characters']; + throw new Error(msg.join(' ')); } + var shasum = crypto.createHash('sha1'); shasum.update(readENV('API_SECRET')); env.api_secret = shasum.digest('hex'); } +} - env.thresholds = { - bg_high: readIntENV('BG_HIGH', 260) - , bg_target_top: readIntENV('BG_TARGET_TOP', 180) - , bg_target_bottom: readIntENV('BG_TARGET_BOTTOM', 80) - , bg_low: readIntENV('BG_LOW', 55) - }; - - //NOTE: using +/- 1 here to make the thresholds look visibly wrong in the UI - // if all thresholds were set to the same value you should see 4 lines stacked right on top of each other - if (env.thresholds.bg_target_bottom >= env.thresholds.bg_target_top) { - console.warn('BG_TARGET_BOTTOM(' + env.thresholds.bg_target_bottom + ') was >= BG_TARGET_TOP(' + env.thresholds.bg_target_top + ')'); - env.thresholds.bg_target_bottom = env.thresholds.bg_target_top - 1; - console.warn('BG_TARGET_BOTTOM is now ' + env.thresholds.bg_target_bottom); - } - - if (env.thresholds.bg_target_top <= env.thresholds.bg_target_bottom) { - console.warn('BG_TARGET_TOP(' + env.thresholds.bg_target_top + ') was <= BG_TARGET_BOTTOM(' + env.thresholds.bg_target_bottom + ')'); - env.thresholds.bg_target_top = env.thresholds.bg_target_bottom + 1; - console.warn('BG_TARGET_TOP is now ' + env.thresholds.bg_target_top); - } +function setVersion() { + var software = require('./package.json'); + var git = require('git-rev'); - if (env.thresholds.bg_low >= env.thresholds.bg_target_bottom) { - console.warn('BG_LOW(' + env.thresholds.bg_low + ') was >= BG_TARGET_BOTTOM(' + env.thresholds.bg_target_bottom + ')'); - env.thresholds.bg_low = env.thresholds.bg_target_bottom - 1; - console.warn('BG_LOW is now ' + env.thresholds.bg_low); + if (readENV('APPSETTING_ScmType') === readENV('ScmType') && readENV('ScmType') === 'GitHub') { + env.head = require('./scm-commit-id.json'); + console.log('SCM COMMIT ID', env.head); + } else { + git.short(function record_git_head(head) { + console.log('GIT HEAD', head); + env.head = head || readENV('SCM_COMMIT_ID') || readENV('COMMIT_HASH', ''); + }); } + env.version = software.version; + env.name = software.name; +} - if (env.thresholds.bg_high <= env.thresholds.bg_target_top) { - console.warn('BG_HIGH(' + env.thresholds.bg_high + ') was <= BG_TARGET_TOP(' + env.thresholds.bg_target_top + ')'); - env.thresholds.bg_high = env.thresholds.bg_target_top + 1; - console.warn('BG_HIGH is now ' + env.thresholds.bg_high); +function setMongo() { + env.mongo = readENV('MONGO_CONNECTION') || readENV('MONGO') || readENV('MONGOLAB_URI'); + env.mongo_collection = readENV('MONGO_COLLECTION', 'entries'); + env.MQTT_MONITOR = readENV('MQTT_MONITOR', null); + if (env.MQTT_MONITOR) { + var hostDbCollection = [env.mongo.split('mongodb://').pop().split('@').pop(), env.mongo_collection].join('/'); + var mongoHash = crypto.createHash('sha1'); + mongoHash.update(hostDbCollection); + //some MQTT servers only allow the client id to be 23 chars + env.mqtt_client_id = mongoHash.digest('base64').substring(0, 23); + console.info('Using Mongo host/db/collection to create the default MQTT client_id', hostDbCollection); + if (env.MQTT_MONITOR.indexOf('?clientId=') === -1) { + console.info('Set MQTT client_id to: ', env.mqtt_client_id); + } else { + console.info('MQTT configured to use a custom client id, it will override the default: ', env.mqtt_client_id); + } } - - //if any of the BG_* thresholds are set, default to "simple" otherwise default to "predict" to preserve current behavior - var thresholdsSet = readIntENV('BG_HIGH') || readIntENV('BG_TARGET_TOP') || readIntENV('BG_TARGET_BOTTOM') || readIntENV('BG_LOW'); - env.alarm_types = readENV('ALARM_TYPES') || (thresholdsSet ? "simple" : "predict"); - - // For pushing notifications to Pushover. - env.pushover_api_token = readENV('PUSHOVER_API_TOKEN'); - env.pushover_user_key = readENV('PUSHOVER_USER_KEY') || readENV('PUSHOVER_GROUP_KEY'); + env.treatments_collection = readENV('MONGO_TREATMENTS_COLLECTION', 'treatments'); + env.profile_collection = readENV('MONGO_PROFILE_COLLECTION', 'profile'); + env.devicestatus_collection = readENV('MONGO_DEVICESTATUS_COLLECTION', 'devicestatus'); // TODO: clean up a bit // Some people prefer to use a json configuration file instead. // This allows a provided json config to override environment variables var DB = require('./database_configuration.json'), DB_URL = DB.url ? DB.url : env.mongo, - DB_COLLECTION = DB.collection ? DB.collection : env.mongo_collection, - DB_SETTINGS_COLLECTION = DB.settings_collection ? DB.settings_collection : env.settings_collection; + DB_COLLECTION = DB.collection ? DB.collection : env.mongo_collection; env.mongo = DB_URL; env.mongo_collection = DB_COLLECTION; - env.settings_collection = DB_SETTINGS_COLLECTION; - env.static_files = readENV('NIGHTSCOUT_STATIC_FILES', __dirname + '/static/'); - - return env; } -function readIntENV(varName, defaultValue) { - return parseInt(readENV(varName)) || defaultValue; +function updateSettings() { + + var envNameOverrides = { + UNITS: 'DISPLAY_UNITS' + }; + + env.settings.eachSettingAsEnv(function settingFromEnv (name) { + var envName = envNameOverrides[name] || name; + return readENV(envName); + }); + + //should always find extended settings last + env.extendedSettings = findExtendedSettings(process.env); } function readENV(varName, defaultValue) { - //for some reason Azure uses this prefix, maybe there is a good reason - var value = process.env['CUSTOMCONNSTR_' + varName] - || process.env['CUSTOMCONNSTR_' + varName.toLowerCase()] - || process.env[varName] - || process.env[varName.toLowerCase()]; + //for some reason Azure uses this prefix, maybe there is a good reason + var value = process.env['CUSTOMCONNSTR_' + varName] + || process.env['CUSTOMCONNSTR_' + varName.toLowerCase()] + || process.env[varName] + || process.env[varName.toLowerCase()]; - return value || defaultValue; + if (typeof value === 'string' && value.toLowerCase() === 'on') { value = true; } + if (typeof value === 'string' && value.toLowerCase() === 'off') { value = false; } + + return value != null ? value : defaultValue; +} + +function findExtendedSettings (envs) { + var extended = {}; + + function normalizeEnv (key) { + return key.toUpperCase().replace('CUSTOMCONNSTR_', ''); + } + + _.each(env.settings.enable, function eachEnable(enable) { + if (_.trim(enable)) { + _.forIn(envs, function eachEnvPair (value, key) { + var env = normalizeEnv(key); + if (_.startsWith(env, enable.toUpperCase() + '_')) { + var split = env.indexOf('_'); + if (split > -1 && split <= env.length) { + var exts = extended[enable] || {}; + extended[enable] = exts; + var ext = _.camelCase(env.substring(split + 1).toLowerCase()); + if (!isNaN(value)) { value = Number(value); } + exts[ext] = value; + } + } + }); + } + }); + return extended; } module.exports = config; diff --git a/example-profiles.md b/example-profiles.md new file mode 100644 index 00000000000..717bc1df9dc --- /dev/null +++ b/example-profiles.md @@ -0,0 +1,77 @@ + + +**Table of Contents** + +- [Example Profiles](#example-profiles) + - [Simple profile](#simple-profile) + - [Profile can also use time periods for any field, for example:](#profile-can-also-use-time-periods-for-any-field-for-example) + + + +#Example Profiles + +These are only examples, make sure you update all fields to fit your needs + +##Simple profile + ```json + { + "dia": 3, + "carbs_hr": 20, + "carbratio": 30, + "sens": 100, + "basal": 0.125, + "target_low": 100, + "target_high": 120 + } + ``` + +##Profile can also use time periods for any field, for example: + + ```json + { + "carbratio": [ + { + "time": "00:00", + "value": 30 + }, + { + "time": "06:00", + "value": 25 + }, + { + "time": "14:00", + "value": 28 + } + ], + "basal": [ + { + "time": "00:00", + "value": 0.175 + }, + { + "time": "02:30", + "value": 0.125 + }, + { + "time": "05:00", + "value": 0.075 + }, + { + "time": "08:00", + "value": 0.100 + }, + { + "time": "14:00", + "value": 0.125 + }, + { + "time": "20:00", + "value": 0.175 + }, + { + "time": "22:00", + "value": 0.200 + } + ] + } + ``` diff --git a/lib/admin_plugins/cleanstatusdb.js b/lib/admin_plugins/cleanstatusdb.js new file mode 100644 index 00000000000..86e61e987f8 --- /dev/null +++ b/lib/admin_plugins/cleanstatusdb.js @@ -0,0 +1,70 @@ +'use strict'; + +var cleanstatusdb = { + name: 'cleanstatusdb' + , label: 'Clean Mongo status database' + , pluginType: 'admin' +}; + +function init() { + return cleanstatusdb; +} + +module.exports = init; + +cleanstatusdb.actions = [ + { + name: 'Delete all documents from devicestatus collection' + , description: 'This task removes all documents from devicestatus collection. Useful when uploader battery status is not properly updated.' + , buttonLabel: 'Delete all documents' + , confirmText: 'Delete all documents from devicestatus collection?' + } + ]; + +cleanstatusdb.actions[0].init = function init(client, callback) { + var translate = client.translate; + var $status = $('#admin_' + cleanstatusdb.name + '_0_status'); + + $status.hide().text(translate('Loading database ...')).fadeIn('slow'); + $.ajax('/api/v1/devicestatus.json?count=500', { + success: function (records) { + var recs = (records.length === 500 ? '500+' : records.length); + $status.hide().text(translate('Database contains %1 records',{ params: [recs] })).fadeIn('slow'); + }, + error: function () { + $status.hide().text(translate('Error loading database')).fadeIn('slow'); + } + }).done(function () { if (callback) { callback(); } }); +}; + +cleanstatusdb.actions[0].code = function deleteRecords(client, callback) { + var translate = client.translate; + var $status = $('#admin_' + cleanstatusdb.name + '_0_status'); + + if (!client.hashauth.isAuthenticated()) { + alert(translate('Your device is not authenticated yet')); + if (callback) { + callback(); + } + return; + }; + + $status.hide().text(translate('Deleting records ...')).fadeIn('slow'); + $.ajax({ + method: 'DELETE' + , url: '/api/v1/devicestatus/*' + , headers: { + 'api-secret': client.hashauth.hash() + } + }).done(function success () { + $status.hide().text(translate('All records removed ...')).fadeIn('slow'); + if (callback) { + callback(); + } + }).fail(function fail() { + $status.hide().text(translate('Error')).fadeIn('slow'); + if (callback) { + callback(); + } + }); +}; diff --git a/lib/admin_plugins/futureitems.js b/lib/admin_plugins/futureitems.js new file mode 100644 index 00000000000..ab888aff0a6 --- /dev/null +++ b/lib/admin_plugins/futureitems.js @@ -0,0 +1,172 @@ +'use strict'; + +var futureitems = { + name: 'futureitems' + , label: 'Remove future items from mongo database' + , pluginType: 'admin' +}; + +function init() { + return futureitems; +} + +module.exports = init; + +futureitems.actions = [ + { + name: 'Find and remove treatments in the future' + , description: 'This task find and remove treatments in the future.' + , buttonLabel: 'Remove treatments in the future' + } + , { + name: 'Find and remove entries in the future' + , description: 'This task find and remove CGM data in the future created by uploader with wrong date/time.' + , buttonLabel: 'Remove entries in the future' + } + ]; + +futureitems.actions[0].init = function init(client, callback) { + var translate = client.translate; + var $status = $('#admin_' + futureitems.name + '_0_status'); + + function valueOrEmpty (value) { + return value ? value : ''; + } + + function showOneTreatment (tr, table) { + table.append($('').css('background-color','#0f0f0f') + .append($('').attr('width','20%').append(new Date(tr.created_at).toLocaleString().replace(/([\d]+:[\d]{2})(:[\d]{2})(.*)/, '$1$3'))) + .append($('').attr('width','20%').append(tr.eventType ? translate(client.careportal.resolveEventName(tr.eventType)) : '')) + .append($('').attr('width','10%').attr('align','center').append(tr.glucose ? tr.glucose + ' ('+translate(tr.glucoseType)+')' : '')) + .append($('').attr('width','10%').attr('align','center').append(valueOrEmpty(tr.insulin))) + .append($('').attr('width','10%').attr('align','center').append(valueOrEmpty(tr.carbs))) + .append($('').attr('width','10%').append(valueOrEmpty(tr.enteredBy))) + .append($('').attr('width','20%').append(valueOrEmpty(tr.notes))) + ); + } + + function showTreatments(treatments, table) { + table.append($('').css('background','#040404') + .append($('').css('width','80px').attr('align','left').append(translate('Time'))) + .append($('').css('width','150px').attr('align','left').append(translate('Event Type'))) + .append($('').css('width','150px').attr('align','left').append(translate('Blood Glucose'))) + .append($('').css('width','50px').attr('align','left').append(translate('Insulin'))) + .append($('').css('width','50px').attr('align','left').append(translate('Carbs'))) + .append($('').css('width','150px').attr('align','left').append(translate('Entered By'))) + .append($('').css('width','300px').attr('align','left').append(translate('Notes'))) + ); + for (var t=0; t').css('margin-top','10px'); + $('#admin_' + futureitems.name + '_0_html').append(table); + showTreatments(records, table); + futureitems.actions[0].confirmText = translate('Remove %1 selected records?', { params: [records.length] }); + }, + error: function () { + $status.hide().text(translate('Error loading database')).fadeIn('slow'); + futureitems.treatmentrecords = []; + } + }).done(function () { if (callback) { callback(); } }); +}; + +futureitems.actions[0].code = function deleteRecords(client, callback) { + var translate = client.translate; + var $status = $('#admin_' + futureitems.name + '_0_status'); + + if (!client.hashauth.isAuthenticated()) { + alert(translate('Your device is not authenticated yet')); + if (callback) { + callback(); + } + return; + }; + + function deleteRecordById (_id) { + $.ajax({ + method: 'DELETE' + , url: '/api/v1/treatments/' + _id + , headers: { + 'api-secret': client.hashauth.hash() + } + }).done(function success () { + $status.text(translate('Record %1 removed ...', { params: [_id] })); + }).fail(function fail() { + $status.text(translate('Error removing record %1', { params: [_id] })); + }); + } + + $status.hide().text(translate('Deleting records ...')).fadeIn('slow'); + for (var i = 0; i < futureitems.treatmentrecords.length; i++) { + deleteRecordById(futureitems.treatmentrecords[i]._id); + } + $('#admin_' + futureitems.name + '_0_html').html(''); + + if (callback) { + callback(); + } +}; + +futureitems.actions[1].init = function init(client, callback) { + var translate = client.translate; + var $status = $('#admin_' + futureitems.name + '_1_status'); + + $status.hide().text(translate('Loading database ...')).fadeIn('slow'); + var now = new Date().getTime(); + $.ajax('/api/v1/entries.json?&find[date][$gte]=' + now, { + success: function (records) { + futureitems.entriesrecords = records; + $status.hide().text(translate('Database contains %1 future records',{ params: [records.length] })).fadeIn('slow'); + futureitems.actions[1].confirmText = translate('Remove %1 selected records?', { params: [records.length] }); + }, + error: function () { + $status.hide().text(translate('Error loading database')).fadeIn('slow'); + futureitems.entriesrecords = []; + } + }).done(function () { if (callback) { callback(); } }); +}; + +futureitems.actions[1].code = function deleteRecords(client, callback) { + var translate = client.translate; + var $status = $('#admin_' + futureitems.name + '_1_status'); + + if (!client.hashauth.isAuthenticated()) { + alert(translate('Your device is not authenticated yet')); + if (callback) { + callback(); + } + return; + }; + + function deteleteRecordById (_id) { + $.ajax({ + method: 'DELETE' + , url: '/api/v1/entries/' + _id + , headers: { + 'api-secret': client.hashauth.hash() + } + }).done(function success () { + $status.text(translate('Record %1 removed ...', { params: [_id] })); + }).fail(function fail() { + $status.text(translate('Error removing record %1', { params: [_id] })); + }); + } + + + $status.hide().text(translate('Deleting records ...')).fadeIn('slow'); + for (var i = 0; i < futureitems.entriesrecords.length; i++) { + deteleteRecordById(futureitems.entriesrecords[i]._id); + } + + if (callback) { + callback(); + } +}; diff --git a/lib/admin_plugins/index.js b/lib/admin_plugins/index.js new file mode 100644 index 00000000000..f7a5c74133d --- /dev/null +++ b/lib/admin_plugins/index.js @@ -0,0 +1,79 @@ +'use strict'; + +var _ = require('lodash'); + +function init() { + var allPlugins = [ + require('./cleanstatusdb')() + , require('./futureitems')() + ]; + + function plugins(name) { + if (name) { + return _.find(allPlugins, {name: name}); + } else { + return plugins; + } + } + + plugins.eachPlugin = function eachPlugin(f) { + _.each(allPlugins, f); + }; + + plugins.createHTML = function createHTML(client) { + var translate = client.translate; + plugins.eachPlugin(function addHtml(p) { + var fs = $('
'); + $('#admin_placeholder').append(fs); + fs.append($('').append(translate(p.label))); + for (var i = 0; i < p.actions.length; i++) { + if (i !== 0) { + fs.append('
'); + } + var a = p.actions[i]; + // add main plugin html + fs.append($('').css('text-decoration','underline').append(translate(a.name))); + fs.append('
'); + fs.append($('').append(translate(a.description))); + fs.append($('
').attr('id','admin_' + p.name + '_' + i + '_html')); + fs.append($(' + + + + + + + + + + + + diff --git a/static/profile/js/profileeditor.js b/static/profile/js/profileeditor.js new file mode 100644 index 00000000000..f1415d769eb --- /dev/null +++ b/static/profile/js/profileeditor.js @@ -0,0 +1,517 @@ +(function () { + 'use strict'; + //for the tests window isn't the global object + var $ = window.$; + var _ = window._; + var moment = window.moment; + var Nightscout = window.Nightscout; + var client = Nightscout.client; + + var c_profile = null; + + //some commonly used selectors + var peStatus = $('.pe_status'); + var timezoneInput = $('#pe_timezone'); + var timeInput = $('#pe_time'); + var dateInput = $('#pe_date'); + var submitButton = $('#pe_submit'); + + if (serverSettings === undefined) { + console.error('server settings were not loaded, will not call init'); + } else { + client.init(serverSettings, Nightscout.plugins); + } + + var translate = client.translate; + + var defaultprofile = { + //General values + 'dia':3, + + // Simple style values, 'from' are in minutes from midnight + 'carbratio': [ + { + 'time': '00:00', + 'value': 30 + }], + 'carbs_hr': 20, + 'delay': 20, + 'sens': [ + { + 'time': '00:00', + 'value': 100 + }], + 'startDate': new Date(), + 'timezone': 'UTC', + + //perGIvalues style values + 'perGIvalues': false, + 'carbs_hr_high': 30, + 'carbs_hr_medium': 30, + 'carbs_hr_low': 30, + 'delay_high': 15, + 'delay_medium': 20, + 'delay_low': 20, + + 'basal':[ + { + 'time': '00:00', + 'value': 0.1 + }], + 'target_low':[ + { + 'time': '00:00', + 'value': 0 + }], + 'target_high':[ + { + 'time': '00:00', + 'value': 0 + }] + }; + defaultprofile.startDate.setSeconds(0); + defaultprofile.startDate.setMilliseconds(0); + + var icon_add = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABa0lEQVQ4T6WTzysEYRjHP+/Mrv2hHOTuqJRNOfgPSORHokg4OClHcnVzVygHF6WIcuHMnyCHVRyI3ZYxa23vzOzs7LzamaI0e5i89fTWt/f5vPV5n1cQsXLbHepvfLv5JaLORoZNwMbyFo5vYfsWB0c7xAasLa5T/vCg45Oj48P4gJWFVYxCA63L5PzkND5gfm4Jo+Chd5W5OrtsDYgS1pQ1OTuNUfTQO8tcX9xE+QugYnS/X81MzGP7MpTWkEFVZY1KxcVPV3h27zAtA+oCagIcDfWUCgEje31qfHwK06gHjaF5iXQcHCV5lHmqqgQCNEAI0IsavCVDwNBurxoeGwmaAkDDwvYsqtIh//6AJUoklP97s62BbJYeAqIcpJNZsoM+r2aVbKKOekiBL8An3BuAEiGg1SSKAYnttpFxPdR9Jv4zipxFTUuQKqsfYbFGWfTYuO06yRfxIyweoLuG+iMsFuBfvzFy7FqE33vs2BFqlfN5AAAAAElFTkSuQmCC'; + var icon_remove = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACrElEQVQ4T42Ty2sTQRzHv5tmk2yyjRNtpfZhL8V6s2KoUNC2XqwgaCsVQcGiFqpHi0c9iRdR/ANE9KR40FIQX4cueKoPaKFoLdSYNtE0abKT1+5s9iW7aUMiHtzTzO7v85md+c6PA4DrHbsPCKIgOWO1pA7dT6YXnXH949SE/F63pqwZtRrO+SCKgjQ5NUV+azpmHj2krMwaJC4c8Erj+/eRyloMMwWFKgbn1nC3ervlK1evkXBLGBZT8SOewotnTylTNLdgeg/pDgZDC2cPHSR8bB22DVC9hFe0SG/H0xFXcHlykjRHRDBWgJcZSCY38Xx2lhqMnRYE34Px/sN9vlQWeoHBAx2yXsRruVAVuFsIBaSJ8+eJGPaBqQV4NROJjTzez89jLBoFn6FgybQL54wS3uTyVDFQ3cL2IYpBv3RhdJSIIQ80tQyv7gEqJvS8AmUlBs7UXPhtjtZgh3UFNYngk86NHCfNAg9dMwHVBPu+CpsVkTXKeJeVG+AGgTOZ3tt6MSKKjy+NjEBjFrR4ElZmA4pdxstMFsyyJu6tZZ7Ux9vwB6EAL50ZGiRECEPPUOixVTRxHlicgSVWxEdZpuZWfNuS2hk48NjwMIkIYZglBnV5Cbqtws/5IaAJmsfCglrEl2y2QeKmEBJ80tixKmxrFpSVr0gV0viQoxho2YUuPohmeFD22PiklLC4ma5JuBvdrfLJI0dJd0s7bM0ES8aR/BXDXGaTskqlL+D3Lwy0tZEePoAd4EA5YF4tYymdonfjmQh3s6dTPjU4SHYGwjAKecSXFyGlM1TdytntE56T+ts7SC/vhw3gm6njc2Kd3vm5Ub1IwQAvnYhGiZpYw1wiWYPrIw7wnBTt7CLOOwdmut14kQQvqt24tfK/utGR6LaF+iRqMf4N/O/8D28HiiCRYqzAAAAAAElFTkSuQmCC'; + + var mongoprofiles = []; + + // Fetch data from mongo + peStatus.hide().text('Loading profile records ...').fadeIn('slow'); + $.ajax('/api/v1/profile.json', { + success: function (records) { + c_profile = {}; + mongoprofiles = records; + // create new profile to be edited from last record + if (records[0] && records[0].dia) { + // Use only values(keys) defined in defaultprofile, drop the rest. Preparation for future changes. + c_profile = _.cloneDeep(defaultprofile); + c_profile.created_at = records[0].created_at; + for (var key in records[0]) { + if (records[0].hasOwnProperty(key)) { + if (typeof c_profile[key] !== 'undefined') { + c_profile[key] = records[0][key]; + } + // copy _id of record too + if (key === '_id') { + c_profile[key] = records[0][key]; + } + } + } + convertToRanges(c_profile); + + peStatus.hide().text('Values loaded.').fadeIn('slow'); + mongoprofiles.unshift(c_profile); + } else { + c_profile = _.cloneDeep(defaultprofile); + mongoprofiles.unshift(c_profile); + peStatus.hide().text('Default values used.').fadeIn('slow'); + } + }, + error: function () { + c_profile = _.cloneDeep(defaultprofile); + mongoprofiles.unshift(c_profile); + peStatus.hide().text('Error. Default values used.').fadeIn('slow'); + } + }).done(initeditor); + + // convert simple values to ranges if needed + function convertToRanges(profile) { + if (typeof profile.carbratio !== 'object') { profile.carbratio = [{ 'time': '00:00', 'value': profile.carbratio }]; } + if (typeof profile.sens !== 'object') { profile.sens = [{ 'time': '00:00', 'value': profile.sens }]; } + if (typeof profile.target_low !== 'object') { profile.target_low = [{ 'time': '00:00', 'value': profile.target_low }]; } + if (typeof profile.target_high !== 'object') { profile.target_high = [{ 'time': '00:00', 'value': profile.target_high }]; } + if (typeof profile.basal !== 'object') { profile.basal = [{ 'time': '00:00', 'value': profile.basal }]; } + if (profile.target_high.length !== profile.target_low.length) { + alert('Time ranges of target_low and target_high don\'t match. Values are restored to defaults.'); + profile.target_low = _.cloneDeep(defaultprofile.target_low); + profile.target_high = _.cloneDeep(defaultprofile.target_high); + } + } + + function initeditor() { + // Load timezones + timezoneInput.empty(); + moment.tz.names().forEach(function addTz(tz) { + timezoneInput.append(''); + }); + + $('#pe_form').find('button').click(profileSubmit); + + // Add handler for style switching + $('#pe_perGIvalues').change(switchStyle); + + // display status + $('#pe_units').text(client.settings.units); + $('#pe_timeformat').text(client.settings.timeFormat+'h'); + $('#pe_title').text(client.settings.customTitle); + + var lastvalidfrom = new Date(mongoprofiles[1] && mongoprofiles[1].startDate ? mongoprofiles[1].startDate : null); + + //timepicker + dateInput.on('change', dateChanged); + timeInput.on('change', dateChanged); + + + // Set values from profile to html + fillTimeRanges(); + // hide unused style of ratios + switchStyle(); + // show proper submit button + dateChanged(); + + // date of last record + if (lastvalidfrom) { + $('#pe_lastrecvalidfrom').html('Last record date: '+lastvalidfrom.toLocaleString()+' (Date must be newer to create new record or the same to update current record)'); + } else { + $('#pe_lastrecvalidfrom').html(''); + } + console.log('Done initeditor()'); + } + + // Handling valid from date change + function dateChanged(event) { + var newdate = new Date(client.utils.mergeInputTime(timeInput.val(), dateInput.val())); + if (mongoprofiles.length<2 || !mongoprofiles[1].startDate || mongoprofiles.length>=2 && new Date(mongoprofiles[1].startDate).getTime() === newdate.getTime()) { + submitButton.text('Update record').css('display',''); + timeInput.css({'background-color':'white'}); + dateInput.css({'background-color':'white'}); + submitButton.css({'background-color':'buttonface'}); + } else if (mongoprofiles.length<2 || new Date(mongoprofiles[1].startDate).getTime() < newdate.getTime()) { + submitButton.text('Create new record').css('display',''); + timeInput.css({'background-color':'green'}); + dateInput.css({'background-color':'green'}); + submitButton.css({'background-color':'green'}); + } else { + submitButton.css('display','none'); + timeInput.css({'background-color':'red'}); + dateInput.css({'background-color':'red'}); + submitButton.css({'background-color':'red'}); + } + if (event) { + event.preventDefault(); + } + } + + // Handling html events and setting/getting values + function switchStyle(event) { + if (!$('#pe_perGIvalues').is(':checked')) { + $('#pe_simple').show('slow'); + $('#pe_advanced').hide('slow'); + } else { + $('#pe_simple').hide('slow'); + $('#pe_advanced').show('slow'); + } + if (event) { + event.preventDefault(); + } + } + + function fillTimeRanges(event) { + if (event) { + GUIToObject(); + } + + function shouldAddTime(i, time, array) { + if (i === 0 && time === 0) { + return true; + } else if (i === 0) { + return false; + } else { + var minutesFromMidnight = toMinutesFromMidnight(c_profile[array][i - 1].time); + return !isNaN(minutesFromMidnight) && minutesFromMidnight < time * 30; + } + } + + function addSingleLine(e,i) { + var tr = $(''); + var select = $('').attr('id',e.prefix+'_val_'+i).attr('value',c_profile[e.array][i].value))); + var icons_td = $('').append($('').attr('class','addsingle').attr('style','cursor:pointer').attr('title','Add new interval before').attr('src',icon_add).attr('array',e.array).attr('pos',i)); + if (c_profile[e.array].length>1) { + icons_td.append($('').attr('class','delsingle').attr('style','cursor:pointer').attr('title','Delete interval').attr('src',icon_remove).attr('array',e.array).attr('pos',i)); + } + tr.append(icons_td); + + if (lowest>toMinutesFromMidnight(c_profile[e.array][i].time)) { + c_profile[e.array][i].time = toTimeString(lowest); + } + return tr[0].outerHTML; + } + + // Fill dropdown boxes + _.each([{prefix:'pe_basal', array:'basal', label:'Rate: '}, + {prefix:'pe_ic', array:'carbratio', label:'IC: '}, + {prefix:'pe_isf', array:'sens', label:'ISF: '} + ], function (e) { + var html = ''; + for (var i=0; i'; + html += '
'; + $('#'+e.prefix+'_placeholder').html(html); + }); + + $('.addsingle').click(function addsingle_click() { + var array = $(this).attr('array'); + var pos = $(this).attr('pos'); + GUIToObject(); + c_profile[array].splice(pos,0,{time:'00:00',value:0}); + return fillTimeRanges(); + }); + + $('.delsingle').click(function delsingle_click() { + var array = $(this).attr('array'); + var pos = $(this).attr('pos'); + GUIToObject(); + c_profile[array].splice(pos,1); + c_profile[array][0].time = '00:00'; + return fillTimeRanges(); + }); + + function addBGLine(i) { + var tr = $(''); + var select = $('').attr('id','pe_targetbg_low_'+i).attr('value',c_profile.target_low[i].value))); + tr.append($('').append('High : ').append($('').attr('id','pe_targetbg_high_'+i).attr('value',c_profile.target_high[i].value))); + var icons_td = $('').append($('').attr('class','addtargetbg').attr('style','cursor:pointer').attr('title','Add new interval before').attr('src',icon_add).attr('pos',i)); + if (c_profile.target_low.length>1) { + icons_td.append($('').attr('class','deltargetbg').attr('style','cursor:pointer').attr('title','Delete interval').attr('src',icon_remove).attr('pos',i)); + } + tr.append(icons_td); + + // Fix time to correct value after add or change + if (lowesttime>toMinutesFromMidnight(c_profile.target_low[i].time)) { + c_profile.target_low[i].time = toTimeString(lowesttime); + } + return tr[0].outerHTML; + } + + + // target BG + var html = ''; + for (var i=0; i'; + html += '
'; + $('#pe_targetbg_placeholder').html(html); + + $('.addtargetbg').click(function addtargetbg_click() { + var pos = $(this).attr('pos'); + GUIToObject(); + c_profile.target_low.splice(pos,0,{time:'00:00',value:0}); + c_profile.target_high.splice(pos,0,{time:'00:00',value:0}); + return fillTimeRanges(); + }); + + $('.deltargetbg').click(function deltargetbg_click() { + var pos = $(this).attr('pos'); + GUIToObject(); + c_profile.target_low.splice(pos,1); + c_profile.target_high.splice(pos,1); + c_profile.target_low[0].time = '00:00'; + c_profile.target_high[0].time = '00:00'; + return fillTimeRanges(); + }); + + $('.pe_selectabletime').change(fillTimeRanges); + + objectToGUI(); + if (event) { + event.preventDefault(); + } + return false; + } + + // fill GUI with values from c_profile object + function objectToGUI() { + + $('#pe_dia').val(c_profile.dia); + timeInput.val(moment(c_profile.startDate).format('HH:mm')); + dateInput.val(moment(c_profile.startDate).format('YYYY-MM-DD')); + $('#pe_hr').val(c_profile.carbs_hr); + $('#pe_perGIvalues').prop('checked', c_profile.perGIvalues); + $('#pe_hr_high').val(c_profile.carbs_hr_high); + $('#pe_hr_medium').val(c_profile.carbs_hr_medium); + $('#pe_hr_low').val(c_profile.carbs_hr_low); + $('#pe_delay_high').val(c_profile.delay_high); + $('#pe_delay_medium').val(c_profile.delay_medium); + $('#pe_delay_low').val(c_profile.delay_low); + timezoneInput.val(c_profile.timezone); + + var index; + [ { prefix:'pe_basal', array:'basal' }, + { prefix:'pe_ic', array:'carbratio' }, + { prefix:'pe_isf', array:'sens' } + ].forEach(function (e) { + for (index=0; index new Date()) { + alert('Date must be set in the past'); + peStatus.hide().html('Wrong date').fadeIn('slow'); + return false; + } + + if (!client.hashauth.isAuthenticated()) { + alert(translate('Your device is not authenticated yet')); + return false; + } + + c_profile.units = client.settings.units; + + var adjustedProfile = _.cloneDeep(c_profile); + + if (!adjustedProfile.perGIvalues) { + delete adjustedProfile.perGIvalues; + delete adjustedProfile.carbs_hr_high; + delete adjustedProfile.carbs_hr_medium; + delete adjustedProfile.carbs_hr_low; + delete adjustedProfile.delay_high; + delete adjustedProfile.delay_medium; + delete adjustedProfile.delay_low; + } + + adjustedProfile.startDate = adjustedProfile.startDate.toISOString( ); + + console.info('saving profile'); + if (submitButton.text().indexOf('Create new record')>-1) { + if (mongoprofiles.length > 1 && (new Date(c_profile.startDate) <= new Date(mongoprofiles[1].validfrom))) { + alert('Date must be greater than last record '+new Date(mongoprofiles[1].startDate)); + peStatus.hide().html('Wrong date').fadeIn('slow'); + return false; + } + + // remove _id when creating new record + delete adjustedProfile._id; + + $.ajax({ + method: 'POST' + , url: '/api/v1/profile/' + , data: adjustedProfile + , headers: { + 'api-secret': client.hashauth.hash() + } + }).done(function postSuccess (data, status) { + console.info('profile created', data); + peStatus.hide().text(status).fadeIn('slow'); + + //not using the adjustedProfile here (doesn't have the defaults other code needs) + var newprofile = _.cloneDeep(c_profile); + mongoprofiles.unshift(newprofile); + initeditor(); + }).fail(function(xhr, status, errorThrown) { + console.error('Profile not saved', status, errorThrown); + peStatus.hide().text(status).fadeIn('slow'); + }); + } else { + $.ajax({ + method: 'PUT' + , url: '/api/v1/profile/' + , data: adjustedProfile + , headers: { + 'api-secret': client.hashauth.hash() + } + }).done(function putSuccess (data, status) { + console.info('profile updated', data); + peStatus.hide().text(status).fadeIn('slow'); + }).fail(function(xhr, status, errorThrown) { + console.error('Profile not saved', status, errorThrown); + peStatus.hide().text(status).fadeIn('slow'); + }); + } + + if (event) { + event.preventDefault(); + } + return false; + } + +})(); diff --git a/static/report/index.html b/static/report/index.html new file mode 100644 index 00000000000..4b6c5e27560 --- /dev/null +++ b/static/report/index.html @@ -0,0 +1,132 @@ + + + + Nightscout reporting + + + + + + + + + + + + + + + + + + + + + + + + + +

Nightscout reporting

+
    +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ From: + To: + Today + Last 2 days + Last 3 days + Last week + Last 2 weeks + Last month + Last 3 months +
+ Notes contain: +
+ Event Type: +
+ Mo + Tu + We + Th + Fr + Sa + Su +
+ Target bg range bottom: + + top: + +
+ Order: + + +   + + +
+ +
+
+
+ +
+ Authentication status: + + + + + + + + + + + + + + + diff --git a/static/report/js/flotcandle.js b/static/report/js/flotcandle.js new file mode 100644 index 00000000000..3ac641d1cf5 --- /dev/null +++ b/static/report/js/flotcandle.js @@ -0,0 +1,87 @@ +(function ($) { + var options = { + series: { candle: null } // or number/string + }; + var offset, x, y; + + function init(plot) { + plot.hooks.processOptions.push(processOptions); + function processOptions(plot,options){ + if(options.series.candle){ + //plot.hooks.processRawData.push(processRawData); + plot.hooks.drawSeries.push(drawSeries); + } + } + /*function processRawData(plot,s,data,datapoints){ + if(s.candle){ + } + }*/ + function drawSeries(plot, ctx, serie){ + if (serie.candle) { + offset = plot.getPlotOffset(); + offset.left = offset.left; + var x1 = serie.xaxis.p2c(serie.data[0][0]); + var x2 = serie.xaxis.p2c(serie.data[1][0]); + var width = (x2 - x1) * 4 / 5; + for (var j = 0; j < serie.data.length; j++) { getAndDrawCandle(ctx, serie, width, serie.data[j]);} + } + } + function getAndDrawCandle(ctx, serie, width, data){ + var dt = data[0]; + var open = data[1]; + var close = data[2]; + var low = data[3]; + var high = data[4]; + drawCandle(ctx, serie, width, dt, open, low, close, high); + } + function drawCandle(ctx, serie, width, dt, open, low, close, high){ + var height; + if (open < close){ //Rising + y = offset.top + serie.yaxis.p2c(open); + height = serie.yaxis.p2c(close) - serie.yaxis.p2c(open); + ctx.fillStyle = '#51FF21'; + } else { //Decending + y = offset.top + serie.yaxis.p2c(close); + height = serie.yaxis.p2c(open) - serie.yaxis.p2c(close); + ctx.fillStyle = '#FF0000'; + } + ctx.strokeStyle = '#000000'; + ctx.lineWidth = 0; + x = offset.left + serie.xaxis.p2c(dt); + + //body + ctx.fillRect (x, y, width, height); + ctx.strokeRect(x, y, width, height); + + var highY = serie.yaxis.p2c(high); + var lowY = serie.yaxis.p2c(low); + + //top + var lineX; + if (highY < y + height){ + ctx.beginPath(); + lineX = x + (width /2); + ctx.moveTo(lineX,y + height); + ctx.lineTo(lineX,highY); + ctx.closePath(); + ctx.stroke(); + } + + //bottom + if (lowY > y){ + ctx.beginPath(); + ctx.moveTo(lineX,y); + ctx.lineTo(lineX,lowY); + ctx.closePath(); + ctx.stroke(); + } + } + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'candle', + version: '1.0' + }); +})(jQuery); \ No newline at end of file diff --git a/static/report/js/report.js b/static/report/js/report.js new file mode 100644 index 00000000000..095970b90b1 --- /dev/null +++ b/static/report/js/report.js @@ -0,0 +1,642 @@ +// TODO: +// - bypass nightmode in reports +// - on save/delete treatment ctx.bus.emit('data-received'); is not enough. we must add something like 'data-updated' + +(function () { + 'use strict'; + //for the tests window isn't the global object + var $ = window.$; + var _ = window._; + var moment = window.moment; + var Nightscout = window.Nightscout; + var client = Nightscout.client; + var report_plugins = Nightscout.report_plugins; + + if (serverSettings === undefined) { + console.error('server settings were not loaded, will not call init'); + } else { + client.init(serverSettings, Nightscout.plugins); + } + + // init HTML code + report_plugins.addHtmlFromPlugins( client ); + // make show() accessible outside for treatments.js + report_plugins.show = show; + + var translate = client.translate; + + var maxInsulinValue = 0 + ,maxCarbsValue = 0; + var maxdays = 3 * 31; + var datastorage = {}; + var daystoshow = {}; + var sorteddaystoshow = []; + + var targetBGdefault = { + 'mg/dl': { + low: client.settings.thresholds.bgTargetBottom + , high: client.settings.thresholds.bgTargetTop + } + , 'mmol': { + low: client.utils.scaleMgdl(client.settings.thresholds.bgTargetBottom) + , high: client.utils.scaleMgdl(client.settings.thresholds.bgTargetTop) + } + }; + + var ONE_MIN_IN_MS = 60000; + + prepareGUI(); + + // ****** FOOD CODE START ****** + var food_categories = []; + var food_list = []; + + var filter = { + category: '' + , subcategory: '' + , name: '' + }; + + function fillFoodForm(event) { + $('#rp_category').empty().append(''); + Object.keys(food_categories).forEach(function eachCategory(s) { + $('#rp_category').append(''); + }); + filter.category = ''; + fillFoodSubcategories(); + + $('#rp_category').change(fillFoodSubcategories); + $('#rp_subcategory').change(doFoodFilter); + $('#rp_name').on('input',doFoodFilter); + + return maybePrevent(event); + } + + function fillFoodSubcategories(event) { + filter.category = $('#rp_category').val(); + filter.subcategory = ''; + $('#rp_subcategory').empty().append(''); + if (filter.category !== '') { + Object.keys(food_categories[filter.category]).forEach(function eachSubCategory(s) { + $('#rp_subcategory').append(''); + }); + } + doFoodFilter(); + return maybePrevent(event); + } + + function doFoodFilter(event) { + if (event) { + filter.category = $('#rp_category').val(); + filter.subcategory = $('#rp_subcategory').val(); + filter.name = $('#rp_name').val(); + } + $('#rp_food').empty(); + for (var i=0; i' + o + ''); + } + + return maybePrevent(event); + } + + $('#info').html(''+translate('Loading food database')+' ...'); + $.ajax('/api/v1/food/regular.json', { + success: function foodLoadSuccess(records) { + records.forEach(function (r) { + food_list.push(r); + if (r.category && !food_categories[r.category]) { food_categories[r.category] = {}; } + if (r.category && r.subcategory) { food_categories[r.category][r.subcategory] = true; } + }); + fillFoodForm(); + } + }).done(function() { + if (food_list.length) { + enableFoodGUI(); + } else { + disableFoodGUI(); + } + }).fail(function() { + disableFoodGUI(); + }); + + function enableFoodGUI( ) { + $('#info').html(''); + + $('.rp_foodgui').css('display',''); + $('#rp_food').change(function (event) { + $('#rp_enablefood').prop('checked',true); + return maybePrevent(event); + }); + } + + function disableFoodGUI(){ + $('#info').html(''); + $('.rp_foodgui').css('display','none'); + } + + // ****** FOOD CODE END ****** + + + function getTimeZoneOffset () { + var offset; + if (client.sbx.data.profile.getTimezone()) { + offset = moment().tz(client.sbx.data.profile.getTimezone())._offset; + } else { + offset = new Date().getTimezoneOffset(); + } + return offset; + } + + function prepareGUI() { + $('.presetdates').click(function(event) { + var days = $(this).attr('days'); + $('#rp_enabledate').prop('checked',true); + return setDataRange(event,days); + }); + $('#rp_show').click(show); + $('#rp_notes').bind('input', function (event) { + $('#rp_enablenotes').prop('checked',true); + return maybePrevent(event); + }); + $('#rp_eventtype').bind('input', function (event) { + $('#rp_enableeventtype').prop('checked',true); + return maybePrevent(event); + }); + + // fill careportal events + $('#rp_eventtype').empty(); + _.each(client.careportal.events, function eachEvent(event) { + $('#rp_eventtype').append(''); + }); + $('#rp_eventtype').append(''); + + $('#rp_targetlow').val(targetBGdefault[client.settings.units.toLowerCase()].low); + $('#rp_targethigh').val(targetBGdefault[client.settings.units.toLowerCase()].high); + + $('.menutab').click(switchreport_handler); + + setDataRange(null,7); + } + + function sgvToColor(sgv,options) { + var color = 'darkgreen'; + + if (sgv > options.targetHigh) { + color = 'red'; + } else if (sgv < options.targetLow) { + color = 'red'; + } + + return color; + } + + function show(event) { + var options = { + width: 1000 + , height: 300 + , targetLow: 3.5 + , targetHigh: 10 + , raw: true + , notes: true + , food: true + , insulin: true + , carbs: true + , iob : true + , cob : true + , scale: report_plugins.consts.SCALE_LINEAR + , units: client.settings.units + }; + + // default time range if no time range specified in GUI + var zone = client.sbx.data.profile.getTimezone(); + var timerange = '&find[created_at][$gte]='+moment.tz('2000-01-01',zone).toISOString(); + //console.log(timerange,zone); + options.targetLow = parseFloat($('#rp_targetlow').val().replace(',','.')); + options.targetHigh = parseFloat($('#rp_targethigh').val().replace(',','.')); + options.raw = $('#rp_optionsraw').is(':checked'); + options.iob = $('#rp_optionsiob').is(':checked'); + options.cob = $('#rp_optionscob').is(':checked'); + options.notes = $('#rp_optionsnotes').is(':checked'); + options.food = $('#rp_optionsfood').is(':checked'); + options.insulin = $('#rp_optionsinsulin').is(':checked'); + options.carbs = $('#rp_optionscarbs').is(':checked'); + options.scale = ( $('#rp_linear').is(':checked') ? report_plugins.consts.SCALE_LINEAR : report_plugins.consts.SCALE_LOG ); + options.order = ( $('#rp_oldestontop').is(':checked') ? report_plugins.consts.ORDER_OLDESTONTOP : report_plugins.consts.ORDER_NEWESTONTOP ); + options.width = parseInt($('#rp_size :selected').attr('x')); + options.height = parseInt($('#rp_size :selected').attr('y')); + + var matchesneeded = 0; + + // date range + function datefilter() { + if ($('#rp_enabledate').is(':checked')) { + matchesneeded++; + var from = moment.tz($('#rp_from').val().replace(/\//g,'-') + 'T00:00:00',zone); + var to = moment.tz($('#rp_to').val().replace(/\//g,'-') + 'T23:59:59',zone); + timerange = '&find[created_at][$gte]='+from.toISOString()+'&find[created_at][$lt]='+to.toISOString(); + //console.log($('#rp_from').val(),$('#rp_to').val(),zone,timerange); + while (from <= to) { + if (daystoshow[from.format('YYYY-MM-DD')]) { + daystoshow[from.format('YYYY-MM-DD')]++; + } else { + daystoshow[from.format('YYYY-MM-DD')] = 1; + } + from.add(1, 'days'); + } + } + console.log('Dayfilter: ',daystoshow); + foodfilter(); + } + + //food filter + function foodfilter() { + if ($('#rp_enablefood').is(':checked')) { + matchesneeded++; + var _id = $('#rp_food').val(); + if (_id) { + var treatmentData; + var tquery = '?find[boluscalc.foods._id]=' + _id + timerange; + $.ajax('/api/v1/treatments.json'+tquery, { + success: function (xhr) { + treatmentData = xhr.map(function (treatment) { + return moment.tz(treatment.created_at,zone).format('YYYY-MM-DD'); + }); + // unique it + treatmentData = $.grep(treatmentData, function(v, k){ + return $.inArray(v ,treatmentData) === k; + }); + treatmentData.sort(function(a, b) { return a > b; }); + } + }).done(function () { + console.log('Foodfilter: ',treatmentData); + for (var d=0; d b; }); + } + }).done(function () { + console.log('Notesfilter: ',treatmentData); + for (var d=0; d b; }); + } + }).done(function () { + console.log('Eventtypefilter: ',treatmentData); + for (var d=0; d'+translate('Loading')+' ...
'); + for (var d in daystoshow) { + if (daystoshow[d]===matchesneeded) { + if (count < maxdays) { + $('#info').append($('
')); + count++; + loadData(d, options, dataLoadedCallback); + } else { + $('#info').append($('
'+d+' '+translate('not displayed')+'.
')); + delete daystoshow[d]; + } + } else { + delete daystoshow[d]; + } + } + if (count===0) { + $('#info').html(''+translate('Result is empty')+''); + $('#rp_show').css('display',''); + } + } + + var dayscount = 0; + var loadeddays = 0; + + function countDays() { + for (var d in daystoshow) { + if (daystoshow.hasOwnProperty(d)) { + if (daystoshow[d]===matchesneeded) { + if (dayscount < maxdays) { + dayscount++; + } + } + } + } + console.log('Total: ', daystoshow, 'Matches needed: ', matchesneeded, 'Will be loaded: ', dayscount); + } + + function dataLoadedCallback (day) { + loadeddays++; + sorteddaystoshow.push(day); + if (loadeddays === dayscount) { + sorteddaystoshow.sort(); + if (options.order === report_plugins.consts.ORDER_NEWESTONTOP) { + sorteddaystoshow.reverse(); + } + showreports(options); + } + } + + $('#rp_show').css('display','none'); + daystoshow = {}; + + datefilter(); + return maybePrevent(event); + } + + function showreports(options) { + // prepare some data used in more reports + datastorage.allstatsrecords = []; + datastorage.alldays = 0; + sorteddaystoshow.forEach(function eachDay(day) { + datastorage.allstatsrecords = datastorage.allstatsrecords.concat(datastorage[day].statsrecords); + datastorage.alldays++; + }); + options.maxInsulinValue = maxInsulinValue; + options.maxCarbsValue = maxCarbsValue; + + + report_plugins.eachPlugin(function (plugin) { + // jquery plot doesn't draw to hidden div + $('#'+plugin.name+'-placeholder').css('display',''); + //console.log('Drawing ',plugin.name); + plugin.report(datastorage,sorteddaystoshow,options); + if (!$('#'+plugin.name).hasClass('selected')) { + $('#'+plugin.name+'-placeholder').css('display','none'); + } + }); + + $('#info').html(''); + $('#rp_show').css('display',''); + } + + function setDataRange(event,days) { + $('#rp_to').val(moment().format('YYYY-MM-DD')); + $('#rp_from').val(moment().add(-days+1, 'days').format('YYYY-MM-DD')); + return maybePrevent(event); + } + + function switchreport_handler(event) { + var id = $(this).attr('id'); + + $('.menutab').removeClass('selected'); + $('#'+id).addClass('selected'); + + $('.tabplaceholder').css('display','none'); + $('#'+id+'-placeholder').css('display',''); + return maybePrevent(event); + } + + function loadData(day, options, callback) { + // check for loaded data + if (datastorage[day] && day !== moment().format('YYYY-MM-DD')) { + callback(day); + return; + } + // patientData = [actual, predicted, mbg, treatment, cal, devicestatusData]; + var data = {}; + var cgmData = [] + , mbgData = [] + , treatmentData = [] + , calData = [] + ; + var dt = new Date(day); + var from = dt.getTime() + getTimeZoneOffset() * 60 * 1000; + var to = from + 1000 * 60 * 60 * 24; + var query = '?find[date][$gte]='+from+'&find[date][$lt]='+to+'&count=10000'; + + $('#info-' + day).html(''+translate('Loading CGM data of')+' '+day+' ...'); + $.ajax('/api/v1/entries.json'+query, { + success: function (xhr) { + xhr.forEach(function (element) { + if (element) { + if (element.mbg) { + mbgData.push({ + y: element.mbg + , mills: element.date + , d: element.dateString + , device: element.device + }); + } else if (element.sgv) { + cgmData.push({ + y: element.sgv + , mills: element.date + , d: element.dateString + , device: element.device + , filtered: element.filtered + , unfiltered: element.unfiltered + , noise: element.noise + , rssi: element.rssi + , sgv: element.sgv + }); + } else if (element.type === 'cal') { + calData.push({ + mills: element.date + , d: element.dateString + , scale: element.scale + , intercept: element.intercept + , slope: element.slope + }); + } + } + }); + // sometimes cgm contains duplicates. uniq it. + data.sgv = cgmData.slice(); + data.sgv.sort(function(a, b) { return a.mills - b.mills; }); + var lastDate = 0; + data.sgv = data.sgv.filter(function(d) { + var ok = (lastDate + ONE_MIN_IN_MS) < d.mills; + lastDate = d.mills; + return ok; + }); + data.mbg = mbgData.slice(); + data.mbg.sort(function(a, b) { return a.mills - b.mills; }); + data.cal = calData.slice(); + data.cal.sort(function(a, b) { return a.mills - b.mills; }); + } + }).done(function () { + $('#info-' + day).html(''+translate('Loading treatments data of')+' '+day+' ...'); + var tquery = '?find[created_at][$gte]='+new Date(from).toISOString()+'&find[created_at][$lt]='+new Date(to).toISOString(); + $.ajax('/api/v1/treatments.json'+tquery, { + success: function (xhr) { + treatmentData = xhr.map(function (treatment) { + var timestamp = new Date(treatment.timestamp || treatment.created_at); + treatment.mills = timestamp.getTime(); + return treatment; + }); + data.treatments = treatmentData.slice(); + data.treatments.sort(function(a, b) { return a.mills - b.mills; }); + } + }).done(function () { + $('#info-' + day).html(''+translate('Processing data of')+' '+day+' ...'); + processData(data, day, options, callback); + }); + + }); + } + + function processData(data, day, options, callback) { + // treatments + data.treatments.forEach(function (d) { + if (parseFloat(d.insulin) > maxInsulinValue) { + maxInsulinValue = parseFloat(d.insulin); + } + if (parseFloat(d.carbs) > maxCarbsValue) { + maxCarbsValue = parseFloat(d.carbs); + } + }); + + var cal = data.cal[data.cal.length-1]; + var temp1 = [ ]; + var rawbg = Nightscout.plugins('rawbg'); + if (cal) { + temp1 = data.sgv.map(function (entry) { + entry.mgdl = entry.y; // value names changed from enchilada + var rawBg = rawbg.calc(entry, cal); + return { mills: entry.mills, date: new Date(entry.mills - 2 * 1000), y: rawBg, sgv: client.utils.scaleMgdl(rawBg), color: 'gray', type: 'rawbg', filtered: entry.filtered, unfiltered: entry.unfiltered }; + }).filter(function(entry) { return entry.y > 0}); + } + var temp2 = data.sgv.map(function (obj) { + return { mills: obj.mills, date: new Date(obj.mills), y: obj.y, sgv: client.utils.scaleMgdl(obj.y), color: sgvToColor(client.utils.scaleMgdl(obj.y),options), type: 'sgv', noise: obj.noise, filtered: obj.filtered, unfiltered: obj.unfiltered}; + }); + data.sgv = [].concat(temp1, temp2); + + //Add MBG's also, pretend they are SGV's + data.sgv = data.sgv.concat(data.mbg.map(function (obj) { return { date: new Date(obj.mills), y: obj.y, sgv: client.utils.scaleMgdl(obj.y), color: 'red', type: 'mbg', device: obj.device } })); + + // make sure data range will be exactly 24h + var from = new Date(new Date(day).getTime() + (getTimeZoneOffset() * 60 * 1000)); + var to = new Date(from.getTime() + 1000 * 60 * 60 * 24); + data.sgv.push({ date: from, y: 40, sgv: 40, color: 'transparent', type: 'rawbg'}); + data.sgv.push({ date: to, y: 40, sgv: 40, color: 'transparent', type: 'rawbg'}); + + // clear error data. we don't need it to display them + data.sgv = data.sgv.filter(function (d) { + if (d.y < 39) { + return false; + } + return true; + }); + + // for other reports + data.statsrecords = data.sgv.filter(function(r) { + if (r.type) { + return r.type === 'sgv'; + } else { + return true; + } + }).map(function (r) { + var ret = {}; + ret.sgv = parseFloat(r.sgv); + ret.bgValue = parseInt(r.y); + ret.displayTime = r.date; + return ret; + }); + + + datastorage[day] = data; + callback(day); + } + + function maybePrevent(event) { + if (event) { + event.preventDefault(); + } + return false; + } +})(); \ No newline at end of file diff --git a/static/treatments.html b/static/treatments.html deleted file mode 100644 index 24761c4d7af..00000000000 --- a/static/treatments.html +++ /dev/null @@ -1,98 +0,0 @@ - - - - - Nightscout: Treatments - - - - - - - - -
-

Nightscout: Treatments

- - - - - - - - - - - - - - - - - - - - - - - -
TimeEvent TypeBGInsulinCarbsEntered ByNotes
{{treatment.created_at | date:'short'}}{{treatment.eventType}}{{glucoseDisplay(treatment)}}{{treatment.insulin | number: 2}}{{treatment.carbs}}{{treatment.enteredBy}}{{treatment.notes}}
-
- - - diff --git a/swagger.yaml b/swagger.yaml new file mode 100644 index 00000000000..3dce8801407 --- /dev/null +++ b/swagger.yaml @@ -0,0 +1,637 @@ +swagger: '2.0' +info: + title: Nightscout API + description: Own your DData with the Nightscout API + version: "0.8.0" + license: + name: AGPL 3 + url: https://www.gnu.org/licenses/agpl.txt +basePath: /api/v1 +produces: + - application/json +security: + - api_secret: [] + +paths: + + /entries/{spec}: + get: + summary: All Entries matching query + description: | + The Entries endpoint returns information about the + Nightscout entries. + + parameters: + - name: spec + in: path + type: string + description: | + entry id, such as `55cf81bc436037528ec75fa5` or a type filter such + as `sgv`, `mbg`, etc. + + default: sgv + required: true + - name: find + in: query + description: | + The query used to find entries, support nested query syntax, for + example `find[dateString][$gte]=2015-08-27`. All find parameters + are interpreted as strings. + required: false + type: string + - name: count + in: query + description: Number of entries to return. + required: false + type: number + tags: + - Entries + responses: + "200": + description: An array of entries + schema: + $ref: '#/definitions/Entries' + default: + description: Entries + schema: + $ref: '#/definitions/Entries' + + /slice/{storage}/{field}/{type}/{prefix}/{regex}: + get: + summary: All Entries matching query + description: The Entries endpoint returns information about the Nightscout entries. + parameters: + - name: storage + in: path + type: string + description: Prefix to use in constructing a prefix-based regex, default is `entries`. + required: true + default: entries + - name: field + in: path + type: string + description: Name of the field to use Regex against in query object, default is `dateString`. + default: dateString + required: true + - name: type + in: path + type: string + description: The type field to search against, default is sgv. + required: true + default: sgv + - name: prefix + in: path + type: string + description: Prefix to use in constructing a prefix-based regex. + required: true + default: '2015' + - name: regex + in: path + type: string + description: | + Tail part of regexp to use in expanding/construccting a query object. + Regexp also has bash-style brace and glob expansion applied to it, + creating ways to search for modal times of day, perhaps using + something like this syntax: `T{15..17}:.*`, this would search for + all records from 3pm to 5pm. + required: true + default: .* + - name: find + in: query + description: | + The query used to find entries, support nested query syntax, for + example `find[dateString][$gte]=2015-08-27`. All find parameters + are interpreted as strings. + required: false + type: string + - name: count + in: query + description: Number of entries to return. + required: false + type: number + tags: + - Entries + responses: + "200": + description: An array of entries + schema: + $ref: '#/definitions/Entries' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + /echo/{storage}/{spec}: + get: + summary: View generated Mongo Query object + description: | + Information about the mongo query object created by the query. + + parameters: + - name: storage + in: path + type: string + description: | + `entries`, or `treatments` to select the storage layer. + + default: sgv + required: true + - name: spec + in: path + type: string + description: | + entry id, such as `55cf81bc436037528ec75fa5` or a type filter such + as `sgv`, `mbg`, etc. + This parameter is optional. + + default: sgv + required: true + - name: find + in: query + description: | + The query used to find entries, support nested query syntax, for + example `find[dateString][$gte]=2015-08-27`. All find parameters + are interpreted as strings. + required: false + type: string + - name: count + in: query + description: Number of entries to return. + required: false + type: number + tags: + - Entries + - Debug + responses: + "200": + description: An array of entries + schema: + $ref: '#/definitions/MongoQuery' + + /times/echo/{prefix}/{regex}: + get: + summary: Echo the query object to be used. + description: Echo debug information about the query object constructed. + parameters: + - name: prefix + in: path + type: string + description: Prefix to use in constructing a prefix-based regex. + required: true + - name: regex + in: path + type: string + description: | + Tail part of regexp to use in expanding/construccting a query object. + Regexp also has bash-style brace and glob expansion applied to it, + creating ways to search for modal times of day, perhaps using + something like this syntax: `T{15..17}:.*`, this would search for + all records from 3pm to 5pm. + required: true + - name: find + in: query + description: The query used to find entries, support nested query syntax, for example `find[dateString][$gte]=2015-08-27`. All find parameters are interpreted as strings. + required: false + type: string + - name: count + in: query + description: Number of entries to return. + required: false + type: number + tags: + - Entries + - Debug + responses: + "200": + description: An array of entries + schema: + $ref: '#/definitions/MongoQuery' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + + /times/{prefix}/{regex}: + get: + summary: All Entries matching query + description: The Entries endpoint returns information about the Nightscout entries. + parameters: + - name: prefix + in: path + type: string + description: Prefix to use in constructing a prefix-based regex. + required: true + - name: regex + in: path + type: string + description: | + Tail part of regexp to use in expanding/construccting a query object. + Regexp also has bash-style brace and glob expansion applied to it, + creating ways to search for modal times of day, perhaps using + something like this syntax: `T{15..17}:.*`, this would search for + all records from 3pm to 5pm. + required: true + - name: find + in: query + description: The query used to find entries, support nested query syntax, for example `find[dateString][$gte]=2015-08-27`. All find parameters are interpreted as strings. + required: false + type: string + - name: count + in: query + description: Number of entries to return. + required: false + type: number + tags: + - Entries + responses: + "200": + description: An array of entries + schema: + $ref: '#/definitions/Entries' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + + /entries: + get: + summary: All Entries matching query + description: The Entries endpoint returns information about the Nightscout entries. + parameters: + - name: find + in: query + description: The query used to find entries, support nested query syntax, for example `find[dateString][$gte]=2015-08-27`. All find parameters are interpreted as strings. + required: false + type: string + - name: count + in: query + description: Number of entries to return. + required: false + type: number + tags: + - Entries + responses: + "200": + description: An array of entries + schema: + $ref: '#/definitions/Entries' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + post: + tags: + - Entries + summary: Add new entries. + description: "" + operationId: addEntries + consumes: + - application/json + - text/plain + produces: + - application/json + - text/plain + parameters: + - in: body + name: body + description: Entries to be uploaded. + required: true + schema: + $ref: "#/definitions/Entries" + responses: + "405": + description: Invalid input + "200": + description: Rejected list of entries. Empty list is success. + delete: + tags: + - Entries + summary: Delete entries matching query. + description: "Remove entries, same search syntax as GET." + operationId: remove + parameters: + - name: find + in: query + description: The query used to find entries, support nested query syntax, for example `find[dateString][$gte]=2015-08-27`. All find parameters are interpreted as strings. + required: false + type: string + - name: count + in: query + description: Number of entries to return. + required: false + type: number + responses: + "200": + description: Empty list is success. + + /treatments: + get: + summary: Treatments + description: The Treatments endpoint returns information about the Nightscout treatments. + tags: + - Treatments + parameters: + - name: find + in: query + description: + The query used to find entries, supports nested query syntax. Examples + `find[insulin][$gte]=3` + `find[carb][$gte]=100` + `find[eventType]=Correction+Bolus` + All find parameters are interpreted as strings. + required: false + type: string + - name: count + in: query + description: Number of entries to return. + required: false + type: number + responses: + "200": + description: An array of treatments + schema: + $ref: '#/definitions/Treatments' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + post: + tags: + - Treatments + summary: Add new treatments. + description: "" + operationId: addTreatments + consumes: + - application/json + produces: + - application/json + parameters: + - in: body + name: body + description: Treatments to be uploaded. + required: true + schema: + $ref: "#/definitions/Treatments" + responses: + "405": + description: Invalid input + "200": + description: Rejected list of treatments. Empty list is success. + + /profile: + get: + summary: Profile + description: The Profile endpoint returns information about the Nightscout Treatment Profiles. + tags: + - Profile + responses: + "200": + description: An array of treatments + schema: + $ref: '#/definitions/Profile' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + + /status: + get: + summary: Status + description: Server side status, default settings and capabilities. + tags: + - Status + responses: + "200": + description: Server capabilities and status. + schema: + $ref: '#/definitions/Status' + default: + description: Unexpected error + schema: + $ref: '#/definitions/Error' + +securityDefinitions: + api_secret: + type: apiKey + name: api_secret + in: header + description: The hash of the API_SECRET env var + +definitions: + Entry: + properties: + type: + type: string + description: "sgv, mbg, cal, etc" + dateString: + type: string + description: dateString, prefer ISO `8601` + date: + type: number + description: Epoch + sgv: + type: number + description: The glucose reading. (only available for sgv types) + direction: + type: string + description: Direction of glucose trend reported by CGM. (only available for sgv types) + noise: + type: number + description: Noise level at time of reading. (only available for sgv types) + filtered: + type: number + description: The raw filtered value directly from CGM transmitter. (only available for sgv types) + unfiltered: + type: number + description: The raw unfiltered value directly from CGM transmitter. (only available for sgv types) + rssi: + type: number + description: The signal strength from CGM transmitter. (only available for sgv types) + + Entries: + type: array + items: + $ref: '#/definitions/Entry' + + Treatment: + properties: + _id: + type: string + description: Internally assigned id. + eventType: + type: string + description: The type of treatment event. + created_at: + type: string + description: The date of the event, might be set retroactively . + glucose: + type: string + description: Current glucose. + glucoseType: + type: string + description: Method used to obtain glucose, Finger or Sensor. + carbs: + type: number + description: Number of carbs. + insulin: + type: number + description: Amount of insulin, if any. + units: + type: string + description: The units for the glucose value, mg/dl or mmol. + notes: + type: string + description: Description/notes of treatment. + enteredBy: + type: string + description: Who entered the treatment. + + Treatments: + type: array + items: + $ref: '#/definitions/Treatment' + + Profile: + properties: + sens: + type: integer + description: 'Internally assigned id' + dia: + type: integer + description: 'Internally assigned id' + carbratio: + type: integer + description: 'Internally assigned id' + carbs_hr: + type: integer + description: 'Internally assigned id' + _id: + type: string + description: 'Internally assigned id' + + Status: + properties: + apiEnabled: + type: boolean + description: 'Whether or not the REST API is enabled.' + careportalEnabled: + type: boolean + description: 'Whether or not the careportal is enabled in the API.' + head: + type: string + description: 'The git identifier for the running instance of the app.' + name: + type: string + description: Nightscout (static) + version: + type: string + description: 'The version label of the app.' + settings: + $ref: '#/definitions/Settings' + extendedSettings: + $ref: '#/definitions/ExtendedSettings' + + Settings: + properties: + units: + type: string + description: Default units for glucose measurements across the server. + timeFormat: + type: string + description: Default time format + enum: + - 12 + - 24 + customTitle: + type: string + description: Default custom title to be displayed system wide. + nightMode: + type: boolean + description: Should Night mode be enabled by default? + theme: + type: string + description: Default theme to be displayed system wide, `default` or `colors`. + language: + type: string + description: Default language code to be used system wide + showPlugins: + type: string + description: Plugins that should be shown by default + showRawbg: + type: string + description: If Raw BG is enabled when should it be shown? `never`, `always`, `noise` + alarmTypes: + type: array + items: + type: string + enum: + - simple + - predict + description: Enabled alarm types, can be multiple + alarmUrgentHigh: + type: boolean + description: Enable/Disable client-side Urgent High alarms by default, for use with `simple` alarms. + alarmHigh: + type: boolean + description: Enable/Disable client-side High alarms by default, for use with `simple` alarms. + alarmLow: + type: boolean + description: Enable/Disable client-side Low alarms by default, for use with `simple` alarms. + alarmUrgentLow: + type: boolean + description: Enable/Disable client-side Urgent Low alarms by default, for use with `simple` alarms. + alarmTimeagoWarn: + type: boolean + description: Enable/Disable client-side stale data alarms by default. + alarmTimeagoWarnMins: + type: number + description: Number of minutes before a stale data warning is generated. + alarmTimeagoUrgent: + type: boolean + description: Enable/Disable client-side urgent stale data alarms by default. + alarmTimeagoUrgentMins: + type: number + description: Number of minutes before a stale data warning is generated. + enable: + type: array + items: + type: string + description: Enabled features + thresholds: + $ref: '#/definitions/Threshold' + + Threshold: + properties: + bg_high: + type: integer + description: 'High BG range.' + bg_target_top: + type: integer + description: 'Top of target range.' + bg_target_bottom: + type: integer + description: 'Bottom of target range.' + bg_low: + type: integer + description: 'Low BG range.' + + ExtendedSettings: + description: Extended settings of client side plugins + + MongoQuery: + description: Mongo Query object. + + + Error: + properties: + code: + type: integer + format: int32 + message: + type: string + fields: + type: object + diff --git a/testing/convert-treatments.js b/testing/convert-treatments.js index 1ef65786be9..3c3cbf17f6e 100644 --- a/testing/convert-treatments.js +++ b/testing/convert-treatments.js @@ -1,16 +1,16 @@ db.treatments.find().forEach( - function (elem) { - db.treatments.update( - { - _id: elem._id - }, - { - $set: { - glucose: elem.glucoseValue, - insulin: elem.insulinGiven, - carbs: elem.carbsGiven - } - } - ); - } + function (elem) { + db.treatments.update( + { + _id: elem._id + }, + { + $set: { + glucose: elem.glucoseValue, + insulin: elem.insulinGiven, + carbs: elem.carbsGiven + } + } + ); + } ); diff --git a/testing/make_high_data.js b/testing/make_high_data.js index 8cbf1917859..bfdb21c34a3 100644 --- a/testing/make_high_data.js +++ b/testing/make_high_data.js @@ -1,26 +1,26 @@ var fs = require('fs'); +var times = require('../lib/times'); -var data = "" +var data = ''; var END_TIME = Date.now(); -var FIVE_MINS_IN_MS = 300000; var TIME_PERIOD_HRS = 24; var DATA_PER_HR = 12; var START_BG = 50; var currentBG = START_BG; -var currentTime = END_TIME - (TIME_PERIOD_HRS * DATA_PER_HR * FIVE_MINS_IN_MS); +var currentTime = END_TIME - (TIME_PERIOD_HRS * DATA_PER_HR * times.mins(5).msecs); for(var i = 0; i < TIME_PERIOD_HRS * DATA_PER_HR; i++) { - currentBG += Math.ceil(Math.cos(i)*5+.2); - currentTime += FIVE_MINS_IN_MS; - data += "1," + currentBG + ",,,,,,,,," + new Date(currentTime).toString() + ",,,,\n"; + currentBG += Math.ceil(Math.cos(i)*5+.2); + currentTime += times.mins(5).msecs; + data += '1,' + currentBG + ',,,,,,,,,' + new Date(currentTime).toString() + ',,,,\n'; } -fs.writeFile("../Dexcom.csv", data); +fs.writeFile('../Dexcom.csv', data); function makedata() { - currentBG -= 1; - currentTime += FIVE_MINS_IN_MS; - data += "1," + currentBG + ",,,,,,,,," + new Date(currentTime).toString() + ",,,,\n"; - fs.writeFile("../Dexcom.csv", data); + currentBG -= 1; + currentTime += times.mins(5).msecs; + data += '1,' + currentBG + ',,,,,,,,,' + new Date(currentTime).toString() + ',,,,\n'; + fs.writeFile('../Dexcom.csv', data); } -setInterval(makedata, 1000 * 10) \ No newline at end of file +setInterval(makedata, 1000 * 10); \ No newline at end of file diff --git a/testing/populate.js b/testing/populate.js index f4fff19ae6e..61bbadf0342 100644 --- a/testing/populate.js +++ b/testing/populate.js @@ -1,125 +1,33 @@ 'use strict'; -/////////////////////////////////////////////////// -// This script is intended to be run as a cron job -// every n-minutes or whatever the equiv is on windows -// -// Author: John A. [euclidjda](https://github.com/euclidjda) -// Source: https://gist.github.com/euclidjda/4ae207a89921f21382a9 -/////////////////////////////////////////////////// -/////////////////////////////////////////////////// -// DB Connection setup and utils -/////////////////////////////////////////////////// +var mongodb = require('mongodb'); +var env = require('./../env')(); -var mongodb = require('mongodb'); -var software = require('./package.json'); -var env = require('./env')( ); +var util = require('./helpers/util'); main(); -function main( ) { +function main() { + var MongoClient = mongodb.MongoClient; + MongoClient.connect(env.mongo, function connected(err, db) { - var MongoClient = mongodb.MongoClient; - - MongoClient.connect(env.mongo, function connected (err, db) { - - console.log("Connected to mongo, ERROR: %j", err); - if (err) { throw err; } - populate_collection( db ); - - }); - -} - -function populate_collection( db ) { - - //console.log( 'mongo = ' + env.mongo ); - //console.log( 'collection = ' + env.mongo_collection ); - - var cgm_collection = db.collection( env.mongo_collection ); - - var new_cgm_record = get_cgm_record(); - - cgm_collection.insert( new_cgm_record, function(err,created) { - - // TODO: Error checking - process.exit( 0 ); - - } ); - - -} - -function get_cgm_record( ) { - - var dateobj = new Date(); - var datemil = dateobj.getTime(); - var datesec = datemil / 1000; - var datestr = getDateString( dateobj ); - - // We put the time in a range from -1 to +1 for every thiry minute period - var range = (datesec % 1800)/900 - 1.0; - - // The we push through a COS function and scale between 40 and 400 (so it is like a bg level) - var sgv = Math.floor(360*(Math.cos( 10.0 * range / 3.14 ) / 2 + 0.5)) + 40; - var dir = range > 0.0 ? "FortyFiveDown" : "FortyFiveUp"; - - console.log( 'Writing Record: '); - console.log( 'sgv = ' + sgv ); - console.log( 'date = ' + datemil ); - console.log( 'dir = ' + dir ); - console.log( 'str = ' + datestr ); - - var mondo_db = null; - var doc = { 'device' :'dexcom' , - 'date' : datemil , - 'sgv' : sgv , - 'direction' : dir , - 'dateString' : datestr }; - - - return doc; + console.log('Connecting to mongo...'); + if (err) { + console.log('Error occurred: ', err); + throw err; + } + populate_collection(db); + }); } -function getDateString( d ) { - - // How I wish js had strftime. This would be one line of code! - - var month = d.getMonth(); - var day = d.getDay(); - var year = d.getFullYear(); - - if (month < 10 ) month = '0'+month; - if (day < 10 ) day = '0'+day; +function populate_collection(db) { + var cgm_collection = db.collection(env.mongo_collection); + var new_cgm_record = util.get_cgm_record(); - var hour = d.getHours(); - var min = d.getMinutes(); - var sec = d.getSeconds(); - - var ampm = 'PM'; - - if (hour < 12) - { - ampm = "AM"; - } - else - { - ampm = "PM"; - } - - if (hour == 0) - { - hour = 12; + cgm_collection.insert(new_cgm_record, function (err) { + if (err) { + throw err; } - if (hour > 12) - { - hour = hour - 12; - } - - if (hour < 10) hour = '0' + hour; - if (min < 10) min = '0' + min; - if (sec < 10) sec = '0' + sec; - - return month + '/' + day + '/' + year + ' ' + hour + ':' + min + ':' + sec + ' ' + ampm; - + process.exit(0); + }); } diff --git a/testing/populate_rest.js b/testing/populate_rest.js new file mode 100644 index 00000000000..a3f75faddbe --- /dev/null +++ b/testing/populate_rest.js @@ -0,0 +1,40 @@ +'use strict'; + +var env = require('./../env')(); +var http = require('http'); +var util = require('./util'); + +main(); + +function main() { + send_entry_rest(); +} + +function send_entry_rest() { + var new_cgm_record = util.get_cgm_record(); + var new_cgm_record_string = JSON.stringify(new_cgm_record); + + var options = { + host: 'localhost', + port: env.PORT, + path: '/api/v1/entries/', + method: 'POST', + headers: { + 'api-secret' : env.api_secret, + 'Content-Type': 'application/json', + 'Content-Length': new_cgm_record_string.length + } + }; + + var req = http.request(options, function(res) { + console.log('Ok: ', res.statusCode); + }); + + req.on('error', function(e) { + console.error('error'); + console.error(e); + }); + + req.write(new_cgm_record_string); + req.end(); +} \ No newline at end of file diff --git a/testing/util.js b/testing/util.js new file mode 100644 index 00000000000..82a61fd4702 --- /dev/null +++ b/testing/util.js @@ -0,0 +1,29 @@ +'use strict'; + +exports.get_cgm_record = function() { + var dateobj = new Date(); + var datemil = dateobj.getTime(); + var datesec = datemil / 1000; + var datestr = dateobj.toISOString(); + + // We put the time in a range from -1 to +1 for every thiry minute period + var range = (datesec % 1800) / 900 - 1.0; + + // The we push through a COS function and scale between 40 and 400 (so it is like a bg level) + var sgv = Math.floor(360 * (Math.cos(10.0 * range / 3.14) / 2 + 0.5)) + 40; + var dir = range > 0.0 ? 'FortyFiveDown' : 'FortyFiveUp'; + + console.log('Writing Record: '); + console.log('sgv = ' + sgv); + console.log('date = ' + datemil); + console.log('dir = ' + dir); + console.log('str = ' + datestr); + + return { + 'device': 'dexcom', + 'date': datemil, + 'sgv': sgv, + 'direction': dir, + 'dateString': datestr + }; +}; \ No newline at end of file diff --git a/tests/admintools.test.js b/tests/admintools.test.js new file mode 100644 index 00000000000..c990d8639bc --- /dev/null +++ b/tests/admintools.test.js @@ -0,0 +1,210 @@ +'use strict'; + +require('should'); +var _ = require('lodash'); +var benv = require('benv'); +var read = require('fs').readFileSync; +var serverSettings = require('./fixtures/default-server-settings'); + +var nowData = { + sgvs: [ + { mgdl: 100, mills: Date.now(), direction: 'Flat', type: 'sgv' } + ] +}; + +var someData = { + '/api/v1/devicestatus.json?count=500': [ + { + '_id': { + '$oid': '56096da3c5d0fef41b212362' + }, + 'uploaderBattery': 37, + 'created_at': '2015-09-28T16:41:07.144Z' + }, + { + '_id': { + '$oid': '56096da3c5d0fef41b212363' + }, + 'uploaderBattery': 38, + 'created_at': '2025-09-28T16:41:07.144Z' + } + ], + '/api/v1/treatments.json?&find[created_at][$gte]=': [ + { + '_id': '5609a9203c8104a8195b1c1e', + 'enteredBy': '', + 'eventType': 'Carb Correction', + 'carbs': 3, + 'created_at': '2025-09-28T20:54:00.000Z' + } + ], + '/api/v1/entries.json?&find[date][$gte]=': [ + { + '_id': '560983f326c5a592d9b9ae0c', + 'device': 'dexcom', + 'date': 1543464149000, + 'sgv': 83, + 'direction': 'Flat', + 'type': 'sgv', + 'filtered': 107632, + 'unfiltered': 106256, + 'rssi': 178, + 'noise': 1 + } + ] + }; + + +describe('admintools', function ( ) { + var self = this; + + before(function (done) { + benv.setup(function() { + self.$ = require('jquery'); + self.$.localStorage = require('./fixtures/localstorage'); + + self.$.fn.tipsy = function mockTipsy ( ) { }; + + self.$.fn.dialog = function mockDialog (opts) { + function maybeCall (name, obj) { + if (obj[name] && obj[name].call) { + obj[name](); + } + + } + maybeCall('open', opts); + + _.forEach(opts.buttons, function (button) { + maybeCall('click', button); + }); + }; + + var indexHtml = read(__dirname + '/../static/admin/index.html', 'utf8'); + self.$('body').html(indexHtml); + + //var filesys = require('fs'); + //var logfile = filesys.createWriteStream('out.txt', { flags: 'a'} ) + + self.$.ajax = function mockAjax (url, opts) { + //logfile.write(url+'\n'); + //console.log(url,opts); + if (opts && opts.success && opts.success.call) { + if (url.indexOf('/api/v1/treatments.json?&find[created_at][$gte]=')===0) { + url = '/api/v1/treatments.json?&find[created_at][$gte]='; + } + if (url.indexOf('/api/v1/entries.json?&find[date][$gte]=')===0) { + url = '/api/v1/entries.json?&find[date][$gte]='; + } + return { + done: function mockDone (fn) { + if (someData[url]) { + console.log('+++++Data for ' + url + ' sent'); + opts.success(someData[url]); + } else { + console.log('-----Data for ' + url + ' missing'); + opts.success([]); + } + fn(); + return self.$.ajax(); + }, + fail: function mockFail () { + return self.$.ajax(); + } + }; + } + return { + done: function mockDone (fn) { + fn({message: 'OK'}); + return self.$.ajax(); + }, + fail: function mockFail () { + return self.$.ajax(); + } + }; + }; + + self.$.plot = function mockPlot () { + }; + + var d3 = require('d3'); + //disable all d3 transitions so most of the other code can run with jsdom + d3.timer = function mockTimer() { }; + + benv.expose({ + $: self.$ + , jQuery: self.$ + , d3: d3 + , serverSettings: serverSettings + , io: { + connect: function mockConnect ( ) { + return { + on: function mockOn ( ) { } + }; + } + } + }); + + benv.require(__dirname + '/../bundle/bundle.source.js'); + benv.require(__dirname + '/../static/admin/js/admin.js'); + + done(); + }); + }); + + after(function (done) { + benv.teardown(true); + done(); + }); + + it ('should produce some html', function (done) { + var plugins = require('../lib/plugins/')().registerClientDefaults(); + var client = require('../lib/client'); + + var hashauth = require('../lib/hashauth'); + hashauth.init(client,$); + hashauth.verifyAuthentication = function mockVerifyAuthentication(next) { + hashauth.authenticated = true; + next(true); + }; + + window.confirm = function mockConfirm (text) { + console.log('Confirm:', text); + return true; + }; + + window.alert = function mockAlert () { + return true; + }; + + client.init(serverSettings, plugins); + client.dataUpdate(nowData); + + //var result = $('body').html(); + //var filesys = require('fs'); + //var logfile = filesys.createWriteStream('out.txt', { flags: 'a'} ) + //logfile.write($('body').html()); + + //console.log(result); + + $('#admin_cleanstatusdb_0_html + button').text().should.equal('Delete all documents'); // devicestatus button + $('#admin_cleanstatusdb_0_status').text().should.equal('Database contains 2 records'); // devicestatus init result + + $('#admin_cleanstatusdb_0_html + button').click(); + $('#admin_cleanstatusdb_0_status').text().should.equal('All records removed ...'); // devicestatus code result + + $('#admin_futureitems_0_html + button').text().should.equal('Remove treatments in the future'); // futureitems button 0 + $('#admin_futureitems_0_status').text().should.equal('Database contains 1 future records'); // futureitems init result 0 + + $('#admin_futureitems_0_html + button').click(); + $('#admin_futureitems_0_status').text().should.equal('Record 5609a9203c8104a8195b1c1e removed ...'); // futureitems code result 0 + + $('#admin_futureitems_1_html + button').text().should.equal('Remove entries in the future'); // futureitems button 1 + $('#admin_futureitems_1_status').text().should.equal('Database contains 1 future records'); // futureitems init result 1 + + $('#admin_futureitems_1_html + button').click(); + $('#admin_futureitems_1_status').text().should.equal('Record 560983f326c5a592d9b9ae0c removed ...'); // futureitems code result 1 + + done(); + }); + +}); diff --git a/tests/api.entries.test.js b/tests/api.entries.test.js index d8a60230b55..9696269d686 100644 --- a/tests/api.entries.test.js +++ b/tests/api.entries.test.js @@ -1,49 +1,160 @@ +'use strict'; var request = require('supertest'); -var should = require('should'); var load = require('./fixtures/load'); +var bootevent = require('../lib/bootevent'); +require('should'); describe('Entries REST api', function ( ) { var entries = require('../lib/api/entries/'); + + this.timeout(10000); before(function (done) { var env = require('../env')( ); this.wares = require('../lib/middleware/')(env); - var store = require('../lib/storage')(env); - this.archive = require('../lib/entries').storage(env.mongo_collection, store); + this.archive = null; this.app = require('express')( ); this.app.enable('api'); var self = this; - store(function ( ) { - self.app.use('/', entries(self.app, self.wares, self.archive)); - self.archive.create(load('json'), done); + bootevent(env).boot(function booted (ctx) { + self.app.use('/', entries(self.app, self.wares, ctx)); + self.archive = require('../lib/entries')(env, ctx); + + var creating = load('json'); + creating.push({type: 'sgv', sgv: 100, date: Date.now()}); + self.archive.create(creating, done); }); }); - after(function (done) { + + beforeEach(function (done) { + var creating = load('json'); + creating.push({type: 'sgv', sgv: 100, date: Date.now()}); + this.archive.create(creating, done); + }); + + afterEach(function (done) { this.archive( ).remove({ }, done); }); - it('should be a module', function ( ) { - entries.should.be.ok; + after(function (done) { + this.archive( ).remove({ }, done); + }); + // keep this test pinned at or near the top in order to validate all + // entries successfully uploaded. if res.body.length is short of the + // expected value, it may indicate a regression in the create + // function callback logic in entries.js. + it('gets requested number of entries', function (done) { + var count = 30; + request(this.app) + .get('/entries.json?find[dateString][$gte]=2014-07-19&count=' + count) + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(count); + done(); + }); }); - it('/entries.json', function (done) { + + it('gets default number of entries', function (done) { + var defaultCount = 10; request(this.app) - .get('/entries.json') + .get('/entries/sgv.json?find[dateString][$gte]=2014-07-19&find[dateString][$lte]=2014-07-20') .expect(200) - .end(function (err, res) { - // console.log('body', res.body); - res.body.length.should.equal(10); + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(defaultCount); done( ); }); }); - it('/entries.json', function (done) { + it('/echo/ api shows query', function (done) { request(this.app) - .get('/entries.json?count=30') + .get('/echo/entries/sgv.json?find[dateString][$gte]=2014-07-19&find[dateString][$lte]=2014-07-20') + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Object); + res.body.query.should.be.instanceof(Object); + res.body.input.should.be.instanceof(Object); + res.body.input.find.should.be.instanceof(Object); + res.body.storage.should.equal('entries'); + done( ); + }); + }); + + it('/slice/ can slice time', function (done) { + var app = this.app; + request(app) + .get('/slice/entries/dateString/sgv/2014-07.json?count=20') + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(20); + done( ); + }); + }); + + + it('/times/echo can describe query', function (done) { + var app = this.app; + request(app) + .get('/times/echo/2014-07/.*T{00..05}:.json?count=20&find[sgv][$gte]=160') .expect(200) - .end(function (err, res) { - // console.log('body', res.body); - res.body.length.should.equal(30); + .end(function (err, res) { + res.body.should.be.instanceof(Object); + res.body.req.should.have.property('query'); + res.body.should.have.property('pattern').with.lengthOf(6); + done( ); + }); + }); + + it('/slice/ can slice with multiple prefix', function (done) { + var app = this.app; + request(app) + .get('/slice/entries/dateString/sgv/2014-07-{17..20}.json?count=20') + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(20); + done( ); + }); + }); + + it('/slice/ can slice time with prefix and no results', function (done) { + var app = this.app; + request(app) + .get('/slice/entries/dateString/sgv/1999-07.json?count=20&find[sgv][$lte]=401') + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(0); + done( ); + }); + }); + + it('/times/ can get modal times', function (done) { + var app = this.app; + request(app) + .get('/times/2014-07-/{0..30}T.json?') + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(10); + done( ); + }); + }); + + it('/times/ can get modal minutes and times', function (done) { + var app = this.app; + request(app) + .get('/times/20{14..15}-07/T{09..10}.json?') + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(10); + done( ); + }); + }); + it('/times/ can get multiple prefixen and modal minutes and times', function (done) { + var app = this.app; + request(app) + .get('/times/20{14..15}/T.*:{00..60}.json?') + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(10); done( ); }); }); @@ -52,29 +163,69 @@ describe('Entries REST api', function ( ) { request(this.app) .get('/entries/current.json') .expect(200) - .end(function (err, res) { - res.body.length.should.equal(1); - done( ); - // console.log('err', err, 'res', res); + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(1); + res.body[0].sgv.should.equal(100); + done(); + }); + }); + + it('/entries/:id', function (done) { + var app = this.app; + this.archive.list({count: 1}, function(err, records) { + var currentId = records.pop()._id.toString(); + request(app) + .get('/entries/'+currentId+'.json') + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(1); + res.body[0]._id.should.equal(currentId); + done( ); + }); }); + }); + it('/entries/:model', function (done) { + var app = this.app; + request(app) + .get('/entries/sgv/.json?count=10&find[dateString][$gte]=2014') + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(10); + done( ); + }); }); it('/entries/preview', function (done) { + request(this.app) + .post('/entries/preview.json') + .send(load('json')) + .expect(201) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(30); + done(); + }); + }); - request(this.app) - .post('/entries/preview.json') - .send(load('json')) - .expect(201) - .end(function (err, res) { - // console.log(res.body); - res.body.length.should.equal(30); - done( ); - // console.log('err', err, 'res', res); - }) - ; + it('disallow deletes unauthorized', function (done) { + var app = this.app; + request(app) + .delete('/entries/sgv?find[dateString][$gte]=2014-07-19&find[dateString][$lte]=2014-07-20') + .expect(401) + .end(function (err) { + if (err) { + done(err); + } else { + request(app) + .get('/entries/sgv.json?find[dateString][$gte]=2014-07-19&find[dateString][$lte]=2014-07-20') + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(10); + done(); + }); + } + }); }); }); - diff --git a/tests/api.status.test.js b/tests/api.status.test.js new file mode 100644 index 00000000000..5044c2afdd2 --- /dev/null +++ b/tests/api.status.test.js @@ -0,0 +1,69 @@ +'use strict'; + +var request = require('supertest'); +require('should'); + +describe('Status REST api', function ( ) { + var api = require('../lib/api/'); + before(function (done) { + var env = require('../env')( ); + env.settings.enable = ['careportal', 'rawbg']; + env.api_secret = 'this is my long pass phrase'; + this.wares = require('../lib/middleware/')(env); + this.app = require('express')( ); + this.app.enable('api'); + var self = this; + require('../lib/bootevent')(env).boot(function booted (ctx) { + self.app.use('/api', api(env, ctx)); + done(); + }); + }); + + it('/status.json', function (done) { + request(this.app) + .get('/api/status.json') + .expect(200) + .end(function (err, res) { + res.body.apiEnabled.should.equal(true); + res.body.careportalEnabled.should.equal(true); + res.body.settings.enable.length.should.equal(2); + res.body.settings.enable.should.containEql('careportal'); + res.body.settings.enable.should.containEql('rawbg'); + done( ); + }); + }); + + it('/status.html', function (done) { + request(this.app) + .get('/api/status.html') + .end(function(err, res) { + res.type.should.equal('text/html'); + res.statusCode.should.equal(200); + done(); + }); + }); + + it('/status.js', function (done) { + request(this.app) + .get('/api/status.js') + .end(function(err, res) { + res.type.should.equal('application/javascript'); + res.statusCode.should.equal(200); + res.text.should.startWith('this.serverSettings ='); + done(); + }); + }); + + it('/status.png', function (done) { + request(this.app) + .get('/api/status.png') + .end(function(err, res) { + res.headers.location.should.equal('http://img.shields.io/badge/Nightscout-OK-green.png'); + res.statusCode.should.equal(302); + done(); + }); + }); + + +}); + diff --git a/tests/api.treatments.test.js b/tests/api.treatments.test.js new file mode 100644 index 00000000000..3f588172684 --- /dev/null +++ b/tests/api.treatments.test.js @@ -0,0 +1,71 @@ +'use strict'; + +var request = require('supertest'); +var should = require('should'); + +describe('Treatment API', function ( ) { + var self = this; + + var api = require('../lib/api/'); + before(function (done) { + process.env.API_SECRET = 'this is my long pass phrase'; + self.env = require('../env')(); + self.env.settings.enable = ['careportal']; + this.wares = require('../lib/middleware/')(self.env); + self.app = require('express')(); + self.app.enable('api'); + require('../lib/bootevent')(self.env).boot(function booted(ctx) { + self.ctx = ctx; + self.app.use('/api', api(self.env, ctx)); + done(); + }); + }); + + after(function () { + delete process.env.API_SECRET; + }); + + it('post a some treatments', function (done) { + self.ctx.bus.on('data-loaded', function dataWasLoaded ( ) { + self.ctx.data.treatments.length.should.equal(3); + self.ctx.data.treatments[0].mgdl.should.equal(100); + should.not.exist(self.ctx.data.treatments[0].eventTime); + should.not.exist(self.ctx.data.treatments[0].notes); + + should.not.exist(self.ctx.data.treatments[1].eventTime); + should.not.exist(self.ctx.data.treatments[1].glucose); + should.not.exist(self.ctx.data.treatments[1].glucoseType); + should.not.exist(self.ctx.data.treatments[1].units); + self.ctx.data.treatments[1].insulin.should.equal(2); + self.ctx.data.treatments[2].carbs.should.equal(30); + + done(); + }); + + self.ctx.treatments().remove({ }, function ( ) { + request(self.app) + .post('/api/treatments/') + .set('api-secret', self.env.api_secret || '') + .send({eventType: 'BG Check', glucose: 100, preBolus: '0', glucoseType: 'Finger', units: 'mg/dl', notes: ''}) + .expect(200) + .end(function (err) { + if (err) { + done(err); + } + }); + + request(self.app) + .post('/api/treatments/') + .set('api-secret', self.env.api_secret || '') + .send({eventType: 'Meal Bolus', carbs: '30', insulin: '2.00', preBolus: '15', glucoseType: 'Finger', units: 'mg/dl'}) + .expect(200) + .end(function (err) { + if (err) { + done(err); + } + }); + + }); + }); + +}); \ No newline at end of file diff --git a/tests/api.unauthorized.test.js b/tests/api.unauthorized.test.js new file mode 100644 index 00000000000..64e7d254bd0 --- /dev/null +++ b/tests/api.unauthorized.test.js @@ -0,0 +1,148 @@ +'use strict'; + +var request = require('supertest'); +var load = require('./fixtures/load'); +var should = require('should'); + +describe('authed REST api', function ( ) { + var entries = require('../lib/api/entries/'); + + before(function (done) { + var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; + delete process.env.API_SECRET; + process.env.API_SECRET = 'this is my long pass phrase'; + var env = require('../env')( ); + this.wares = require('../lib/middleware/')(env); + this.archive = null; + this.app = require('express')( ); + this.app.enable('api'); + var self = this; + self.known_key = known; + require('../lib/bootevent')(env).boot(function booted (ctx) { + self.app.use('/', entries(self.app, self.wares, ctx)); + self.archive = require('../lib/entries')(env, ctx); + + var creating = load('json'); + // creating.push({type: 'sgv', sgv: 100, date: Date.now()}); + self.archive.create(creating, done); + }); + }); + + beforeEach(function (done) { + var creating = load('json'); + creating.push({type: 'sgv', sgv: 100, date: Date.now()}); + this.archive.create(creating, done); + }); + + afterEach(function (done) { + this.archive( ).remove({ }, done); + }); + + after(function (done) { + this.archive( ).remove({ }, done); + }); + + it('disallow unauthorized POST', function (done) { + var app = this.app; + + var new_entry = {type: 'sgv', sgv: 100, date: Date.now() }; + new_entry.dateString = new Date(new_entry.date).toISOString( ); + request(app) + .post('/entries.json?') + .send([new_entry]) + .expect(401) + .end(function (err, res) { + res.body.status.should.equal(401); + res.body.message.should.equal('Unauthorized'); + should.exist(res.body.description); + done(err); + }); + }); + + it('allow authorized POST', function (done) { + var app = this.app; + var known_key = this.known_key; + + var new_entry = {type: 'sgv', sgv: 100, date: Date.now() }; + new_entry.dateString = new Date(new_entry.date).toISOString( ); + request(app) + .post('/entries.json?') + .set('api-secret', known_key) + .send([new_entry]) + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(1); + request(app) + .get('/slice/entries/dateString/sgv/' + new_entry.dateString.split('T')[0] + '.json') + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(1); + + if (err) { + done(err); + } else { + request(app) + .delete('/entries/sgv?find[dateString]=' + new_entry.dateString) + .set('api-secret', known_key) + .expect(200) + .end(function (err) { + done(err); + }); + } + }); + }); + }); + + it('disallow deletes unauthorized', function (done) { + var app = this.app; + + request(app) + .get('/entries.json?find[dateString][$gte]=2014-07-18') + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(10); + request(app) + .delete('/entries/sgv?find[dateString][$gte]=2014-07-18&find[dateString][$lte]=2014-07-20') + // .set('api-secret', 'missing') + .expect(401) + .end(function (err) { + if (err) { + done(err); + } else { + request(app) + .get('/entries/sgv.json?find[dateString][$gte]=2014-07-18&find[dateString][$lte]=2014-07-20') + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(10); + done(); + }); + } + }); + }); + }); + + it('allow deletes when authorized', function (done) { + var app = this.app; + + request(app) + .delete('/entries/sgv?find[dateString][$gte]=2014-07-18&find[dateString][$lte]=2014-07-20') + .set('api-secret', this.known_key) + .expect(200) + .end(function (err) { + if (err) { + done(err); + } else { + request(app) + .get('/entries/sgv.json?find[dateString][$gte]=2014-07-18&find[dateString][$lte]=2014-07-20') + .expect(200) + .end(function (err, res) { + res.body.should.be.instanceof(Array).and.have.lengthOf(0); + done(); + }); + } + }); + }); + + + +}); diff --git a/tests/api.verifyauth.test.js b/tests/api.verifyauth.test.js new file mode 100644 index 00000000000..1cb9b295cae --- /dev/null +++ b/tests/api.verifyauth.test.js @@ -0,0 +1,45 @@ +'use strict'; + +var request = require('supertest'); +require('should'); + +describe('Verifyauth REST api', function ( ) { + var self = this; + + var api = require('../lib/api/'); + before(function (done) { + self.env = require('../env')( ); + self.env.api_secret = 'this is my long pass phrase'; + this.wares = require('../lib/middleware/')(self.env); + self.app = require('express')( ); + self.app.enable('api'); + require('../lib/bootevent')(self.env).boot(function booted (ctx) { + self.app.use('/api', api(self.env, ctx)); + done(); + }); + }); + + it('/verifyauth should return UNAUTHORIZED', function (done) { + request(self.app) + .get('/api/verifyauth') + .expect(200) + .end(function(err, res) { + res.body.message.should.equal('UNAUTHORIZED'); + done(); + }); + }); + + it('/verifyauth should return OK', function (done) { + request(self.app) + .get('/api/verifyauth') + .set('api-secret', self.env.api_secret || '') + .expect(200) + .end(function(err, res) { + res.body.message.should.equal('OK'); + done(); + }); + }); + + +}); + diff --git a/tests/ar2.test.js b/tests/ar2.test.js new file mode 100644 index 00000000000..9d3fed4a397 --- /dev/null +++ b/tests/ar2.test.js @@ -0,0 +1,240 @@ +'use strict'; + +var should = require('should'); +var levels = require('../lib/levels'); + +var FIVE_MINS = 300000; +var SIX_MINS = 360000; + +describe('ar2', function ( ) { + + var ar2 = require('../lib/plugins/ar2')(); + var delta = require('../lib/plugins/delta')(); + + var env = require('../env')(); + var ctx = {}; + ctx.data = require('../lib/data')(env, ctx); + ctx.notifications = require('../lib/notifications')(env, ctx); + + var now = Date.now(); + var before = now - FIVE_MINS; + + function prepareSandbox(base) { + var sbx = base || require('../lib/sandbox')().serverInit(env, ctx); + ar2.setProperties(sbx); + delta.setProperties(sbx); + return sbx; + } + + function rawSandbox() { + var envRaw = require('../env')(); + envRaw.extendedSettings = {'ar2': {useRaw: true}}; + + var sbx = require('../lib/sandbox')().serverInit(envRaw, ctx).withExtendedSettings(ar2); + + sbx.offerProperty('rawbg', function setFakeRawBG() { + return {}; + }); + + return prepareSandbox(sbx); + } + + it('should plot a cone', function () { + ctx.data.sgvs = [{mgdl: 100, mills: before}, {mgdl: 105, mills: now}]; + var sbx = prepareSandbox(); + var cone = ar2.forecastCone(sbx); + cone.length.should.equal(26); + }); + + it('should plot a line if coneFactor is 0', function () { + ctx.data.sgvs = [{mgdl: 100, mills: before}, {mgdl: 105, mills: now}]; + + var env0 = require('../env')(); + env0.extendedSettings = { ar2: { coneFactor: 0 } }; + var sbx = require('../lib/sandbox')().serverInit(env0, ctx).withExtendedSettings(ar2); + + var cone = ar2.forecastCone(sbx); + cone.length.should.equal(13); + }); + + + it('Not trigger an alarm when in range', function (done) { + ctx.notifications.initRequests(); + ctx.data.sgvs = [{mgdl: 100, mills: before}, {mgdl: 105, mills: now}]; + + var sbx = prepareSandbox(); + ar2.checkNotifications(sbx); + should.not.exist(ctx.notifications.findHighestAlarm()); + + done(); + }); + + it('should trigger a warning when going above target', function (done) { + ctx.notifications.initRequests(); + ctx.data.sgvs = [{mgdl: 150, mills: before}, {mgdl: 170, mills: now}]; + + var sbx = prepareSandbox(); + sbx.offerProperty('iob', function setFakeIOB() { + return {displayLine: 'IOB: 1.25U'}; + }); + sbx.offerProperty('direction', function setFakeDirection() { + return {value: 'FortyFiveUp', label: '↗', entity: '↗'}; + }); + ar2.checkNotifications(sbx); + var highest = ctx.notifications.findHighestAlarm(); + highest.level.should.equal(levels.WARN); + highest.title.should.equal('Warning, HIGH predicted'); + highest.message.should.equal('BG Now: 170 +20 ↗ mg/dl\nBG 15m: 206 mg/dl\nIOB: 1.25U'); + + done(); + }); + + it('should trigger a urgent alarm when going high fast', function (done) { + ctx.notifications.initRequests(); + ctx.data.sgvs = [{mgdl: 140, mills: before}, {mgdl: 200, mills: now}]; + + var sbx = prepareSandbox(); + ar2.checkNotifications(sbx); + var highest = ctx.notifications.findHighestAlarm(); + highest.level.should.equal(levels.URGENT); + highest.title.should.equal('Urgent, HIGH'); + + done(); + }); + + it('should trigger a warning when below target', function (done) { + ctx.notifications.initRequests(); + ctx.data.sgvs = [{mgdl: 90, mills: before}, {mgdl: 80, mills: now}]; + + var sbx = prepareSandbox(); + ar2.checkNotifications(sbx); + var highest = ctx.notifications.findHighestAlarm(); + highest.level.should.equal(levels.WARN); + highest.title.should.equal('Warning, LOW'); + + done(); + }); + + it('should trigger a warning when almost below target', function (done) { + ctx.notifications.initRequests(); + ctx.data.sgvs = [{mgdl: 90, mills: before}, {mgdl: 83, mills: now}]; + + var sbx = prepareSandbox(); + ar2.checkNotifications(sbx); + var highest = ctx.notifications.findHighestAlarm(); + highest.level.should.equal(levels.WARN); + highest.title.should.equal('Warning, LOW predicted'); + + done(); + }); + + it('should trigger a urgent alarm when falling fast', function (done) { + ctx.notifications.initRequests(); + ctx.data.sgvs = [{mgdl: 120, mills: before}, {mgdl: 85, mills: now}]; + + var sbx = prepareSandbox(); + ar2.checkNotifications(sbx); + var highest = ctx.notifications.findHighestAlarm(); + highest.level.should.equal(levels.URGENT); + highest.title.should.equal('Urgent, LOW predicted'); + + done(); + }); + + it('should trigger a warning alarm by interpolating when more than 5mins apart', function (done) { + ctx.notifications.initRequests(); + + //same as previous test but prev is 10 mins ago, so delta isn't enough to trigger an urgent alarm + ctx.data.sgvs = [{mgdl: 120, mills: before - SIX_MINS}, {mgdl: 85, mills: now}]; + + var sbx = prepareSandbox(); + ar2.checkNotifications(sbx); + var highest = ctx.notifications.findHighestAlarm(); + highest.level.should.equal(levels.WARN); + highest.title.should.equal('Warning, LOW predicted'); + + done(); + }); + + it('should include current raw bg and raw bg forecast when predicting w/raw', function (done) { + ctx.notifications.initRequests(); + ctx.data.sgvs = [{unfiltered: 113680, filtered: 111232, mgdl: 100, mills: before, noise: 1}, {unfiltered: 183680, filtered: 111232, mgdl: 100, mills: now, noise: 1}]; + ctx.data.cals = [{scale: 1, intercept: 25717.82377004309, slope: 766.895601715918, mills: now}]; + + var envRaw = require('../env')(); + envRaw.extendedSettings = {'ar2': {useRaw: true}}; + + var sbx = require('../lib/sandbox')().serverInit(envRaw, ctx).withExtendedSettings(ar2); + + sbx.offerProperty('rawbg', function setFakeIOB() { + return {displayLine: 'Raw BG: 200 mg/dl Clean'}; + }); + sbx.offerProperty('iob', function setFakeIOB() { + return {displayLine: 'IOB: 1.25U'}; + }); + sbx.offerProperty('direction', function setFakeDirection() { + return {value: 'FortyFiveUp', label: '↗', entity: '↗'}; + }); + + sbx = prepareSandbox(sbx); + + ar2.checkNotifications(sbx.withExtendedSettings(ar2)); + + var highest = ctx.notifications.findHighestAlarm(); + highest.level.should.equal(levels.WARN); + highest.title.should.equal('Warning, HIGH predicted w/raw'); + highest.message.should.equal('BG Now: 100 +0 ↗ mg/dl\nRaw BG: 200 mg/dl Clean\nRaw BG 15m: 400 mg/dl\nIOB: 1.25U'); + + done(); + }); + + it('should not trigger an alarm when raw is missing or 0', function (done) { + ctx.notifications.initRequests(); + ctx.data.sgvs = [{unfiltered: 0, filtered: 0, mgdl: 100, mills: before, noise: 1}, {unfiltered: 0, filtered: 0, mgdl: 100, mills: now, noise: 1}]; + ctx.data.cals = [{scale: 1, intercept: 25717.82377004309, slope: 766.895601715918, mills: now}]; + + var sbx = rawSandbox(); + ar2.checkNotifications(sbx.withExtendedSettings(ar2)); + should.not.exist(ctx.notifications.findHighestAlarm()); + + done(); + }); + + + it('should trigger a warning (no urgent for raw) when raw is falling really fast, but sgv is steady', function (done) { + ctx.notifications.initRequests(); + ctx.data.sgvs = [{unfiltered: 113680, filtered: 111232, mgdl: 100, mills: before, noise: 1}, {unfiltered: 43680, filtered: 111232, mgdl: 100, mills: now, noise: 1}]; + ctx.data.cals = [{scale: 1, intercept: 25717.82377004309, slope: 766.895601715918, mills: now}]; + + var sbx = rawSandbox(); + sbx.offerProperty('rawbg', function setFakeIOB() { + return {}; + }); + + ar2.checkNotifications(sbx.withExtendedSettings(ar2)); + var highest = ctx.notifications.findHighestAlarm(); + highest.level.should.equal(levels.WARN); + highest.title.should.equal('Warning, LOW predicted w/raw'); + + done(); + }); + + it('should trigger a warning (no urgent for raw) when raw is rising really fast, but sgv is steady', function (done) { + ctx.notifications.initRequests(); + ctx.data.sgvs = [{unfiltered: 113680, filtered: 111232, mgdl: 100, mills: before, noise: 1}, {unfiltered: 183680, filtered: 111232, mgdl: 100, mills: now, noise: 1}]; + ctx.data.cals = [{scale: 1, intercept: 25717.82377004309, slope: 766.895601715918, mills: now}]; + + var sbx = rawSandbox(); + sbx.offerProperty('rawbg', function setFakeIOB() { + return {}; + }); + + ar2.checkNotifications(sbx.withExtendedSettings(ar2)); + var highest = ctx.notifications.findHighestAlarm(); + highest.level.should.equal(levels.WARN); + highest.title.should.equal('Warning, HIGH predicted w/raw'); + + done(); + }); + +}); \ No newline at end of file diff --git a/tests/basalprofileplugin.test.js b/tests/basalprofileplugin.test.js new file mode 100644 index 00000000000..2d1b354350b --- /dev/null +++ b/tests/basalprofileplugin.test.js @@ -0,0 +1,73 @@ +require('should'); + +describe('basalprofile', function ( ) { + + var basal = require('../lib/plugins/basalprofile')(); + + var sandbox = require('../lib/sandbox')(); + var env = require('../env')(); + var ctx = {}; + ctx.data = require('../lib/data')(env, ctx); + ctx.notifications = require('../lib/notifications')(env, ctx); + + var profileData = + { + 'timezone': 'UTC', + 'startDate': '2015-06-21', + 'basal': [ + { + 'time': '00:00', + 'value': 0.175 + }, + { + 'time': '02:30', + 'value': 0.125 + }, + { + 'time': '05:00', + 'value': 0.075 + }, + { + 'time': '08:00', + 'value': 0.1 + }, + { + 'time': '14:00', + 'value': 0.125 + }, + { + 'time': '20:00', + 'value': 0.3 + }, + { + 'time': '22:00', + 'value': 0.225 + } + ] + }; + + + var profile = require('../lib/profilefunctions')([profileData]); + + it('update basal profile pill', function (done) { + + var clientSettings = {}; + var data = {}; + + var pluginBase = { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.value.should.equal('0.175U'); + done(); + } + }; + + var time = new Date('2015-06-21T00:00:00').getTime(); + + var sbx = sandbox.clientInit(clientSettings, time, pluginBase, data); + sbx.data.profile = profile; + basal.updateVisualisation(sbx); + + }); + + +}); \ No newline at end of file diff --git a/tests/boluswizardpreview.test.js b/tests/boluswizardpreview.test.js new file mode 100644 index 00000000000..3954342e1c1 --- /dev/null +++ b/tests/boluswizardpreview.test.js @@ -0,0 +1,291 @@ +var should = require('should'); +var Stream = require('stream'); +var levels = require('../lib/levels'); + +describe('boluswizardpreview', function ( ) { + + var boluswizardpreview = require('../lib/plugins/boluswizardpreview')(); + var ar2 = require('../lib/plugins/ar2')(); + var iob = require('../lib/plugins/iob')(); + var delta = require('../lib/plugins/delta')(); + + var env = require('../env')(); + env.testMode = true; + var ctx = {}; + ctx.data = require('../lib/data')(env, ctx); + ctx.notifications = require('../lib/notifications')(env, ctx); + + function prepareSandbox ( ) { + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + iob.setProperties(sbx); + ar2.setProperties(sbx); + delta.setProperties(sbx); + boluswizardpreview.setProperties(sbx); + sbx.offerProperty('direction', function setFakeDirection() { + return {value: 'FortyFiveUp', label: '↗', entity: '↗'}; + }); + + return sbx; + } + + var now = Date.now(); + var before = now - (5 * 60 * 1000); + + var profile = { + dia: 3 + , sens: 90 + , target_high: 120 + , target_low: 100 + }; + + it('should calculate IOB results correctly with 0 IOB', function (done) { + ctx.notifications.initRequests(); + ctx.data.sgvs = [{mills: before, mgdl: 100}, {mills: now, mgdl: 100}]; + ctx.data.treatments = []; + ctx.data.profiles = [profile]; + + var sbx = prepareSandbox(); + var results = boluswizardpreview.calc(sbx); + + results.effect.should.equal(0); + results.effectDisplay.should.equal(0); + results.outcome.should.equal(100); + results.outcomeDisplay.should.equal(100); + results.bolusEstimate.should.equal(0); + results.displayLine.should.equal('BWP: 0U'); + + done(); + }); + + it('should calculate IOB results correctly with 1.0 U IOB', function (done) { + ctx.notifications.initRequests(); + ctx.data.sgvs = [{mills: before, mgdl: 100}, {mills: now, mgdl: 100}]; + ctx.data.treatments = [{mills: now, insulin: '1.0'}]; + + var profile = { + dia: 3 + , sens: 50 + , target_high: 100 + , target_low: 50 + }; + + ctx.data.profiles = [profile]; + + var sbx = prepareSandbox(); + var results = boluswizardpreview.calc(sbx); + + Math.round(results.effect).should.equal(50); + results.effectDisplay.should.equal(50); + Math.round(results.outcome).should.equal(50); + results.outcomeDisplay.should.equal(50); + results.bolusEstimate.should.equal(0); + results.displayLine.should.equal('BWP: 0U'); + + done(); + }); + + it('should calculate IOB results correctly with 1.0 U IOB resulting in going low', function (done) { + ctx.notifications.initRequests(); + ctx.data.sgvs = [{mills: before, mgdl: 100}, {mills: now, mgdl: 100}]; + ctx.data.treatments = [{mills: now, insulin: '1.0'}]; + + var profile = { + dia: 3 + , sens: 50 + , target_high: 200 + , target_low: 100 + , basal: 1 + }; + + + ctx.data.profiles = [profile]; + + var sbx = prepareSandbox(); + var results = boluswizardpreview.calc(sbx); + + Math.round(results.effect).should.equal(50); + results.effectDisplay.should.equal(50); + Math.round(results.outcome).should.equal(50); + results.outcomeDisplay.should.equal(50); + Math.round(results.bolusEstimate).should.equal(-1); + results.displayLine.should.equal('BWP: -1.00U'); + results.tempBasalAdjustment.thirtymin.should.equal(-100); + results.tempBasalAdjustment.onehour.should.equal(0); + + done(); + }); + + it('should calculate IOB results correctly with 1.0 U IOB resulting in going low in MMOL', function (done) { + + // boilerplate for client sandbox running in mmol + + var profileData = { + dia: 3 + , units: 'mmol' + , sens: 10 + , target_high: 10 + , target_low: 5.6 + , basal: 1 + }; + + var sandbox = require('../lib/sandbox')(); + var pluginBase = {}; + var clientSettings = { units: 'mmol' }; + var data = {sgvs: [{mills: before, mgdl: 100}, {mills: now, mgdl: 100}]}; + data.treatments = [{mills: now, insulin: '1.0'}]; + data.profile = require('../lib/profilefunctions')([profileData]); + var sbx = sandbox.clientInit(clientSettings, Date.now(), pluginBase, data); + var iob = require('../lib/plugins/iob')(); + sbx.properties.iob = iob.calcTotal(data.treatments, data.profile, now); + + var results = boluswizardpreview.calc(sbx); + + results.effect.should.equal(10); + results.outcome.should.equal(-4.4); + results.bolusEstimate.should.equal(-1); + results.displayLine.should.equal('BWP: -1.00U'); + results.tempBasalAdjustment.thirtymin.should.equal(-100); + results.tempBasalAdjustment.onehour.should.equal(0); + + done(); + }); + + + it('should calculate IOB results correctly with 0.45 U IOB resulting in going low in MMOL', function (done) { + + // boilerplate for client sandbox running in mmol + + var profileData = { + dia: 3 + , units: 'mmol' + , sens: 9 + , target_high: 6 + , target_low: 5 + , basal: 0.125 + }; + + var sandbox = require('../lib/sandbox')(); + var pluginBase = {}; + var clientSettings = { units: 'mmol' }; + var data = {sgvs: [{mills: before, mgdl: 175}, {mills: now, mgdl: 153}]}; + data.treatments = [{mills: now, insulin: '0.45'}]; + data.profile = require('../lib/profilefunctions')([profileData]); + var sbx = sandbox.clientInit(clientSettings, Date.now(), pluginBase, data); + var iob = require('../lib/plugins/iob')(); + sbx.properties.iob = iob.calcTotal(data.treatments, data.profile, now); + + var results = boluswizardpreview.calc(sbx); + + results.effect.should.equal(4.05); + results.outcome.should.equal(4.45); + Math.round(results.bolusEstimate*100).should.equal(-6); + results.displayLine.should.equal('BWP: -0.07U'); + results.tempBasalAdjustment.thirtymin.should.equal(2); + results.tempBasalAdjustment.onehour.should.equal(51); + + done(); + }); + + + it('Not trigger an alarm when in range', function (done) { + ctx.notifications.initRequests(); + ctx.data.sgvs = [{mills: before, mgdl: 95}, {mills: now, mgdl: 100}]; + ctx.data.treatments = []; + ctx.data.profiles = [profile]; + + var sbx = prepareSandbox(); + boluswizardpreview.checkNotifications(sbx); + + should.not.exist(ctx.notifications.findHighestAlarm()); + + done(); + }); + + it('trigger a warning when going out of range', function (done) { + ctx.notifications.initRequests(); + ctx.data.sgvs = [{mills: before, mgdl: 175}, {mills: now, mgdl: 180}]; + ctx.data.treatments = []; + ctx.data.profiles = [profile]; + + var sbx = prepareSandbox(); + boluswizardpreview.checkNotifications(sbx); + + var highest = ctx.notifications.findHighestAlarm(); + highest.level.should.equal(levels.WARN); + highest.title.should.equal('Warning, Check BG, time to bolus?'); + highest.message.should.equal('BG Now: 180 +5 ↗ mg/dl\nBG 15m: 187 mg/dl\nBWP: 0.66U\nIOB: 0U'); + done(); + }); + + it('trigger an urgent alarms when going too high', function (done) { + ctx.notifications.initRequests(); + ctx.data.sgvs = [{mills: before, mgdl: 295}, {mills: now, mgdl: 300}]; + ctx.data.treatments = []; + ctx.data.profiles = [profile]; + + var sbx = prepareSandbox(); + boluswizardpreview.checkNotifications(sbx); + ctx.notifications.findHighestAlarm().level.should.equal(levels.URGENT); + + done(); + }); + + it('request a snooze when there is enough IOB', function (done) { + + ctx.notifications.resetStateForTests(); + ctx.notifications.initRequests(); + ctx.data.sgvs = [{mills: before, mgdl: 295}, {mills: now, mgdl: 300}]; + ctx.data.treatments = [{mills: before, insulin: '5.0'}]; + ctx.data.profiles = [profile]; + + var sbx = prepareSandbox(); + + //start fresh to we don't pick up other notifications + ctx.bus = new Stream; + //if notification doesn't get called test will time out + ctx.bus.on('notification', function callback (notify) { + notify.clear.should.equal(true); + if (notify.clear) { + done(); + } + }); + + ar2.checkNotifications(sbx); + boluswizardpreview.checkNotifications(sbx); + ctx.notifications.process(); + + }); + + it('set a pill to the BWP with infos', function (done) { + var pluginBase = { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.label.should.equal('BWP'); + options.value.should.equal('0.50U'); + done(); + } + }; + + var clientSettings = {}; + + var loadedProfile = require('../lib/profilefunctions')(); + loadedProfile.loadData([profile]); + + var data = { + sgvs: [{mills: before, mgdl: 295}, {mills: now, mgdl: 300}] + , treatments: [{mills: before, insulin: '1.5'}] + , profile: loadedProfile + }; + + var sbx = require('../lib/sandbox')().clientInit(clientSettings, Date.now(), pluginBase, data); + + iob.setProperties(sbx); + boluswizardpreview.setProperties(sbx); + boluswizardpreview.updateVisualisation(sbx); + + ctx.notifications.resetStateForTests(); + ctx.notifications.initRequests(); + ctx.data.profiles = [profile]; + + }); + +}); \ No newline at end of file diff --git a/tests/bridge.test.js b/tests/bridge.test.js new file mode 100644 index 00000000000..99c1587fab4 --- /dev/null +++ b/tests/bridge.test.js @@ -0,0 +1,42 @@ +'use strict'; + +var should = require('should'); + +describe('bridge', function ( ) { + var bridge = require('../lib/plugins/bridge'); + + var env = { + extendedSettings: { + bridge: { + userName: 'nightscout' + , password: 'wearenotwaiting' + } + } + }; + + it('be creatable', function () { + var configed = bridge(env); + should.exist(configed); + should.exist(configed.startEngine); + should.exist(configed.startEngine.call); + }); + + it('set options from env', function () { + var opts = bridge.options(env); + should.exist(opts); + + opts.login.accountName.should.equal('nightscout'); + opts.login.password.should.equal('wearenotwaiting'); + }); + + it('store entries from share', function (done) { + var mockEntries = { + create: function mockCreate (err, callback) { + callback(null); + done(); + } + }; + bridge.bridged(mockEntries)(null); + }); + +}); diff --git a/tests/cannulaage.test.js b/tests/cannulaage.test.js new file mode 100644 index 00000000000..60ecdfd4281 --- /dev/null +++ b/tests/cannulaage.test.js @@ -0,0 +1,88 @@ +'use strict'; + +require('should'); +var levels = require('../lib/levels'); + +describe('cage', function ( ) { + var cage = require('../lib/plugins/cannulaage')(); + var sandbox = require('../lib/sandbox')(); + var env = require('../env')(); + var ctx = {}; + ctx.data = require('../lib/data')(env, ctx); + ctx.notifications = require('../lib/notifications')(env, ctx); + + function prepareSandbox ( ) { + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + sbx.offerProperty('iob', function () { + return {iob: 0}; + }); + return sbx; + } + + it('set a pill to the current cannula age', function (done) { + + var clientSettings = {}; + + var data = { + treatments: [ + {eventType: 'Site Change', notes: 'Foo', mills: Date.now() - 48 * 60 * 60000} + , {eventType: 'Site Change', notes: 'Bar', mills: Date.now() - 24 * 60 * 60000} + ] + }; + + var pluginBase = { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.value.should.equal('24h'); + options.info[1].value.should.equal('Bar'); + done(); + } + }; + + var sbx = sandbox.clientInit(clientSettings, Date.now(), pluginBase, data); + cage.updateVisualisation(sbx); + + }); + + it('set a pill to the current cannula age', function (done) { + + var clientSettings = {}; + + var data = { + treatments: [ + {eventType: 'Site Change', notes: 'Foo', mills: Date.now() - 48 * 60 * 60000} + , {eventType: 'Site Change', notes: '', mills: Date.now() - 59 * 60000} + ] + }; + + var pluginBase = { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.value.should.equal('0h'); + options.info.length.should.equal(1); + done(); + } + }; + + var sbx = sandbox.clientInit(clientSettings, Date.now(), pluginBase, data); + cage.updateVisualisation(sbx); + + }); + + + it('trigger a warning when cannula is 48 hours old', function (done) { + ctx.notifications.initRequests(); + + var before = Date.now() - (48 * 60 * 60 * 1000); + + ctx.data.treatments = [{eventType: 'Site Change', mills: before}]; + + var sbx = prepareSandbox(); + sbx.extendedSettings = { 'enableAlerts': 'TRUE' }; + cage.checkNotifications(sbx); + + var highest = ctx.notifications.findHighestAlarm(); + highest.level.should.equal(levels.WARN); + highest.title.should.equal('Cannula age 48 hours'); + done(); + }); + +}); diff --git a/tests/careportal.test.js b/tests/careportal.test.js new file mode 100644 index 00000000000..11821fd3ffe --- /dev/null +++ b/tests/careportal.test.js @@ -0,0 +1,114 @@ +'use strict'; + +require('should'); +var benv = require('benv'); +var read = require('fs').readFileSync; +var serverSettings = require('./fixtures/default-server-settings'); + +var nowData = { + sgvs: [ + { mgdl: 100, mills: Date.now(), direction: 'Flat', type: 'sgv' } + ] +}; + +describe('client', function ( ) { + var self = this; + + before(function (done) { + benv.setup(function() { + self.$ = require('jquery'); + self.$.localStorage = require('./fixtures/localstorage'); + + self.$.fn.tipsy = function mockTipsy ( ) { }; + + var indexHtml = read(__dirname + '/../static/index.html', 'utf8'); + self.$('body').html(indexHtml); + + var d3 = require('d3'); + //disable all d3 transitions so most of the other code can run with jsdom + d3.timer = function mockTimer() { }; + + benv.expose({ + $: self.$ + , jQuery: self.$ + , d3: d3 + , io: { + connect: function mockConnect ( ) { + return { + on: function mockOn ( ) { } + }; + } + } + }); + done(); + }); + }); + + after(function (done) { + benv.teardown(); + done(); + }); + it ('open careportal, and enter a treatment', function (done) { + var plugins = require('../lib/plugins/')().registerClientDefaults(); + var client = require('../lib/client'); + + self.$.ajax = function mockAjax ( ) { + return { + done: function mockDone (fn) { + fn(); + done(); + return self.$.ajax(); + } + , fail: function mockFail ( ) { + return self.$.ajax(); + } + }; + }; + + var hashauth = require('../lib/hashauth'); + hashauth.init(client,$); + hashauth.verifyAuthentication = function mockVerifyAuthentication(next) { + hashauth.authenticated = true; + next(true); + }; + + + client.init(serverSettings, plugins); + client.dataUpdate(nowData); + + client.careportal.prepareEvents(); + client.careportal.toggleDrawer(); + + $('#eventType').val('Snack Bolus'); + $('#glucoseValue').val('100'); + $('#carbsGiven').val('10'); + $('#insulinGiven').val('0.60'); + $('#preBolus').val(15); + $('#notes').val('Testing'); + $('#enteredBy').val('Dad'); + + //simulate some events + client.careportal.eventTimeTypeChange(); + client.careportal.dateTimeFocus(); + client.careportal.dateTimeChange(); + + window.confirm = function mockConfirm (message) { + function containsLine (line) { + message.indexOf(line + '\n').should.be.greaterThan(0); + } + + containsLine('Event Type: Snack Bolus'); + containsLine('Blood Glucose: 100'); + containsLine('Carbs Given: 10'); + containsLine('Insulin Given: 0.60'); + containsLine('Carb Time: 15 mins'); + containsLine('Notes: Testing'); + containsLine('Entered By: Dad'); + + return true; + }; + + client.careportal.save(); + }); + +}); diff --git a/tests/client.test.js b/tests/client.test.js new file mode 100644 index 00000000000..18ad0f6ee1b --- /dev/null +++ b/tests/client.test.js @@ -0,0 +1,137 @@ +'use strict'; + +require('should'); +var times = require('../lib/times'); +var benv = require('benv'); +var read = require('fs').readFileSync; +var serverSettings = require('./fixtures/default-server-settings'); + +var TEST_TITLE = 'Test Title'; + +var stored = { }; +var removed = { }; + +var now = Date.now(); +var next = Date.now() + times.mins(5).msecs; + +var nowData = { + sgvs: [ + { device: 'dexcom', mgdl: 100, mills: now, direction: 'Flat', type: 'sgv', filtered: 113984, unfiltered: 111920, rssi: 179, noise: 1 } + ], mbgs: [ + { mgdl: 100, mills: now } + ], cals: [ + { + device: 'dexcom' + , slope: 895.8571693029189 + , intercept: 34281.06876195567 + , scale: 1 + , type: 'cal' + } + ], devicestatus: { uploaderBattery: 100 } + , treatments: [ + { + insulin: '1.00' + , carbs: '18' + , mills: now + } + ] +}; + +var nextData = { + sgvs: [ + { device: 'dexcom', mgdl: 101, mills: next, direction: 'Flat', type: 'sgv', filtered: 113984, unfiltered: 111920, rssi: 179, noise: 1 } + ], mbgs: [ ] + , cals: [] + , devicestatus: { uploaderBattery: 100 } + , treatments: [] +}; + +describe('client', function ( ) { + var self = this; + before(function (done) { + benv.setup(function() { + self.$ = require('jquery'); + self.$.localStorage = { + get: function mockGet (name) { + return name === 'alarmTimeagoWarnMins' ? 99 : undefined; + } + , set: function mockSet (name, value) { + stored[name] = value; + } + , remove: function mockRemove (name) { + removed[name] = true; + } + }; + + self.$.fn.tipsy = function mockTipsy ( ) { }; + + var indexHtml = read(__dirname + '/../static/index.html', 'utf8'); + self.$('body').html(indexHtml); + + var d3 = require('d3'); + //disable all d3 transitions so most of the other code can run with jsdom + d3.timer = function mockTimer() { }; + + benv.expose({ + $: self.$ + , jQuery: self.$ + , d3: d3 + , io: { + connect: function mockConnect ( ) { + return { + on: function mockOn ( ) { } + }; + } + } + }); + done(); + }); + }); + + after(function (done) { + benv.teardown(); + done(); + }); + + it ('not blow up with mg/dl', function () { + var plugins = require('../lib/plugins/')().registerClientDefaults(); + var client = require('../lib/client'); + client.init(serverSettings, plugins); + client.dataUpdate(nowData); + }); + + it ('handle 2 updates', function () { + var plugins = require('../lib/plugins/')().registerClientDefaults(); + var client = require('../lib/client'); + client.init(serverSettings, plugins); + client.dataUpdate(nowData); + client.dataUpdate(nextData); + }); + + it ('not blow up with mmol', function () { + serverSettings.settings.units = 'mmol'; + serverSettings.settings.timeFormat = 24; + + var plugins = require('../lib/plugins/')().registerClientDefaults(); + var client = require('../lib/client'); + client.init(serverSettings, plugins); + client.dataUpdate(nowData); + }); + + it ('load, store, and clear settings', function () { + var plugins = require('../lib/plugins/')().registerClientDefaults(); + var client = require('../lib/client'); + client.init(serverSettings, plugins); + client.dataUpdate(nowData); + + var browserSettings = require('../lib/client/browser-settings')(client, plugins, serverSettings, self.$); + browserSettings.alarmTimeagoWarnMins.should.equal(99); + browserSettings.customTitle.should.equal(TEST_TITLE); + + self.$('#save').click(); + stored.customTitle.should.equal(TEST_TITLE); + self.$('#useDefaults').click(); + removed.customTitle.should.equal(true); + }); + +}); diff --git a/tests/cob.test.js b/tests/cob.test.js new file mode 100644 index 00000000000..336dffe41b3 --- /dev/null +++ b/tests/cob.test.js @@ -0,0 +1,93 @@ +'use strict'; + +require('should'); + +describe('COB', function ( ) { + var cob = require('../lib/plugins/cob')(); + + var profileData = { + sens: 95 + , carbratio: 18 + , carbs_hr: 30 + }; + + var profile = require('../lib/profilefunctions')([profileData]); + + it('should calculate IOB, multiple treatments', function() { + + var treatments = [ + { + 'carbs': '100', + 'mills': new Date('2015-05-29T02:03:48.827Z').getTime() + }, + { + 'carbs': '10', + 'mills': new Date('2015-05-29T03:45:10.670Z').getTime() + } + ]; + + var after100 = cob.cobTotal(treatments, profile, new Date('2015-05-29T02:03:49.827Z').getTime()); + var before10 = cob.cobTotal(treatments, profile, new Date('2015-05-29T03:45:10.670Z').getTime()); + var after10 = cob.cobTotal(treatments, profile, new Date('2015-05-29T03:45:11.670Z').getTime()); + + after100.cob.should.equal(100); + Math.round(before10.cob).should.equal(59); + Math.round(after10.cob).should.equal(69); //WTF == 128 + }); + + it('should calculate IOB, single treatment', function() { + + var treatments = [ + { + 'carbs': '8', + 'mills': new Date('2015-05-29T04:40:40.174Z').getTime() + } + ]; + + var rightAfterCorrection = new Date('2015-05-29T04:41:40.174Z').getTime(); + var later1 = new Date('2015-05-29T05:04:40.174Z').getTime(); + var later2 = new Date('2015-05-29T05:20:00.174Z').getTime(); + var later3 = new Date('2015-05-29T05:50:00.174Z').getTime(); + var later4 = new Date('2015-05-29T06:50:00.174Z').getTime(); + + var result1 = cob.cobTotal(treatments, profile, rightAfterCorrection); + var result2 = cob.cobTotal(treatments, profile, later1); + var result3 = cob.cobTotal(treatments, profile, later2); + var result4 = cob.cobTotal(treatments, profile, later3); + var result5 = cob.cobTotal(treatments, profile, later4); + + result1.cob.should.equal(8); + result2.cob.should.equal(6); + result3.cob.should.equal(0); + result4.cob.should.equal(0); + result5.cob.should.equal(0); + }); + + it('set a pill to the current COB', function (done) { + + var clientSettings = {}; + + var data = { + treatments: [{ + carbs: '8' + , 'mills': Date.now() - 60000 //1m ago + }] + , profile: profile + }; + + var pluginBase = { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.value.should.equal('8g'); + done(); + } + }; + + var sandbox = require('../lib/sandbox')(); + var sbx = sandbox.clientInit(clientSettings, Date.now(), pluginBase, data); + cob.setProperties(sbx); + cob.updateVisualisation(sbx); + + }); + + +}); \ No newline at end of file diff --git a/tests/data.test.js b/tests/data.test.js new file mode 100644 index 00000000000..60c74abe035 --- /dev/null +++ b/tests/data.test.js @@ -0,0 +1,86 @@ +'use strict'; + +require('should'); + +describe('Data', function ( ) { + + var env = require('../env')(); + var ctx = {}; + var data = require('../lib/data')(env, ctx); + + var now = Date.now(); + var before = now - (5 * 60 * 1000); + + + it('should return original data if there are no changes', function() { + data.sgvs = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + var delta = data.calculateDeltaBetweenDatasets(data,data); + delta.should.equal(data); + }); + + it('adding one sgv record should return delta with one sgv', function() { + data.sgvs = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + var newData = data.clone(); + newData.sgvs = [{mgdl: 100, mills:101},{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + var delta = data.calculateDeltaBetweenDatasets(data,newData); + delta.delta.should.equal(true); + delta.sgvs.length.should.equal(1); + }); + + it('adding one treatment record should return delta with one treatment', function() { + data.treatments = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + var newData = data.clone(); + newData.treatments = [{mgdl: 100, mills: before},{mgdl: 100, mills: now},{mgdl: 100, mills:98}]; + var delta = data.calculateDeltaBetweenDatasets(data,newData); + delta.delta.should.equal(true); + delta.treatments.length.should.equal(1); + }); + + it('changes to treatments, mbgs and cals should be calculated even if sgvs is not changed', function() { + data.sgvs = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + data.treatments = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + data.mbgs = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + data.cals = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + var newData = data.clone(); + newData.sgvs = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + newData.treatments = [{mgdl: 100, mills:101},{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + newData.mbgs = [{mgdl: 100, mills:101},{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + newData.cals = [{mgdl: 100, mills:101},{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + var delta = data.calculateDeltaBetweenDatasets(data,newData); + delta.delta.should.equal(true); + delta.treatments.length.should.equal(1); + delta.mbgs.length.should.equal(1); + delta.cals.length.should.equal(1); + }); + + it('delta should include profile and devicestatus object if changed', function() { + data.sgvs = [{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + data.profiles = {foo:true}; + data.devicestatus = {foo:true}; + var newData = data.clone(); + newData.sgvs = [{mgdl: 100, mills:101},{mgdl: 100, mills: before},{mgdl: 100, mills: now}]; + newData.profiles = {bar:true}; + newData.devicestatus = {bar:true}; + var delta = data.calculateDeltaBetweenDatasets(data,newData); + delta.profiles.bar.should.equal(true); + delta.devicestatus.bar.should.equal(true); + }); + + it('update treatment display BGs', function() { + data.sgvs = [{mgdl: 90, mills: before},{mgdl: 100, mills: now}]; + data.treatments = [ + {mills: before, glucose: 100, units: 'mgdl'} //with glucose and units + , {mills: before, glucose: 5.5, units: 'mmol'} //with glucose and units + , {mills: now - 120000, insulin: '1.00'} //without glucose, between sgvs + , {mills: now + 60000, insulin: '1.00'} //without glucose, after sgvs + , {mills: before - 120000, insulin: '1.00'} //without glucose, before sgvs + ]; + data.updateTreatmentDisplayBGs(); + data.treatments[0].mgdl.should.equal(100); + data.treatments[1].mmol.should.equal(5.5); + data.treatments[2].mgdl.should.equal(95); + data.treatments[3].mgdl.should.equal(100); + data.treatments[4].mgdl.should.equal(90); + }); + +}); \ No newline at end of file diff --git a/tests/delta.test.js b/tests/delta.test.js new file mode 100644 index 00000000000..cd250d499d5 --- /dev/null +++ b/tests/delta.test.js @@ -0,0 +1,132 @@ +'use strict'; + +require('should'); +var _ =require('lodash'); + +var FIVE_MINS = 300000; +var SIX_MINS = 360000; + +describe('Delta', function ( ) { + var delta = require('../lib/plugins/delta')(); + var sandbox = require('../lib/sandbox')(); + + var pluginBase = {}; + var now = Date.now(); + var before = now - FIVE_MINS; + + it('should calculate BG Delta', function (done) { + var clientSettings = { units: 'mg/dl' }; + var data = {sgvs: [{mills: before, mgdl: 100}, {mills: now, mgdl: 105}]}; + + var callbackPluginBase = { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.label.should.equal(clientSettings.units); + options.value.should.equal('+5'); + options.info.length.should.equal(0); + done(); + } + }; + + var sbx = sandbox.clientInit(clientSettings, Date.now(), callbackPluginBase, data); + + delta.setProperties(sbx); + + var prop = sbx.properties.delta; + prop.mgdl.should.equal(5); + prop.interpolated.should.equal(false); + prop.scaled.should.equal(5); + prop.display.should.equal('+5'); + + delta.updateVisualisation(sbx); + }); + + it('should calculate BG Delta by interpolating when more than 5mins apart', function (done) { + var clientSettings = { units: 'mg/dl' }; + var data = {sgvs: [{mills: before - SIX_MINS, mgdl: 100}, {mills: now, mgdl: 105}]}; + + var callbackPluginBase = { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.label.should.equal(clientSettings.units); + options.value.should.equal('+2 *'); + findInfoValue('Elapsed Time', options.info).should.equal('11 mins'); + findInfoValue('Absolute Delta', options.info).should.equal('5 mg/dl'); + findInfoValue('Interpolated', options.info).should.equal('103 mg/dl'); + done(); + } + }; + + var sbx = sandbox.clientInit(clientSettings, Date.now(), callbackPluginBase, data); + + delta.setProperties(sbx); + + var prop = sbx.properties.delta; + prop.mgdl.should.equal(2); + prop.interpolated.should.equal(true); + prop.scaled.should.equal(2); + prop.display.should.equal('+2'); + delta.updateVisualisation(sbx); + + }); + + it('should calculate BG Delta in mmol', function (done) { + var clientSettings = { units: 'mmol' }; + var data = {sgvs: [{mills: before, mgdl: 100}, {mills: now, mgdl: 105}]}; + var sbx = sandbox.clientInit(clientSettings, Date.now(), pluginBase, data); + + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('delta'); + var result = setter(); + result.mgdl.should.equal(5); + result.interpolated.should.equal(false); + result.scaled.should.equal(0.2); + result.display.should.equal('+0.2'); + done(); + }; + + delta.setProperties(sbx); + }); + + it('should calculate BG Delta in mmol and not show a change because of rounding', function (done) { + var clientSettings = { units: 'mmol' }; + var data = {sgvs: [{mills: before, mgdl: 85}, {mills: now, mgdl: 85}]}; + var sbx = sandbox.clientInit(clientSettings, Date.now(), pluginBase, data); + + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('delta'); + var result = setter(); + result.mgdl.should.equal(0); + result.interpolated.should.equal(false); + result.scaled.should.equal(0); + result.display.should.equal('+0'); + done(); + }; + + delta.setProperties(sbx); + }); + + it('should calculate BG Delta in mmol by interpolating when more than 5mins apart', function (done) { + var clientSettings = { units: 'mmol' }; + var data = {sgvs: [{mills: before - SIX_MINS, mgdl: 100}, {mills: now, mgdl: 105}]}; + var sbx = sandbox.clientInit(clientSettings, Date.now(), pluginBase, data); + + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('delta'); + var result = setter(); + result.mgdl.should.equal(2); + result.interpolated.should.equal(true); + result.scaled.should.equal(0.1); + result.display.should.equal('+0.1'); + done(); + }; + + delta.setProperties(sbx); + }); + +}); + +function findInfoValue (label, info) { + var found = _.find(info, function checkLine (line) { + return line.label === label; + }); + return found && found.value; +} diff --git a/tests/direction.test.js b/tests/direction.test.js new file mode 100644 index 00000000000..bf421795d01 --- /dev/null +++ b/tests/direction.test.js @@ -0,0 +1,96 @@ +'use strict'; + +require('should'); + +describe('BG direction', function ( ) { + function setupSandbox(data, pluginBase) { + var clientSettings = {}; + + var sandbox = require('../lib/sandbox')(); + return sandbox.clientInit(clientSettings, Date.now(), pluginBase || {}, data); + } + + it('set the direction property - Flat', function (done) { + var sbx = setupSandbox({sgvs: [{direction: 'Flat'}]}); + + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('direction'); + var result = setter(); + result.value.should.equal('Flat'); + result.label.should.equal('→'); + result.entity.should.equal('→'); + done(); + }; + + var direction = require('../lib/plugins/direction')(); + direction.setProperties(sbx); + + }); + + it('set the direction property Double Up', function (done) { + var sbx = setupSandbox({sgvs: [{direction: 'DoubleUp'}]}); + + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('direction'); + var result = setter(); + result.value.should.equal('DoubleUp'); + result.label.should.equal('⇈'); + result.entity.should.equal('⇈'); + done(); + }; + + var direction = require('../lib/plugins/direction')(); + direction.setProperties(sbx); + + }); + + it('set a pill to the direction', function (done) { + var pluginBase = { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.label.should.equal('→︎'); + done(); + } + }; + + var sbx = setupSandbox({sgvs: [{direction: 'Flat'}]}, pluginBase); + var direction = require('../lib/plugins/direction')(); + direction.setProperties(sbx); + direction.updateVisualisation(sbx); + }); + + it('get the info for a direction', function () { + var direction = require('../lib/plugins/direction')(); + + direction.info({direction: 'NONE'}).label.should.equal('⇼'); + direction.info({direction: 'NONE'}).entity.should.equal('⇼'); + + direction.info({direction: 'DoubleUp'}).label.should.equal('⇈'); + direction.info({direction: 'DoubleUp'}).entity.should.equal('⇈'); + + direction.info({direction: 'SingleUp'}).label.should.equal('↑'); + direction.info({direction: 'SingleUp'}).entity.should.equal('↑'); + + direction.info({direction: 'FortyFiveUp'}).label.should.equal('↗'); + direction.info({direction: 'FortyFiveUp'}).entity.should.equal('↗'); + + direction.info({direction: 'Flat'}).label.should.equal('→'); + direction.info({direction: 'Flat'}).entity.should.equal('→'); + + direction.info({direction: 'FortyFiveDown'}).label.should.equal('↘'); + direction.info({direction: 'FortyFiveDown'}).entity.should.equal('↘'); + + direction.info({direction: 'SingleDown'}).label.should.equal('↓'); + direction.info({direction: 'SingleDown'}).entity.should.equal('↓'); + + direction.info({direction: 'DoubleDown'}).label.should.equal('⇊'); + direction.info({direction: 'DoubleDown'}).entity.should.equal('⇊'); + + direction.info({direction: 'NOT COMPUTABLE'}).label.should.equal('-'); + direction.info({direction: 'NOT COMPUTABLE'}).entity.should.equal('-'); + + direction.info({direction: 'RATE OUT OF RANGE'}).label.should.equal('⇕'); + direction.info({direction: 'RATE OUT OF RANGE'}).entity.should.equal('⇕'); + }); + + +}); diff --git a/tests/env.test.js b/tests/env.test.js new file mode 100644 index 00000000000..15467d23f6e --- /dev/null +++ b/tests/env.test.js @@ -0,0 +1,56 @@ +'use strict'; + +require('should'); + +describe('env', function ( ) { + it('show the right plugins', function () { + process.env.SHOW_PLUGINS = 'iob'; + process.env.ENABLE = 'iob cob'; + + var env = require('../env')(); + var showPlugins = env.settings.showPlugins; + showPlugins.should.containEql('iob'); + showPlugins.should.containEql('delta'); + showPlugins.should.containEql('direction'); + showPlugins.should.containEql('upbat'); + showPlugins.should.not.containEql('cob'); + + delete process.env.SHOW_PLUGINS; + delete process.env.ENABLE; + }); + + it('get extended settings', function () { + process.env.ENABLE = 'scaryplugin'; + process.env.SCARYPLUGIN_DO_THING = 'yes'; + + var env = require('../env')(); + env.settings.isEnabled('scaryplugin').should.equal(true); + + //Note the camelCase + env.extendedSettings.scaryplugin.doThing.should.equal('yes'); + + delete process.env.ENABLE; + delete process.env.SCARYPLUGIN_DO_THING; + }); + + it('add pushover to enable if one of the env vars is set', function () { + process.env.PUSHOVER_API_TOKEN = 'abc12345'; + + var env = require('../env')(); + env.settings.enable.should.containEql('pushover'); + env.extendedSettings.pushover.apiToken.should.equal('abc12345'); + + delete process.env.PUSHOVER_API_TOKEN; + }); + + it('add pushover to enable if one of the weird azure env vars is set', function () { + process.env.CUSTOMCONNSTR_PUSHOVER_API_TOKEN = 'abc12345'; + + var env = require('../env')(); + env.settings.enable.should.containEql('pushover'); + env.extendedSettings.pushover.apiToken.should.equal('abc12345'); + + delete process.env.PUSHOVER_API_TOKEN; + }); + +}); diff --git a/tests/errorcodes.test.js b/tests/errorcodes.test.js new file mode 100644 index 00000000000..ed1b158637b --- /dev/null +++ b/tests/errorcodes.test.js @@ -0,0 +1,111 @@ +var _ = require('lodash'); +var should = require('should'); +var levels = require('../lib/levels'); + +describe('errorcodes', function ( ) { + + var errorcodes = require('../lib/plugins/errorcodes')(); + + var now = Date.now(); + var env = require('../env')(); + var ctx = {}; + ctx.data = require('../lib/data')(env, ctx); + ctx.notifications = require('../lib/notifications')(env, ctx); + + + it('Not trigger an alarm when in range', function (done) { + ctx.notifications.initRequests(); + ctx.data.sgvs = [{mgdl: 100, mills: now}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + errorcodes.checkNotifications(sbx); + should.not.exist(ctx.notifications.findHighestAlarm()); + + done(); + }); + + it('should trigger a urgent alarm when ???', function (done) { + ctx.notifications.initRequests(); + ctx.data.sgvs = [{mgdl: 10, mills: now}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + errorcodes.checkNotifications(sbx); + ctx.notifications.findHighestAlarm().level.should.equal(levels.URGENT); + + done(); + }); + + it('should trigger a urgent alarm when hourglass', function (done) { + ctx.notifications.initRequests(); + ctx.data.sgvs = [{mgdl: 9, mills: now}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + errorcodes.checkNotifications(sbx); + var findHighestAlarm = ctx.notifications.findHighestAlarm(); + findHighestAlarm.level.should.equal(levels.URGENT); + findHighestAlarm.pushoverSound.should.equal('alien'); + + done(); + }); + + it('should trigger a low notification when needing calibration', function (done) { + ctx.notifications.initRequests(); + ctx.data.sgvs = [{mgdl: 5, mills: now}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + errorcodes.checkNotifications(sbx); + should.not.exist(ctx.notifications.findHighestAlarm()); + var info = _.first(ctx.notifications.findUnSnoozeable()); + info.level.should.equal(levels.INFO); + info.pushoverSound.should.equal('intermission'); + + done(); + }); + + it('should trigger a low notification when code < 9', function (done) { + + for (var i = 1; i < 9; i++) { + ctx.notifications.initRequests(); + ctx.data.sgvs = [{mgdl: i, mills: now}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + errorcodes.checkNotifications(sbx); + should.not.exist(ctx.notifications.findHighestAlarm()); + _.first(ctx.notifications.findUnSnoozeable()).level.should.be.lessThan(levels.WARN); + } + done(); + }); + + it('convert a code to display', function () { + errorcodes.toDisplay(5).should.equal('?NC'); + errorcodes.toDisplay(9).should.equal('?AD'); + errorcodes.toDisplay(10).should.equal('???'); + }); + + it('have default code to level mappings', function () { + var mapping = errorcodes.buildMappingFromSettings({}); + mapping[1].should.equal(levels.INFO); + mapping[2].should.equal(levels.INFO); + mapping[3].should.equal(levels.INFO); + mapping[4].should.equal(levels.INFO); + mapping[5].should.equal(levels.INFO); + mapping[6].should.equal(levels.INFO); + mapping[7].should.equal(levels.INFO); + mapping[8].should.equal(levels.INFO); + mapping[9].should.equal(levels.URGENT); + mapping[10].should.equal(levels.URGENT); + _.keys(mapping).length.should.equal(10); + }); + + it('allow config of custom code to level mappings', function () { + var mapping = errorcodes.buildMappingFromSettings({ + info: 'off' + , warn: '9 10' + , urgent: 'off' + }); + mapping[9].should.equal(levels.WARN); + mapping[10].should.equal(levels.WARN); + _.keys(mapping).length.should.equal(2); + }); + +}); \ No newline at end of file diff --git a/tests/fixtures/default-server-settings.js b/tests/fixtures/default-server-settings.js new file mode 100644 index 00000000000..113a5fa4aeb --- /dev/null +++ b/tests/fixtures/default-server-settings.js @@ -0,0 +1,36 @@ +'use strict'; + +module.exports = { + name: 'Nightscout' + , version: '0.8.0' + , apiEnabled: true + , careportalEnabled: true + , head: 'ae71dca' + , settings: { + units: 'mg/dl' + , timeFormat: 12 + , nightMode: false + , showRawbg: 'noise' + , customTitle: 'Test Title' + , theme: 'colors' + , alarmUrgentHigh: true + , alarmHigh: true + , alarmLow: true + , alarmUrgentLow: true + , alarmTimeagoWarn: true + , alarmTimeagoWarnMins: 15 + , alarmTimeagoUrgent: true + , alarmTimeagoUrgentMins: 30 + , language: 'en' + , enable: 'iob rawbg careportal delta direction upbat errorcodes' + , showPlugins: 'iob' + , alarmTypes: 'predict' + , thresholds: { + bgHigh: 200 + , bgTargetTop: 170 + , bgTargetBottom: 80 + , bgLow: 55 + } + , extendedSettings: { } + } +}; \ No newline at end of file diff --git a/tests/fixtures/example.json b/tests/fixtures/example.json index 88da09c6fa8..f2c3e6de02f 100644 --- a/tests/fixtures/example.json +++ b/tests/fixtures/example.json @@ -1 +1,242 @@ -[{"sgv":"5","dateString":"07/19/2014 10:49:15 AM","date":1405792155000,"device":"dexcom","direction":"NOT COMPUTABLE"},{"sgv":"5","dateString":"07/19/2014 10:44:15 AM","date":1405791855000,"device":"dexcom","direction":"NOT COMPUTABLE"},{"sgv":"5","dateString":"07/19/2014 10:39:15 AM","date":1405791555000,"device":"dexcom","direction":"NOT COMPUTABLE"},{"sgv":"5","dateString":"07/19/2014 10:34:15 AM","date":1405791255000,"device":"dexcom","direction":"NOT COMPUTABLE"},{"sgv":"5","dateString":"07/19/2014 10:29:15 AM","date":1405790955000,"device":"dexcom","direction":"NOT COMPUTABLE"},{"sgv":"5","dateString":"07/19/2014 10:24:15 AM","date":1405790655000,"device":"dexcom","direction":"NOT COMPUTABLE"},{"sgv":"5","dateString":"07/19/2014 10:19:15 AM","date":1405790355000,"device":"dexcom","direction":"NOT COMPUTABLE"},{"sgv":"5","dateString":"07/19/2014 10:14:15 AM","date":1405790055000,"device":"dexcom","direction":"NOT COMPUTABLE"},{"sgv":"5","dateString":"07/19/2014 10:09:15 AM","date":1405789755000,"device":"dexcom","direction":"NOT COMPUTABLE"},{"sgv":"5","dateString":"07/19/2014 10:04:15 AM","date":1405789455000,"device":"dexcom","direction":"NOT COMPUTABLE"},{"sgv":"5","dateString":"07/19/2014 09:59:15 AM","date":1405789155000,"device":"dexcom","direction":"NOT COMPUTABLE"},{"sgv":"5","dateString":"07/19/2014 09:54:15 AM","date":1405788855000,"device":"dexcom","direction":"NOT COMPUTABLE"},{"sgv":"178","dateString":"07/19/2014 03:59:15 AM","date":1405767555000,"device":"dexcom","direction":"Flat"},{"sgv":"179","dateString":"07/19/2014 03:54:15 AM","date":1405767255000,"device":"dexcom","direction":"Flat"},{"sgv":"178","dateString":"07/19/2014 03:49:15 AM","date":1405766955000,"device":"dexcom","direction":"Flat"},{"sgv":"177","dateString":"07/19/2014 03:44:15 AM","date":1405766655000,"device":"dexcom","direction":"Flat"},{"sgv":"176","dateString":"07/19/2014 03:39:15 AM","date":1405766355000,"device":"dexcom","direction":"Flat"},{"sgv":"176","dateString":"07/19/2014 03:34:15 AM","date":1405766055000,"device":"dexcom","direction":"Flat"},{"sgv":"175","dateString":"07/19/2014 03:29:16 AM","date":1405765756000,"device":"dexcom","direction":"Flat"},{"sgv":"174","dateString":"07/19/2014 03:24:15 AM","date":1405765455000,"device":"dexcom","direction":"Flat"},{"sgv":"174","dateString":"07/19/2014 03:19:15 AM","date":1405765155000,"device":"dexcom","direction":"Flat"},{"sgv":"175","dateString":"07/19/2014 03:14:15 AM","date":1405764855000,"device":"dexcom","direction":"Flat"},{"sgv":"176","dateString":"07/19/2014 03:09:15 AM","date":1405764555000,"device":"dexcom","direction":"Flat"},{"sgv":"176","dateString":"07/19/2014 03:04:15 AM","date":1405764255000,"device":"dexcom","direction":"Flat"},{"sgv":"173","dateString":"07/19/2014 02:59:15 AM","date":1405763955000,"device":"dexcom","direction":"Flat"},{"sgv":"171","dateString":"07/19/2014 02:54:15 AM","date":1405763655000,"device":"dexcom","direction":"Flat"},{"sgv":"170","dateString":"07/19/2014 02:49:15 AM","date":1405763355000,"device":"dexcom","direction":"Flat"},{"sgv":"171","dateString":"07/19/2014 02:44:15 AM","date":1405763055000,"device":"dexcom","direction":"Flat"},{"sgv":"169","dateString":"07/19/2014 02:39:15 AM","date":1405762755000,"device":"dexcom","direction":"Flat"},{"sgv":"169","dateString":"07/19/2014 02:34:15 AM","date":1405762455000,"device":"dexcom","direction":"Flat"}] \ No newline at end of file +[ + { + "type": "sgv", + "sgv": "5", + "dateString": "2014-07-19T10:49:15.000-07:00", + "date": 1405792155000, + "device": "dexcom", + "direction": "NOT COMPUTABLE" + }, + { + "type": "sgv", + "sgv": "5", + "dateString": "2014-07-19T10:44:15.000-07:00", + "date": 1405791855000, + "device": "dexcom", + "direction": "NOT COMPUTABLE" + }, + { + "type": "sgv", + "sgv": "5", + "dateString": "2014-07-19T10:39:15.000-07:00", + "date": 1405791555000, + "device": "dexcom", + "direction": "NOT COMPUTABLE" + }, + { + "type": "sgv", + "sgv": "5", + "dateString": "2014-07-19T10:34:15.000-07:00", + "date": 1405791255000, + "device": "dexcom", + "direction": "NOT COMPUTABLE" + }, + { + "type": "sgv", + "sgv": "5", + "dateString": "2014-07-19T10:29:15.000-07:00", + "date": 1405790955000, + "device": "dexcom", + "direction": "NOT COMPUTABLE" + }, + { + "type": "sgv", + "sgv": "5", + "dateString": "2014-07-19T10:24:15.000-07:00", + "date": 1405790655000, + "device": "dexcom", + "direction": "NOT COMPUTABLE" + }, + { + "type": "sgv", + "sgv": "5", + "dateString": "2014-07-19T10:19:15.000-07:00", + "date": 1405790355000, + "device": "dexcom", + "direction": "NOT COMPUTABLE" + }, + { + "type": "sgv", + "sgv": "5", + "dateString": "2014-07-19T10:14:15.000-07:00", + "date": 1405790055000, + "device": "dexcom", + "direction": "NOT COMPUTABLE" + }, + { + "type": "sgv", + "sgv": "5", + "dateString": "2014-07-19T10:09:15.000-07:00", + "date": 1405789755000, + "device": "dexcom", + "direction": "NOT COMPUTABLE" + }, + { + "type": "sgv", + "sgv": "5", + "dateString": "2014-07-19T10:04:15.000-07:00", + "date": 1405789455000, + "device": "dexcom", + "direction": "NOT COMPUTABLE" + }, + { + "type": "sgv", + "sgv": "5", + "dateString": "2014-07-19T09:59:15.000-07:00", + "date": 1405789155000, + "device": "dexcom", + "direction": "NOT COMPUTABLE" + }, + { + "type": "sgv", + "sgv": "5", + "dateString": "2014-07-19T09:54:15.000-07:00", + "date": 1405788855000, + "device": "dexcom", + "direction": "NOT COMPUTABLE" + }, + { + "type": "sgv", + "sgv": "178", + "dateString": "2014-07-19T03:59:15.000-07:00", + "date": 1405767555000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "179", + "dateString": "2014-07-19T03:54:15.000-07:00", + "date": 1405767255000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "178", + "dateString": "2014-07-19T03:49:15.000-07:00", + "date": 1405766955000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "177", + "dateString": "2014-07-19T03:44:15.000-07:00", + "date": 1405766655000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "176", + "dateString": "2014-07-19T03:39:15.000-07:00", + "date": 1405766355000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "176", + "dateString": "2014-07-19T03:34:15.000-07:00", + "date": 1405766055000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "175", + "dateString": "2014-07-19T03:29:16.000-07:00", + "date": 1405765756000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "174", + "dateString": "2014-07-19T03:24:15.000-07:00", + "date": 1405765455000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "174", + "dateString": "2014-07-19T03:19:15.000-07:00", + "date": 1405765155000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "175", + "dateString": "2014-07-19T03:14:15.000-07:00", + "date": 1405764855000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "176", + "dateString": "2014-07-19T03:09:15.000-07:00", + "date": 1405764555000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "176", + "dateString": "2014-07-19T03:04:15.000-07:00", + "date": 1405764255000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "173", + "dateString": "2014-07-19T02:59:15.000-07:00", + "date": 1405763955000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "171", + "dateString": "2014-07-19T02:54:15.000-07:00", + "date": 1405763655000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "170", + "dateString": "2014-07-19T02:49:15.000-07:00", + "date": 1405763355000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "171", + "dateString": "2014-07-19T02:44:15.000-07:00", + "date": 1405763055000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "169", + "dateString": "2014-07-19T02:39:15.000-07:00", + "date": 1405762755000, + "device": "dexcom", + "direction": "Flat" + }, + { + "type": "sgv", + "sgv": "169", + "dateString": "2014-07-19T02:34:15.000-07:00", + "date": 1405762455000, + "device": "dexcom", + "direction": "Flat" + } +] diff --git a/tests/fixtures/localstorage.js b/tests/fixtures/localstorage.js new file mode 100644 index 00000000000..0942cc2fffc --- /dev/null +++ b/tests/fixtures/localstorage.js @@ -0,0 +1,20 @@ +'use strict'; + +var browserStorage = []; + +var localstorage = { + get: function Get(item) { + return browserStorage[item] || null; + } + , set: function Set(item, value) { + browserStorage[item] = value; + } + , remove: function Remove(item) { + delete browserStorage[item]; + } + , removeAll: function RemoveAll() { + browserStorage = []; + } +}; + +module.exports = localstorage; \ No newline at end of file diff --git a/tests/hashauth.test.js b/tests/hashauth.test.js new file mode 100644 index 00000000000..eea93968da9 --- /dev/null +++ b/tests/hashauth.test.js @@ -0,0 +1,153 @@ +'use strict'; + +require('should'); +var benv = require('benv'); +var read = require('fs').readFileSync; +var serverSettings = require('./fixtures/default-server-settings'); + +describe('hashauth', function ( ) { + var self = this; + before(function (done) { + benv.setup(function() { + self.$ = require('jquery'); + self.$.localStorage = require('./fixtures/localstorage'); + + self.$.fn.tipsy = function mockTipsy ( ) { }; + + var indexHtml = read(__dirname + '/../static/index.html', 'utf8'); + self.$('body').html(indexHtml); + + var d3 = require('d3'); + //disable all d3 transitions so most of the other code can run with jsdom + d3.timer = function mockTimer() { }; + + benv.expose({ + $: self.$ + , jQuery: self.$ + , d3: d3 + , io: { + connect: function mockConnect ( ) { + return { + on: function mockOn ( ) { } + }; + } + } + }); + done(); + }); + }); + + after(function (done) { + benv.teardown(); + done(); + }); + + it ('should make module unauthorized', function () { + var plugins = require('../lib/plugins/')().registerClientDefaults(); + var client = require('../lib/client'); + var hashauth = require('../lib/hashauth'); + + hashauth.init(client,$); + hashauth.verifyAuthentication = function mockVerifyAuthentication(next) { + hashauth.authenticated = false; + next(true); + }; + + client.init(serverSettings, plugins); + + hashauth.inlineCode().indexOf('Device not authenticated').should.be.greaterThan(0); + hashauth.isAuthenticated().should.equal(false); + var testnull = (hashauth.hash()===null); + testnull.should.equal(true); + }); + + it ('should make module authorized', function () { + var plugins = require('../lib/plugins/')().registerClientDefaults(); + var client = require('../lib/client'); + var hashauth = require('../lib/hashauth'); + + hashauth.init(client,$); + hashauth.verifyAuthentication = function mockVerifyAuthentication(next) { + hashauth.authenticated = true; + next(true); + }; + + client.init(serverSettings, plugins); + + hashauth.inlineCode().indexOf('Device authenticated').should.be.greaterThan(0); + hashauth.isAuthenticated().should.equal(true); + }); + + it ('should store hash and the remove authentication', function () { + var plugins = require('../lib/plugins/')().registerClientDefaults(); + var client = require('../lib/client'); + var hashauth = require('../lib/hashauth'); + var localStorage = require('./fixtures/localstorage'); + + localStorage.remove('apisecrethash'); + + hashauth.init(client,$); + hashauth.verifyAuthentication = function mockVerifyAuthentication(next) { + hashauth.authenticated = true; + next(true); + }; + + client.init(serverSettings, plugins); + + hashauth.processSecret('this is my long pass phrase',true); + + hashauth.hash().should.equal('b723e97aa97846eb92d5264f084b2823f57c4aa1'); + localStorage.get('apisecrethash').should.equal('b723e97aa97846eb92d5264f084b2823f57c4aa1'); + hashauth.isAuthenticated().should.equal(true); + + hashauth.removeAuthentication(); + hashauth.isAuthenticated().should.equal(false); + }); + + it ('should not store hash', function () { + var plugins = require('../lib/plugins/')().registerClientDefaults(); + var client = require('../lib/client'); + var hashauth = require('../lib/hashauth'); + var localStorage = require('./fixtures/localstorage'); + + localStorage.remove('apisecrethash'); + + hashauth.init(client,$); + hashauth.verifyAuthentication = function mockVerifyAuthentication(next) { + hashauth.authenticated = true; + next(true); + }; + + client.init(serverSettings, plugins); + + hashauth.processSecret('this is my long pass phrase',false); + + hashauth.hash().should.equal('b723e97aa97846eb92d5264f084b2823f57c4aa1'); + var testnull = (localStorage.get('apisecrethash')===null); + testnull.should.equal(true); + hashauth.isAuthenticated().should.equal(true); + }); + + it ('should report secret too short', function () { + var plugins = require('../lib/plugins/')().registerClientDefaults(); + var client = require('../lib/client'); + var hashauth = require('../lib/hashauth'); + var localStorage = require('./fixtures/localstorage'); + + localStorage.remove('apisecrethash'); + + hashauth.init(client,$); + + client.init(serverSettings, plugins); + + window.alert = function mockConfirm (message) { + function containsLine (line) { + message.indexOf(line).should.be.greaterThan(-1); + } + containsLine('Too short API secret'); + return true; + }; + + hashauth.processSecret('short passp',false); + }); +}); diff --git a/tests/iob.test.js b/tests/iob.test.js new file mode 100644 index 00000000000..0c65e17c6e3 --- /dev/null +++ b/tests/iob.test.js @@ -0,0 +1,104 @@ +'use strict'; + +require('should'); + +describe('IOB', function ( ) { + var iob = require('../lib/plugins/iob')(); + + + it('should calculate IOB', function() { + + var time = Date.now() + , treatments = [ { + mills: time - 1, + insulin: '1.00' + } + ]; + + + var profileData = { + dia: 3, + sens: 0}; + + var profile = require('../lib/profilefunctions')([profileData]); + + var rightAfterBolus = iob.calcTotal(treatments, profile, time); + + rightAfterBolus.display.should.equal('1.00'); + + var afterSomeTime = iob.calcTotal(treatments, profile, time + (60 * 60 * 1000)); + + afterSomeTime.iob.should.be.lessThan(1); + afterSomeTime.iob.should.be.greaterThan(0); + + var afterDIA = iob.calcTotal(treatments, profile, time + (3 * 60 * 60 * 1000)); + + afterDIA.iob.should.equal(0); + + }); + + it('should calculate IOB using defaults', function() { + + var treatments = [{ + mills: Date.now() - 1, + insulin: '1.00' + }]; + + var rightAfterBolus = iob.calcTotal(treatments); + + rightAfterBolus.display.should.equal('1.00'); + + }); + + it('should not show a negative IOB when approaching 0', function() { + + var time = Date.now() - 1; + + var treatments = [{ + mills: time, + insulin: '5.00' + }]; + + var whenApproaching0 = iob.calcTotal(treatments, undefined, time + (3 * 60 * 60 * 1000) - (90 * 1000)); + + //before fix we got this: AssertionError: expected '-0.00' to be '0.00' + whenApproaching0.display.should.equal('0.00'); + + }); + + it('should calculate IOB using a 4 hour duration', function() { + + var time = Date.now() + , treatments = [ { + mills: time - 1, + insulin: '1.00' + } ]; + + var profileData = { + dia: 4, + sens: 0}; + + var profile = require('../lib/profilefunctions')([profileData]); + + + var rightAfterBolus = iob.calcTotal(treatments, profile, time); + + rightAfterBolus.display.should.equal('1.00'); + + var afterSomeTime = iob.calcTotal(treatments, profile, time + (60 * 60 * 1000)); + + afterSomeTime.iob.should.be.lessThan(1); + afterSomeTime.iob.should.be.greaterThan(0); + + var after3hDIA = iob.calcTotal(treatments, profile, time + (3 * 60 * 60 * 1000)); + + after3hDIA.iob.should.greaterThan(0); + + var after4hDIA = iob.calcTotal(treatments, profile, time + (4 * 60 * 60 * 1000)); + + after4hDIA.iob.should.equal(0); + + }); + + +}); \ No newline at end of file diff --git a/tests/language.test.js b/tests/language.test.js new file mode 100644 index 00000000000..81c457f2ba9 --- /dev/null +++ b/tests/language.test.js @@ -0,0 +1,24 @@ +'use strict'; + +require('should'); + +describe('language', function ( ) { + + it('use English by default', function () { + var language = require('../lib/language')(); + language.translate('Carbs').should.equal('Carbs'); + }); + + it('translate to French', function () { + var language = require('../lib/language')(); + language.set('fr'); + language.translate('Carbs').should.equal('Glucides'); + }); + + it('translate to Czech', function () { + var language = require('../lib/language')(); + language.set('cs'); + language.translate('Carbs').should.equal('Sacharidy'); + }); + +}); diff --git a/tests/levels.test.js b/tests/levels.test.js new file mode 100644 index 00000000000..537b8be34ba --- /dev/null +++ b/tests/levels.test.js @@ -0,0 +1,39 @@ +'use strict'; + +require('should'); + +describe('levels', function ( ) { + var levels = require('../lib/levels'); + + it('have levels', function () { + levels.URGENT.should.equal(2); + levels.WARN.should.equal(1); + levels.INFO.should.equal(0); + levels.LOW.should.equal(-1); + levels.LOWEST.should.equal(-2); + levels.NONE.should.equal(-3); + }); + + it('convert to display', function () { + levels.toDisplay(levels.URGENT).should.equal('Urgent'); + levels.toDisplay(levels.WARN).should.equal('Warning'); + levels.toDisplay(levels.INFO).should.equal('Info'); + levels.toDisplay(levels.LOW).should.equal('Low'); + levels.toDisplay(levels.LOWEST).should.equal('Lowest'); + levels.toDisplay(levels.NONE).should.equal('None'); + levels.toDisplay(42).should.equal('Unknown'); + levels.toDisplay(99).should.equal('Unknown'); + }); + + it('convert to lowercase', function () { + levels.toLowerCase(levels.URGENT).should.equal('urgent'); + levels.toLowerCase(levels.WARN).should.equal('warning'); + levels.toLowerCase(levels.INFO).should.equal('info'); + levels.toLowerCase(levels.LOW).should.equal('low'); + levels.toLowerCase(levels.LOWEST).should.equal('lowest'); + levels.toLowerCase(levels.NONE).should.equal('none'); + levels.toLowerCase(42).should.equal('unknown'); + levels.toLowerCase(99).should.equal('unknown'); + }); + +}); diff --git a/tests/maker.test.js b/tests/maker.test.js new file mode 100644 index 00000000000..a1256421d8f --- /dev/null +++ b/tests/maker.test.js @@ -0,0 +1,87 @@ +var should = require('should'); +var levels = require('../lib/levels'); + +describe('maker', function ( ) { + var maker = require('../lib/plugins/maker')({extendedSettings: {maker: {key: '12345'}}}); + + //prevent any calls to iftt + function noOpMakeRequest (key, event, eventName, callback) { + if (callback) { callback(); } + } + + maker.makeKeyRequest = noOpMakeRequest; + + it('turn values to a query', function (done) { + maker.valuesToQuery({ + value1: 'This is a title' + , value2: 'This is the message' + }).should.equal('?value1=This%20is%20a%20title&value2=This%20is%20the%20message'); + done(); + }); + + it('send a request', function (done) { + maker.sendEvent({name: 'test', message: 'This is the message', level: levels.toLowerCase(levels.WARN)}, function sendCallback (err) { + should.not.exist(err); + done(); + }); + }); + + it('not send a request without a name', function (done) { + maker.sendEvent({level: levels.toLowerCase(levels.WARN)}, function sendCallback (err) { + should.exist(err); + done(); + }); + }); + + it('not send a request without a level', function (done) { + maker.sendEvent({name: 'test'}, function sendCallback (err) { + should.exist(err); + done(); + }); + }); + + it('send a allclear, but only once', function (done) { + function mockedToTestSingleDone (key, event, eventName, callback) { + callback(); done(); + } + + maker.makeKeyRequest = mockedToTestSingleDone; + maker.sendAllClear({}, function sendCallback (err, result) { + should.not.exist(err); + result.sent.should.equal(true); + }); + + //send again, if done is called again test will fail + maker.sendAllClear({}, function sendCallback (err, result) { + should.not.exist(err); + result.sent.should.equal(false); + }); + }); +}); + + +describe('multi announcement maker', function ( ) { + var maker = require('../lib/plugins/maker')({extendedSettings: {maker: {key: 'use announcementKey instead', announcementKey: '12345 6789'}}}); + + it('send 2 requests for the 2 keys', function (done) { + + var key1Found = false; + var key2Found = false; + + maker.makeKeyRequest = function expect2Keys (key, event, eventName, callback) { + if (callback) { callback(); } + + key1Found = key1Found || key === '12345'; + key2Found = key2Found || key === '6789'; + + if (eventName === 'ns-warning-test' && key1Found && key2Found) { + done(); + } + }; + + maker.sendEvent({name: 'test', level: levels.toLowerCase(levels.WARN), isAnnouncement: true}, function sendCallback (err) { + should.not.exist(err); + }); + }); + +}); diff --git a/tests/mmconnect.test.js b/tests/mmconnect.test.js new file mode 100644 index 00000000000..060f6845394 --- /dev/null +++ b/tests/mmconnect.test.js @@ -0,0 +1,60 @@ +/* jshint node: true */ +'use strict'; + +var should = require('should'); + +describe('mmconnect', function () { + var mmconnect = require('../lib/plugins/mmconnect'); + + var env = { + extendedSettings: { + mmconnect: { + // 'userName' for consistency with the bridge plugin + userName: 'nightscout' + , password: 'wearenotwaiting' + , sgvLimit: '99' + , interval: '12000' + , maxRetryDuration: 1024 + , verbose: 'true' + } + } + }; + + describe('init()', function() { + + it('should create a runner if env vars are present', function() { + var runner = mmconnect.init(env); + should.exist(runner); + should.exist(runner.run); + runner.run.should.be.instanceof(Function); + }); + + it('should not create a runner if any env vars are absent', function() { + [ + {} + , {mmconnect: {}} + , {mmconnect: {userName: 'nightscout'}} + , {mmconnect: {password: 'wearenotwaiting'}} + ].forEach(function (extendedSettings) { + should.not.exist(mmconnect.init({extendedSettings: extendedSettings})); + }); + }); + }); + + + describe('getOptions_()', function() { + + it('should set the carelink client config from env', function() { + mmconnect.getOptions_(env).should.have.properties({ + username: 'nightscout' + , password: 'wearenotwaiting' + , sgvLimit: 99 + , interval: 12000 + , maxRetryDuration: 1024 + , verbose: true + }); + }); + + }); + +}); diff --git a/tests/mqtt.test.js b/tests/mqtt.test.js new file mode 100644 index 00000000000..afbe9205ced --- /dev/null +++ b/tests/mqtt.test.js @@ -0,0 +1,182 @@ +'use strict'; + +var should = require('should'); + +var FIVE_MINS = 5 * 60 * 1000; + +describe('mqtt', function ( ) { + + var self = this; + + before(function () { + process.env.MQTT_MONITOR = 'mqtt://user:password@localhost:12345'; + process.env.MONGO='mongodb://localhost/test_db'; + process.env.MONGO_COLLECTION='test_sgvs'; + self.env = require('../env')(); + self.es = require('event-stream'); + self.results = self.es.through(function (ch) { this.push(ch); }); + function outputs (fn) { + return self.es.writeArray(function (err, results) { + fn(err, results); + self.results.write(err || results); + }); + } + function written (data, fn) { + self.results.write(data); + setTimeout(fn, 5); + } + self.mqtt = require('../lib/mqtt')(self.env, {entries: { persist: outputs, create: written }, devicestatus: { create: written } }); + }); + + after(function () { + delete process.env.MQTT_MONITOR; + }); + + var now = Date.now() + , prev1 = now - FIVE_MINS + , prev2 = prev1 - FIVE_MINS + ; + + it('setup env correctly', function (done) { + self.env.mqtt_client_id.should.equal('fSjoHx8buyCtAc474tg8Dt3'); + done(); + }); + + it('handle a download with only sgvs', function (done) { + var packet = { + sgv: [ + {sgv_mgdl: 110, trend: 4, date: prev2} + , {sgv_mgdl: 105, trend: 4, date: prev1} + , {sgv_mgdl: 100, trend: 4, date: now} + ] + }; + + var merged = self.mqtt.sgvSensorMerge(packet); + + merged.length.should.equal(packet.sgv.length); + + done(); + + }); + + it('merge sgvs and sensor records that match up', function (done) { + var packet = { + sgv: [ + {sgv_mgdl: 110, trend: 4, date: prev2} + , {sgv_mgdl: 105, trend: 4, date: prev1} + , {sgv_mgdl: 100, trend: 4, date: now} + ] + , sensor: [ + {filtered: 99999, unfiltered: 99999, rssi: 200, date: prev2} + , {filtered: 99999, unfiltered: 99999, rssi: 200, date: prev1} + , {filtered: 99999, unfiltered: 99999, rssi: 200, date: now} + ] + }; + + var merged = self.mqtt.sgvSensorMerge(packet); + + merged.length.should.equal(packet.sgv.length); + + merged.filter(function (sgv) { + return sgv.filtered && sgv.unfiltered && sgv.rssi; + }).length.should.equal(packet.sgv.length); + + done(); + + }); + + it('downloadProtobuf should dispatch', function (done) { + + var payload = new Buffer('0a1108b70110d6d1fa6318f08df963200428011a1d323031352d30382d32335432323a35333a35352e3634392d30373a303020d7d1fa6328004a1508e0920b10c0850b18b20120d5d1fa6328ef8df963620a534d34313837393135306a053638393250', 'hex'); + + // var payload = self.mqtt.downloads.format(packet); + console.log('yaploda', '/downloads/protobuf', payload); + var l = [ ]; + self.results.on('data', function (chunk) { + l.push(chunk); + console.log('test data', l.length, chunk.length, chunk); + switch (l.length) { + case 0: // devicestatus + break; + case 2: // sgv + break; + case 3: // sgv + chunk.length.should.equal(1); + var first = chunk[0]; + should.exist(first.sgv); + should.exist(first.noise); + should.exist(first.date); + should.exist(first.dateString); + first.type.should.equal('sgv'); + break; + case 4: // cal + break; + case 1: // meter + break; + default: + break; + } + if (l.length >= 5) { + self.results.end( ); + } + }); + self.results.on('end', function ( ) { + done( ); + }); + self.mqtt.client.emit('message', '/downloads/protobuf', payload); + }); + + it('merge sgvs and sensor records that match up, and get the sgvs that don\'t match', function (done) { + var packet = { + sgv: [ + {sgv_mgdl: 110, trend: 4, date: prev2} + , {sgv_mgdl: 105, trend: 4, date: prev1} + , {sgv_mgdl: 100, trend: 4, date: now} + ] + , sensor: [ + {filtered: 99999, unfiltered: 99999, rssi: 200, date: now} + ] + }; + + var merged = self.mqtt.sgvSensorMerge(packet); + + merged.length.should.equal(packet.sgv.length); + + var withBoth = merged.filter(function (sgv) { + return sgv.sgv && sgv.filtered && sgv.unfiltered && sgv.rssi; + }); + + withBoth.length.should.equal(1); + + done(); + + }); + + it('merge sgvs and sensor records that match up, and get the sensors that don\'t match', function (done) { + var packet = { + sgv: [ + {sgv_mgdl: 100, trend: 4, date: now} + ] + , sensor: [ + {filtered: 99999, unfiltered: 99999, rssi: 200, date: prev2} + , {filtered: 99999, unfiltered: 99999, rssi: 200, date: prev1} + , {filtered: 99999, unfiltered: 99999, rssi: 200, date: now} + ] + }; + + var merged = self.mqtt.sgvSensorMerge(packet); + + merged.length.should.equal(packet.sensor.length); + + var withBoth = merged.filter(function (sgv) { + return sgv.sgv && sgv.filtered && sgv.unfiltered && sgv.rssi; + }); + + withBoth.length.should.equal(1); + + done(); + + }); + + +}); diff --git a/tests/notifications-api.test.js b/tests/notifications-api.test.js new file mode 100644 index 00000000000..8ef4f31e1ea --- /dev/null +++ b/tests/notifications-api.test.js @@ -0,0 +1,78 @@ +'use strict'; + +var request = require('supertest'); +var should = require('should'); +var Stream = require('stream'); + +var levels = require('../lib/levels'); +var notificationsAPI = require('../lib/api/notifications-api'); + +function examplePlugin () {} + +describe('Notifications API', function ( ) { + + it('ack notifications', function (done) { + + var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; + delete process.env.API_SECRET; + process.env.API_SECRET = 'this is my long pass phrase'; + var env = require('../env')( ); + env.api_secret.should.equal(known); + env.testMode = true; + + var ctx = { + bus: new Stream + , data: { + lastUpdated: Date.now() + } + }; + + var notifications = require('../lib/notifications')(env, ctx); + ctx.notifications = notifications; + + //start fresh to we don't pick up other notifications + ctx.bus = new Stream; + //if notification doesn't get called test will time out + ctx.bus.on('notification', function callback (notify) { + if (notify.clear) { + done(); + } + }); + + var exampleWarn = { + title: 'test' + , message: 'testing' + , level: levels.WARN + , plugin: examplePlugin + }; + + notifications.resetStateForTests(); + notifications.initRequests(); + notifications.requestNotify(exampleWarn); + notifications.findHighestAlarm().should.equal(exampleWarn); + notifications.process(); + + var app = require('express')(); + app.enable('api'); + var wares = require('../lib/middleware/')(env); + app.use('/', notificationsAPI(app, wares, ctx)); + + function makeRequest () { + request(app) + .get('/notifications/ack?level=1') + .set('api-secret', env.api_secret || '') + .expect(200) + .end(function (err) { + should.not.exist(err); + if (err) { + console.error(err); + } + }); + } + + makeRequest(); + + //2nd call should have no effect, done should NOT be called again + makeRequest(); + }); +}); \ No newline at end of file diff --git a/tests/notifications.test.js b/tests/notifications.test.js new file mode 100644 index 00000000000..0f0014ef158 --- /dev/null +++ b/tests/notifications.test.js @@ -0,0 +1,206 @@ +var should = require('should'); +var Stream = require('stream'); + +var levels = require('../lib/levels'); + +describe('notifications', function ( ) { + + var env = {testMode: true}; + + var ctx = { + bus: new Stream + , data: { + lastUpdated: Date.now() + } + }; + + var notifications = require('../lib/notifications')(env, ctx); + + function examplePlugin () {} + + var exampleInfo = { + title: 'test' + , message: 'testing' + , level: levels.INFO + , plugin: examplePlugin + }; + + var exampleWarn = { + title: 'test' + , message: 'testing' + , level: levels.WARN + , plugin: examplePlugin + }; + + var exampleUrgent = { + title: 'test' + , message: 'testing' + , level: levels.URGENT + , plugin: examplePlugin + }; + + var exampleSnooze = { + level: levels.WARN + , title: 'exampleSnooze' + , message: 'exampleSnooze message' + , lengthMills: 10000 + }; + + var exampleSnoozeNone = { + level: levels.WARN + , title: 'exampleSnoozeNone' + , message: 'exampleSnoozeNone message' + , lengthMills: 1 + }; + + var exampleSnoozeUrgent = { + level: levels.URGENT + , title: 'exampleSnoozeUrgent' + , message: 'exampleSnoozeUrgent message' + , lengthMills: 10000 + }; + + + function expectNotification (check, done) { + //start fresh to we don't pick up other notifications + ctx.bus = new Stream; + //if notification doesn't get called test will time out + ctx.bus.on('notification', function callback (notify) { + if (check(notify)) { + done(); + } + }); + } + + function clearToDone (done) { + expectNotification(function expectClear (notify) { + return notify.clear; + }, done); + } + + function notifyToDone (done) { + expectNotification(function expectNotClear (notify) { + return ! notify.clear; + }, done); + } + + it('initAndReInit', function (done) { + notifications.initRequests(); + notifications.requestNotify(exampleWarn); + notifications.findHighestAlarm().should.equal(exampleWarn); + notifications.initRequests(); + should.not.exist(notifications.findHighestAlarm()); + done(); + }); + + + it('emitAWarning', function (done) { + //start fresh to we don't pick up other notifications + ctx.bus = new Stream; + //if notification doesn't get called test will time out + ctx.bus.on('notification', function callback ( ) { + done(); + }); + + notifications.resetStateForTests(); + notifications.initRequests(); + notifications.requestNotify(exampleWarn); + notifications.findHighestAlarm().should.equal(exampleWarn); + notifications.process(); + }); + + it('emitAnInfo', function (done) { + notifyToDone(done); + + notifications.resetStateForTests(); + notifications.initRequests(); + notifications.requestNotify(exampleInfo); + should.not.exist(notifications.findHighestAlarm()); + + notifications.process(); + }); + + it('emitAllClear 1 time after alarm is auto acked', function (done) { + clearToDone(done); + + notifications.resetStateForTests(); + notifications.initRequests(); + notifications.requestNotify(exampleWarn); + notifications.findHighestAlarm().should.equal(exampleWarn); + notifications.process(); + + notifications.initRequests(); + //don't request a notify this time, and an auto ack should be sent + should.not.exist(notifications.findHighestAlarm()); + notifications.process(); + + var alarm = notifications.getAlarmForTests(levels.WARN); + alarm.level.should.equal(levels.WARN); + alarm.silenceTime.should.equal(1); + alarm.lastAckTime.should.be.approximately(Date.now(), 2000); + should.not.exist(alarm.lastEmitTime); + + //clear last emit time, even with that all clear shouldn't be sent again since there was no alarm cleared + delete alarm.lastEmitTime; + + //process 1 more time to make sure all clear is only sent once + notifications.initRequests(); + //don't request a notify this time, and an auto ack should be sent + should.not.exist(notifications.findHighestAlarm()); + notifications.process(); + }); + + it('Can be snoozed', function (done) { + notifyToDone(done); //shouldn't get called + + notifications.resetStateForTests(); + notifications.initRequests(); + notifications.requestNotify(exampleWarn); + notifications.requestSnooze(exampleSnooze); + notifications.snoozedBy(exampleWarn).should.equal(exampleSnooze); + notifications.process(); + + done(); + }); + + it('Can be snoozed by last snooze', function (done) { + notifyToDone(done); //shouldn't get called + + notifications.resetStateForTests(); + notifications.initRequests(); + notifications.requestNotify(exampleWarn); + notifications.requestSnooze(exampleSnoozeNone); + notifications.requestSnooze(exampleSnooze); + notifications.snoozedBy(exampleWarn).should.equal(exampleSnooze); + notifications.process(); + + done(); + }); + + it('Urgent alarms can\'t be snoozed by warn', function (done) { + clearToDone(done); //shouldn't get called + + notifications.resetStateForTests(); + notifications.initRequests(); + notifications.requestNotify(exampleUrgent); + notifications.requestSnooze(exampleSnooze); + should.not.exist(notifications.snoozedBy(exampleUrgent)); + notifications.process(); + + done(); + }); + + it('Warnings can be snoozed by urgent', function (done) { + notifyToDone(done); //shouldn't get called + + notifications.resetStateForTests(); + notifications.initRequests(); + notifications.requestNotify(exampleWarn); + notifications.requestSnooze(exampleSnoozeUrgent); + notifications.snoozedBy(exampleWarn).should.equal(exampleSnoozeUrgent); + notifications.process(); + + done(); + }); + +}); diff --git a/tests/pebble.test.js b/tests/pebble.test.js new file mode 100644 index 00000000000..d9b7f7b2912 --- /dev/null +++ b/tests/pebble.test.js @@ -0,0 +1,254 @@ +'use strict'; + +var request = require('supertest'); +var should = require('should'); + +//Mocked ctx +var ctx = {}; +var env = {}; +var now = Date.now(); + +function updateMills (entries) { + //last is now, assume 5m between points + for (var i = 0; i < entries.length; i++) { + var entry = entries[entries.length - i - 1]; + entry.mills = now - (i * 5 * 60 * 1000); + } + return entries; +} + +ctx.data = require('../lib/data')(env, ctx); +ctx.data.sgvs = updateMills([ + { device: 'dexcom', + mgdl: 91, + direction: 'Flat', + type: 'sgv', + filtered: 124048, + unfiltered: 118880, + rssi: 174, + noise: 1 + } + , { device: 'dexcom', + mgdl: 88, + direction: 'Flat', + type: 'sgv', + filtered: 120464, + unfiltered: 116608, + rssi: 175, + noise: 1 + } + , { device: 'dexcom', + mgdl: 86, + direction: 'Flat', + type: 'sgv', + filtered: 117808, + unfiltered: 114640, + rssi: 169, + noise: 1 + } + , { device: 'dexcom', + mgdl: 92, + direction: 'Flat', + type: 'sgv', + filtered: 115680, + unfiltered: 113552, + rssi: 179, + noise: 1 + } + , { device: 'dexcom', + mgdl: 90, + direction: 'Flat', + type: 'sgv', + filtered: 113984, + unfiltered: 111920, + rssi: 179, + noise: 1 + } +]); + +ctx.data.cals = updateMills([ + { device: 'dexcom', + slope: 895.8571693029189, + intercept: 34281.06876195567, + scale: 1, + type: 'cal' + } +]); + +ctx.data.profiles = [{dia: 4 }]; + +ctx.data.treatments = updateMills([ + { eventType: 'Snack Bolus', insulin: '1.50', carbs: '22' } +]); + +ctx.data.devicestatus.uploaderBattery = 100; + +describe('Pebble Endpoint', function ( ) { + var pebble = require('../lib/pebble'); + before(function (done) { + var env = require('../env')( ); + this.app = require('express')( ); + this.app.enable('api'); + this.app.use('/pebble', pebble(env, ctx)); + done(); + }); + + it('/pebble default(1) count', function (done) { + request(this.app) + .get('/pebble') + .expect(200) + .end(function (err, res) { + var bgs = res.body.bgs; + bgs.length.should.equal(1); + var bg = bgs[0]; + bg.sgv.should.equal('90'); + bg.bgdelta.should.equal(-2); + bg.trend.should.equal(4); + bg.direction.should.equal('Flat'); + bg.datetime.should.equal(now); + should.not.exist(bg.filtered); + should.not.exist(bg.unfiltered); + should.not.exist(bg.noise); + should.not.exist(bg.rssi); + should.not.exist(bg.iob); + bg.battery.should.equal('100'); + + res.body.cals.length.should.equal(0); + done( ); + }); + }); + + it('/pebble with mmol param', function (done) { + request(this.app) + .get('/pebble?units=mmol') + .expect(200) + .end(function (err, res) { + var bgs = res.body.bgs; + bgs.length.should.equal(1); + var bg = bgs[0]; + bg.sgv.should.equal('5.0'); + bg.bgdelta.should.equal('-0.1'); + bg.trend.should.equal(4); + bg.direction.should.equal('Flat'); + bg.datetime.should.equal(now); + should.not.exist(bg.filtered); + should.not.exist(bg.unfiltered); + should.not.exist(bg.noise); + should.not.exist(bg.rssi); + bg.battery.should.equal('100'); + + res.body.cals.length.should.equal(0); + done( ); + }); + }); + + it('/pebble?count=2', function (done) { + request(this.app) + .get('/pebble?count=2') + .expect(200) + .end(function (err, res) { + var bgs = res.body.bgs; + bgs.length.should.equal(2); + var bg = bgs[0]; + bg.sgv.should.equal('90'); + bg.bgdelta.should.equal(-2); + bg.trend.should.equal(4); + bg.direction.should.equal('Flat'); + bg.datetime.should.equal(now); + should.not.exist(bg.filtered); + should.not.exist(bg.unfiltered); + should.not.exist(bg.noise); + should.not.exist(bg.rssi); + bg.battery.should.equal('100'); + + res.body.cals.length.should.equal(0); + done( ); + }); + }); + + it('/pebble without battery', function (done) { + delete ctx.data.devicestatus.uploaderBattery; + request(this.app) + .get('/pebble') + .expect(200) + .end(function (err, res) { + var bgs = res.body.bgs; + bgs.length.should.equal(1); + should.not.exist(bgs[0].battery); + + res.body.cals.length.should.equal(0); + done( ); + }); + }); + + it('/pebble with a negative battery', function (done) { + ctx.data.devicestatus.uploaderBattery = -1; + request(this.app) + .get('/pebble') + .expect(200) + .end(function (err, res) { + var bgs = res.body.bgs; + bgs.length.should.equal(1); + should.not.exist(bgs[0].battery); + + res.body.cals.length.should.equal(0); + done( ); + }); + }); + + it('/pebble with a false battery', function (done) { + ctx.data.devicestatus.uploaderBattery = false; + request(this.app) + .get('/pebble') + .expect(200) + .end(function (err, res) { + var bgs = res.body.bgs; + bgs.length.should.equal(1); + should.not.exist(bgs[0].battery); + + res.body.cals.length.should.equal(0); + done( ); + }); + }); +}); + +describe('Pebble Endpoint with Raw and IOB', function ( ) { + var pebbleRaw = require('../lib/pebble'); + before(function (done) { + ctx.data.devicestatus.uploaderBattery = 100; + var envRaw = require('../env')( ); + envRaw.settings.enable = ['rawbg', 'iob']; + this.appRaw = require('express')( ); + this.appRaw.enable('api'); + this.appRaw.use('/pebble', pebbleRaw(envRaw, ctx)); + done(); + }); + + it('/pebble', function (done) { + request(this.appRaw) + .get('/pebble?count=2') + .expect(200) + .end(function (err, res) { + var bgs = res.body.bgs; + bgs.length.should.equal(2); + var bg = bgs[0]; + bg.sgv.should.equal('90'); + bg.bgdelta.should.equal(-2); + bg.trend.should.equal(4); + bg.direction.should.equal('Flat'); + bg.datetime.should.equal(now); + bg.filtered.should.equal(113984); + bg.unfiltered.should.equal(111920); + bg.noise.should.equal(1); + bg.battery.should.equal('100'); + + res.body.cals.length.should.equal(1); + var cal = res.body.cals[0]; + cal.slope.toFixed(3).should.equal('895.857'); + cal.intercept.toFixed(3).should.equal('34281.069'); + cal.scale.should.equal(1); + done( ); + }); + }); + +}); \ No newline at end of file diff --git a/tests/pluginbase.test.js b/tests/pluginbase.test.js new file mode 100644 index 00000000000..2397720b09e --- /dev/null +++ b/tests/pluginbase.test.js @@ -0,0 +1,54 @@ +'use strict'; + +require('should'); +var benv = require('benv'); + +describe('pluginbase', function ( ) { + + before(function (done) { + benv.setup(function() { + benv.expose({ + $: require('jquery') + , jQuery: require('jquery') + }); + done(); + }); + }); + + after(function (done) { + benv.teardown(); + done(); + }); + + it('does stuff', function() { + + function div (clazz) { + return $('
'); + } + + var container = div('container') + , bgStatus = div('bgStatus').appendTo(container) + , majorPills = div('majorPills').appendTo(bgStatus) + , minorPills = div('minorPills').appendTo(bgStatus) + , statusPills = div('statusPills').appendTo(bgStatus) + , tooltip = div('tooltip').appendTo(container) + ; + + var fake = { + name: 'fake' + , label: 'Insulin-on-Board' + , pluginType: 'pill-major' + }; + + var pluginbase = require('../lib/plugins/pluginbase')(majorPills, minorPills, statusPills, bgStatus, tooltip); + + pluginbase.updatePillText(fake, { + value: '123' + , label: 'TEST' + , info: [{label: 'Label', value: 'Value'}] + }); + + majorPills.length.should.equal(1); + }); + +}); \ No newline at end of file diff --git a/tests/plugins.test.js b/tests/plugins.test.js new file mode 100644 index 00000000000..0b0fc20ac43 --- /dev/null +++ b/tests/plugins.test.js @@ -0,0 +1,33 @@ +'use strict'; + +var should = require('should'); + +describe('Plugins', function ( ) { + + + it('should find client plugins, but not server only plugins', function (done) { + var plugins = require('../lib/plugins/')().registerClientDefaults(); + + plugins('delta').name.should.equal('delta'); + plugins('rawbg').name.should.equal('rawbg'); + + //server only plugin + should.not.exist(plugins('treatmentnotify')); + + done( ); + }); + + it('should find sever plugins, but not client only plugins', function (done) { + var plugins = require('../lib/plugins/')().registerServerDefaults(); + + plugins('rawbg').name.should.equal('rawbg'); + plugins('treatmentnotify').name.should.equal('treatmentnotify'); + + //client only plugin + should.not.exist(plugins('cannulaage')); + + done( ); + }); + + +}); diff --git a/tests/profile.test.js b/tests/profile.test.js new file mode 100644 index 00000000000..8171f459e3d --- /dev/null +++ b/tests/profile.test.js @@ -0,0 +1,190 @@ +var should = require('should'); +var moment = require('moment-timezone'); + +describe('Profile', function ( ) { + + var profile_empty = require('../lib/profilefunctions')(); + + it('should say it does not have data before it has data', function() { + var hasData = profile_empty.hasData(); + hasData.should.equal(false); + }); + + it('should return undefined if asking for keys before init', function() { + var dia = profile_empty.getDIA(now); + should.not.exist(dia); + }); + + it('should return undefined if asking for missing keys', function() { + var sens = profile_empty.getSensitivity(now); + should.not.exist(sens); + }); + + var profileData = { + 'dia': 3 + , 'carbs_hr': 30 + , 'carbratio': 7 + , 'sens': 35 + , 'target_low': 95 + , 'target_high': 120 + }; + + var profile = require('../lib/profilefunctions')([profileData]); +// console.log(profile); + + var now = Date.now(); + + it('should know what the DIA is with old style profiles', function() { + var dia = profile.getDIA(now); + dia.should.equal(3); + }); + + it('should know what the DIA is with old style profiles, with missing date argument', function() { + var dia = profile.getDIA(); + dia.should.equal(3); + }); + + it('should know what the carbs_hr is with old style profiles', function() { + var carbs_hr = profile.getCarbAbsorptionRate(now); + carbs_hr.should.equal(30); + }); + + it('should know what the carbratio is with old style profiles', function() { + var carbRatio = profile.getCarbRatio(now); + carbRatio.should.equal(7); + }); + + it('should know what the sensitivity is with old style profiles', function() { + var dia = profile.getSensitivity(now); + dia.should.equal(35); + }); + + it('should know what the low target is with old style profiles', function() { + var dia = profile.getLowBGTarget(now); + dia.should.equal(95); + }); + + it('should know what the high target is with old style profiles', function() { + var dia = profile.getHighBGTarget(now); + dia.should.equal(120); + }); + + it('should know how to reload data and still know what the low target is with old style profiles', function() { + + var profile2 = require('../lib/profilefunctions')([profileData]); + var profileData2 = { + 'dia': 3, + 'carbs_hr': 30, + 'carbratio': 7, + 'sens': 35, + 'target_low': 50, + 'target_high': 120 + }; + + profile2.loadData([profileData2]); + var dia = profile2.getLowBGTarget(now); + dia.should.equal(50); + }); + + var complexProfileData = + { + 'timezone': moment.tz().zoneName(), //Assume these are in the localtime zone so tests pass when not on UTC time + 'sens': [ + { + 'time': '00:00', + 'value': 10 + }, + { + 'time': '02:00', + 'value': 10 + }, + { + 'time': '07:00', + 'value': 9 + } + ], + 'dia': 3, + 'carbratio': [ + { + 'time': '00:00', + 'value': 16 + }, + { + 'time': '06:00', + 'value': 15 + }, + { + 'time': '14:00', + 'value': 16 + } + ], + 'carbs_hr': 30, + 'startDate': '2015-06-21', + 'basal': [ + { + 'time': '00:00', + 'value': 0.175 + }, + { + 'time': '02:30', + 'value': 0.125 + }, + { + 'time': '05:00', + 'value': 0.075 + }, + { + 'time': '08:00', + 'value': 0.1 + }, + { + 'time': '14:00', + 'value': 0.125 + }, + { + 'time': '20:00', + 'value': 0.3 + }, + { + 'time': '22:00', + 'value': 0.225 + } + ], + 'target_low': 4.5, + 'target_high': 8, + 'units': 'mmol' +}; + + var complexProfile = require('../lib/profilefunctions')([complexProfileData]); + + var noon = new Date('2015-06-22 12:00:00').getTime(); + var threepm = new Date('2015-06-22 15:00:00').getTime(); + + it('should return profile units when configured', function() { + var value = complexProfile.getUnits(); + value.should.equal('mmol'); + }); + + + it('should know what the basal rate is at 12:00 with complex style profiles', function() { + var value = complexProfile.getBasal(noon); + value.should.equal(0.1); + }); + + it('should know what the basal rate is at 15:00 with complex style profiles', function() { + var value = complexProfile.getBasal(threepm); + value.should.equal(0.125); + }); + + it('should know what the carbratio is at 12:00 with complex style profiles', function() { + var carbRatio = complexProfile.getCarbRatio(noon); + carbRatio.should.equal(15); + }); + + it('should know what the sensitivity is at 12:00 with complex style profiles', function() { + var dia = complexProfile.getSensitivity(noon); + dia.should.equal(9); + }); + + +}); \ No newline at end of file diff --git a/tests/profileeditor.test.js b/tests/profileeditor.test.js new file mode 100644 index 00000000000..ac6e3c95f29 --- /dev/null +++ b/tests/profileeditor.test.js @@ -0,0 +1,145 @@ +'use strict'; + +require('should'); +var benv = require('benv'); +var read = require('fs').readFileSync; +var serverSettings = require('./fixtures/default-server-settings'); + +var nowData = { + sgvs: [ + { mgdl: 100, mills: Date.now(), direction: 'Flat', type: 'sgv' } + ] +}; + + +var exampleProfile = { + //General values + 'dia':3, + + // Simple style values, 'from' are in minutes from midnight + 'carbratio': [ + { + 'time': '00:00', + 'value': 30 + }], + 'carbs_hr':30, + 'delay': 20, + 'sens': [ + { + 'time': '00:00', + 'value': 100 + } + , { + 'time': '8:00', + 'value': 80 + }], + 'startDate': new Date(), + 'timezone': 'UTC', + + //perGIvalues style values + 'perGIvalues': false, + 'carbs_hr_high': 30, + 'carbs_hr_medium': 30, + 'carbs_hr_low': 30, + 'delay_high': 15, + 'delay_medium': 20, + 'delay_low': 20, + + 'basal':[ + { + 'time': '00:00', + 'value': 0.1 + }], + 'target_low':[ + { + 'time': '00:00', + 'value': 0 + }], + 'target_high':[ + { + 'time': '00:00', + 'value': 0 + }] +}; +exampleProfile.startDate.setSeconds(0); +exampleProfile.startDate.setMilliseconds(0); + + +describe('profile editor', function ( ) { + var self = this; + + before(function (done) { + benv.setup(function() { + self.$ = require('jquery'); + self.$.localStorage = require('./fixtures/localstorage'); + + self.$.fn.tipsy = function mockTipsy ( ) { }; + + var indexHtml = read(__dirname + '/../static/profile/index.html', 'utf8'); + self.$('body').html(indexHtml); + + self.$.ajax = function mockAjax (url, opts) { + return { + done: function mockDone (fn) { + if (opts && opts.success && opts.success.call) { + opts.success([exampleProfile]); + } + fn(); + return { + fail: function () {} + }; + } + }; + }; + + var d3 = require('d3'); + //disable all d3 transitions so most of the other code can run with jsdom + d3.timer = function mockTimer() { }; + + benv.expose({ + $: self.$ + , jQuery: self.$ + , d3: d3 + , serverSettings: serverSettings + , io: { + connect: function mockConnect ( ) { + return { + on: function mockOn ( ) { } + }; + } + } + }); + + benv.require(__dirname + '/../bundle/bundle.source.js'); + benv.require(__dirname + '/../static/profile/js/profileeditor.js'); + + done(); + }); + }); + + after(function (done) { + benv.teardown(true); + done(); + }); + + it ('don\'t blow up', function (done) { + var plugins = require('../lib/plugins/')().registerClientDefaults(); + var client = require('../lib/client'); + + var hashauth = require('../lib/hashauth'); + hashauth.init(client,$); + hashauth.verifyAuthentication = function mockVerifyAuthentication(next) { + hashauth.authenticated = true; + next(true); + }; + + + client.init(serverSettings, plugins); + client.dataUpdate(nowData); + + $('#pe_form').find('button').click(); + + done(); + }); + +}); diff --git a/tests/pushnotify.test.js b/tests/pushnotify.test.js new file mode 100644 index 00000000000..dc0bc9151eb --- /dev/null +++ b/tests/pushnotify.test.js @@ -0,0 +1,112 @@ +'use strict'; + +var should = require('should'); +var levels = require('../lib/levels'); + +describe('pushnotify', function ( ) { + + it('send a pushover alarm, but only 1 time', function (done) { + var env = require('../env')(); + var ctx = {}; + + ctx.notifications = require('../lib/notifications')(env, ctx); + + var notify = { + title: 'Warning, this is a test!' + , message: 'details details details details' + , level: levels.WARN + , pushoverSound: 'climb' + , plugin: {name: 'test'} + }; + + ctx.pushover = { + PRIORITY_NORMAL: 0 + , PRIORITY_EMERGENCY: 2 + , send: function mockedSend (notify2, callback) { + should.deepEqual(notify, notify2); + callback(null, JSON.stringify({receipt: 'abcd12345'})); + done(); + } + }; + + ctx.pushnotify = require('../lib/pushnotify')(env, ctx); + + ctx.pushnotify.emitNotification(notify); + + //call again, but should be deduped, or fail with 'done() called multiple times' + ctx.pushnotify.emitNotification(notify); + + }); + + it('send a pushover notification, but only 1 time', function (done) { + var env = require('../env')(); + var ctx = {}; + + ctx.notifications = require('../lib/notifications')(env, ctx); + + var notify = { + title: 'Sent from a test' + , message: 'details details details details' + , level: levels.INFO + , plugin: {name: 'test'} + }; + + ctx.pushover = { + PRIORITY_NORMAL: 0 + , PRIORITY_EMERGENCY: 2 + , send: function mockedSend (notify2, callback) { + should.deepEqual(notify, notify2); + callback(null, JSON.stringify({})); + done(); + } + }; + + ctx.pushnotify = require('../lib/pushnotify')(env, ctx); + + ctx.pushnotify.emitNotification(notify); + + //call again, but should be deduped, or fail with 'done() called multiple times' + ctx.pushnotify.emitNotification(notify); + + }); + + it('send a pushover alarm, and then cancel', function (done) { + var env = require('../env')(); + var ctx = {}; + + ctx.notifications = require('../lib/notifications')(env, ctx); + + var notify = { + title: 'Warning, this is a test!' + , message: 'details details details details' + , level: levels.WARN + , pushoverSound: 'climb' + , plugin: {name: 'test'} + }; + + ctx.pushover = { + PRIORITY_NORMAL: 0 + , PRIORITY_EMERGENCY: 2 + , send: function mockedSend (notify2, callback) { + should.deepEqual(notify, notify2); + callback(null, JSON.stringify({receipt: 'abcd12345'})); + } + , cancelWithReceipt: function mockedCancel (receipt) { + receipt.should.equal('abcd12345'); + done(); + } + }; + + ctx.pushnotify = require('../lib/pushnotify')(env, ctx); + + //first send the warning + ctx.pushnotify.emitNotification(notify); + + //then pretend is was acked from the web + ctx.pushnotify.emitNotification({clear: true}); + + }); + + + +}); diff --git a/tests/pushover.test.js b/tests/pushover.test.js new file mode 100644 index 00000000000..598086e5159 --- /dev/null +++ b/tests/pushover.test.js @@ -0,0 +1,202 @@ +'use strict'; + +var should = require('should'); +var levels = require('../lib/levels'); + +describe('pushover', function ( ) { + + var baseurl = 'https://nightscout.test'; + + var env = { + settings: { + baseURL: baseurl + } + , extendedSettings: { + pushover: { + userKey: '12345' + , apiToken: '6789' + } + } + }; + + var pushover = require('../lib/plugins/pushover')(env); + + it('convert a warning to a message and send it', function (done) { + + var notify = { + title: 'Warning, this is a test!' + , level: levels.WARN + , pushoverSound: 'climb' + , plugin: {name: 'test'} + }; + + pushover.sendAPIRequest = function mockedSendAPIRequest (msg) { + msg.title.should.equal(notify.title); + should.not.exist(msg.message); + msg.priority.should.equal(2); + msg.retry.should.equal(15 * 60); + msg.sound.should.equal(notify.pushoverSound); + msg.callback.indexOf(baseurl).should.equal(0); + done(); + }; + + pushover.send(notify); + }); + + it('convert an urgent to a message and send it', function (done) { + + var notify = { + title: 'Urgent, this is a test!' + , message: 'details details details details' + , level: levels.URGENT + , pushoverSound: 'persistent' + , plugin: {name: 'test'} + }; + + pushover.sendAPIRequest = function mockedSendAPIRequest (msg) { + msg.title.should.equal(notify.title); + msg.message.should.equal(notify.message); + msg.priority.should.equal(2); + msg.retry.should.equal(2 * 60); + msg.sound.should.equal(notify.pushoverSound); + done(); + }; + + pushover.send(notify); + }); + +}); + +describe('support legacy pushover groupkey', function ( ) { + var env = { + extendedSettings: { + pushover: { + groupKey: 'abcd' + , apiToken: '6789' + } + } + }; + + var pushover = require('../lib/plugins/pushover')(env); + + it('send', function (done) { + + var notify = { + title: 'Warning, this is a test!' + , message: 'details details details details' + , level: levels.WARN + , pushoverSound: 'climb' + , plugin: {name: 'test'} + , isAnnouncement: true + }; + + pushover.sendAPIRequest = function mockedSendAPIRequest (msg) { + msg.title.should.equal(notify.title); + msg.priority.should.equal(2); + msg.sound.should.equal(notify.pushoverSound); + done(); + }; + + pushover.send(notify); + }); + +}); + +describe('multi announcement pushover', function ( ) { + var env = { + extendedSettings: { + pushover: { + userKey: 'use announcementKey instead' + , announcementKey: 'abcd efgh' + , apiToken: '6789' + } + } + }; + + var pushover = require('../lib/plugins/pushover')(env); + + it('send multiple pushes if there are multiple keys', function (done) { + + var notify = { + title: 'Warning, this is a test!' + , message: 'details details details details' + , level: levels.WARN + , pushoverSound: 'climb' + , plugin: {name: 'test'} + , isAnnouncement: true + }; + + var key1Found = false; + var key2Found = false; + + pushover.sendAPIRequest = function mockedSendAPIRequest (msg) { + msg.title.should.equal(notify.title); + msg.priority.should.equal(2); + msg.sound.should.equal(notify.pushoverSound); + + key1Found = key1Found || msg.user === 'abcd'; + key2Found = key2Found || msg.user === 'efgh'; + + if (key1Found && key2Found) { + done(); + } + }; + + pushover.send(notify); + }); + +}); + +describe('announcement only pushover', function ( ) { + var env = { + extendedSettings: { + pushover: { + announcementKey: 'abcd' + , apiToken: '6789' + } + } + }; + + var pushover = require('../lib/plugins/pushover')(env); + + it('send push if announcement', function (done) { + + var notify = { + title: 'Warning, this is a test!' + , message: 'details details details details' + , level: levels.WARN + , pushoverSound: 'climb' + , plugin: {name: 'test'} + , isAnnouncement: true + }; + + pushover.sendAPIRequest = function mockedSendAPIRequest (msg) { + msg.title.should.equal(notify.title); + msg.priority.should.equal(2); + msg.sound.should.equal(notify.pushoverSound); + + done(); + }; + + pushover.send(notify); + }); + + it('not send push if not announcement and no user key', function (done) { + + var notify = { + title: 'Warning, this is a test!' + , message: 'details details details details' + , level: levels.WARN + , pushoverSound: 'climb' + , plugin: {name: 'test'} + }; + + pushover.sendAPIRequest = function failIfSend ( ) { + done(); + }; + + pushover.send(notify); + done(); + }); + +}); diff --git a/tests/rawbg.test.js b/tests/rawbg.test.js new file mode 100644 index 00000000000..135525ca149 --- /dev/null +++ b/tests/rawbg.test.js @@ -0,0 +1,35 @@ +'use strict'; + +require('should'); + +describe('Raw BG', function ( ) { + var rawbg = require('../lib/plugins/rawbg')(); + var sandbox = require('../lib/sandbox')(); + + var now = Date.now(); + var pluginBase = {}; + var data = { + sgvs: [{unfiltered: 113680, filtered: 111232, mgdl: 110, noise: 1, mills: now}] + , cals: [{scale: 1, intercept: 25717.82377004309, slope: 766.895601715918, mills: now}] + }; + var clientSettings = { + units: 'mg/dl' + }; + + it('should calculate Raw BG', function (done) { + var sbx = sandbox.clientInit(clientSettings, Date.now(), pluginBase, data); + + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('rawbg'); + var result = setter(); + result.mgdl.should.equal(113); + result.noiseLabel.should.equal('Clean'); + done(); + }; + + rawbg.setProperties(sbx); + + }); + + +}); diff --git a/tests/reports.test.js b/tests/reports.test.js new file mode 100644 index 00000000000..95d863a3d3f --- /dev/null +++ b/tests/reports.test.js @@ -0,0 +1,309 @@ +'use strict'; + +require('should'); +var _ = require('lodash'); +var benv = require('benv'); +var read = require('fs').readFileSync; +var serverSettings = require('./fixtures/default-server-settings'); + +var nowData = { + sgvs: [ + { mgdl: 100, mills: Date.now(), direction: 'Flat', type: 'sgv' } + ] +}; + +var someData = { + '/api/v1/entries.json?find[date][$gte]=1438992000000&find[date][$lt]=1439078400000&count=10000': [{'_id':'55c697f9459cf1fa5ed71cd8','unfiltered':213888,'filtered':218560,'direction':'Flat','device':'dexcom','rssi':172,'sgv':208,'dateString':'Sat Aug 08 16:58:44 PDT 2015','type':'sgv','date':1439078324000,'noise':1},{'_id':'55c696cc459cf1fa5ed71cd7','unfiltered':217952,'filtered':220864,'direction':'Flat','device':'dexcom','rssi':430,'sgv':212,'dateString':'Sat Aug 08 16:53:45 PDT 2015','type':'sgv','date':1439078025000,'noise':1},{'_id':'55c5d0c6459cf1fa5ed71a04','device':'dexcom','scale':1.1,'dateString':'Sat Aug 08 02:48:05 PDT 2015','date':1439027285000,'type':'cal','intercept':31102.323470336833,'slope':776.9097574914869},{'_id':'55c5d0c5459cf1fa5ed71a03','device':'dexcom','dateString':'Sat Aug 08 02:48:03 PDT 2015','mbg':120,'date':1439027283000,'type':'mbg'}], + '/api/v1/treatments.json?find[created_at][$gte]=2015-08-08T00:00:00.000Z&find[created_at][$lt]=2015-08-09T00:00:00.000Z': [{'enteredBy':'Dad','eventType':'Correction Bolus','glucose':201,'glucoseType':'Finger','insulin':0.65,'units':'mg/dl','created_at':'2015-08-08T23:22:00.000Z','_id':'55c695628a00a3c97a6611ed'},{'enteredBy':'Mom ','eventType':'Correction Bolus','glucose':163,'glucoseType':'Sensor','insulin':0.7,'units':'mg/dl','created_at':'2015-08-08T22:53:11.021Z','_id':'55c68857cd6dd2036036705f'}], + '/api/v1/entries.json?find[date][$gte]=1439078400000&find[date][$lt]=1439164800000&count=10000': [{'_id':'55c7e85f459cf1fa5ed71dc8','unfiltered':183520,'filtered':193120,'direction':'NOT COMPUTABLE','device':'dexcom','rssi':161,'sgv':149,'dateString':'Sun Aug 09 16:53:40 PDT 2015','type':'sgv','date':1439164420000,'noise':1},{'_id':'55c7e270459cf1fa5ed71dc7','unfiltered':199328,'filtered':192608,'direction':'Flat','device':'dexcom','rssi':161,'sgv':166,'dateString':'Sun Aug 09 16:28:40 PDT 2015','type':'sgv','date':1439162920000,'noise':1}], + '/api/v1/treatments.json?find[created_at][$gte]=2015-08-09T00:00:00.000Z&find[created_at][$lt]=2015-08-10T00:00:00.000Z': [{'enteredBy':'Dad','eventType':'Snack Bolus','carbs':18,'insulin':1.1,'created_at':'2015-08-09T22:41:56.253Z','_id':'55c7d734270fbd97191013c2'},{'enteredBy':'Dad','eventType':'Carb Correction','carbs':5,'created_at':'2015-08-09T21:39:13.995Z','_id':'55c7c881270fbd97191013b4'}], + '/api/v1/entries.json?find[date][$gte]=1439164800000&find[date][$lt]=1439251200000&count=10000': [{'_id':'55c93af4459cf1fa5ed71ecc','unfiltered':193248,'filtered':188384,'direction':'NOT COMPUTABLE','device':'dexcom','rssi':194,'sgv':193,'dateString':'Mon Aug 10 16:58:36 PDT 2015','type':'sgv','date':1439251116000,'noise':1},{'_id':'55c939d8459cf1fa5ed71ecb','unfiltered':189888,'filtered':184960,'direction':'NOT COMPUTABLE','device':'dexcom','rssi':931,'sgv':188,'dateString':'Mon Aug 10 16:53:38 PDT 2015','type':'sgv','date':1439250818000,'noise':1}], + '/api/v1/treatments.json?find[created_at][$gte]=2015-08-10T00:00:00.000Z&find[created_at][$lt]=2015-08-11T00:00:00.000Z': [{'enteredBy':'Mom ','eventType':'Snack Bolus','glucose':180,'glucoseType':'Sensor','carbs':18,'insulin':1.9,'units':'mg/dl','created_at':'2015-08-10T23:53:31.970Z','_id':'55c9397b865550df020e3560'},{'enteredBy':'Mom ','eventType':'Meal Bolus','glucose':140,'glucoseType':'Finger','carbs':50,'insulin':3.4,'units':'mg/dl','created_at':'2015-08-10T20:41:23.516Z','_id':'55c90c73865550df020e3539'}], + '/api/v1/entries.json?find[date][$gte]=1439251200000&find[date][$lt]=1439337600000&count=10000': [{'_id':'55ca8c6e459cf1fa5ed71fe2','unfiltered':174080,'filtered':184576,'direction':'FortyFiveDown','device':'dexcom','rssi':169,'sgv':156,'dateString':'Tue Aug 11 16:58:32 PDT 2015','type':'sgv','date':1439337512000,'noise':1},{'_id':'55ca8b42459cf1fa5ed71fe1','unfiltered':180192,'filtered':192768,'direction':'FortyFiveDown','device':'dexcom','rssi':182,'sgv':163,'dateString':'Tue Aug 11 16:53:32 PDT 2015','type':'sgv','date':1439337212000,'noise':1}], + '/api/v1/treatments.json?find[created_at][$gte]=2015-08-11T00:00:00.000Z&find[created_at][$lt]=2015-08-12T00:00:00.000Z': [{'created_at':'2015-08-11T23:37:00.000Z','eventType':'Snack Bolus','carbs':18,'_id':'55ca8644ca3c57683d19c211'},{'enteredBy':'Mom ','eventType':'Snack Bolus','glucose':203,'glucoseType':'Sensor','insulin':1,'preBolus':15,'units':'mg/dl','created_at':'2015-08-11T23:22:00.000Z','_id':'55ca8644ca3c57683d19c210'}], + '/api/v1/entries.json?find[date][$gte]=1439337600000&find[date][$lt]=1439424000000&count=10000': [{'_id':'55cbddee38a8d88ad1b48647','unfiltered':165760,'filtered':167488,'direction':'Flat','device':'dexcom','rssi':165,'sgv':157,'dateString':'Wed Aug 12 16:58:28 PDT 2015','type':'sgv','date':1439423908000,'noise':1},{'_id':'55cbdccc38a8d88ad1b48644','unfiltered':167456,'filtered':169312,'direction':'Flat','device':'dexcom','rssi':168,'sgv':159,'dateString':'Wed Aug 12 16:53:28 PDT 2015','type':'sgv','date':1439423608000,'noise':1}], + '/api/v1/treatments.json?find[created_at][$gte]=2015-08-12T00:00:00.000Z&find[created_at][$lt]=2015-08-13T00:00:00.000Z': [{'enteredBy':'Dad','eventType':'Correction Bolus','insulin':0.8,'created_at':'2015-08-12T23:21:08.907Z','_id':'55cbd4e47e726599048a3f91'},{'enteredBy':'Dad','eventType':'Note','notes':'Milk now','created_at':'2015-08-12T21:23:00.000Z','_id':'55cbba4e7e726599048a3f79'}], + '/api/v1/entries.json?find[date][$gte]=1439424000000&find[date][$lt]=1439510400000&count=10000': [{'_id':'55cd2f6738a8d88ad1b48ca1','unfiltered':209792,'filtered':229344,'direction':'SingleDown','device':'dexcom','rssi':436,'sgv':205,'dateString':'Thu Aug 13 16:58:24 PDT 2015','type':'sgv','date':1439510304000,'noise':1},{'_id':'55cd2e3b38a8d88ad1b48c95','unfiltered':220928,'filtered':237472,'direction':'FortyFiveDown','device':'dexcom','rssi':418,'sgv':219,'dateString':'Thu Aug 13 16:53:24 PDT 2015','type':'sgv','date':1439510004000,'noise':1}], + '/api/v1/treatments.json?find[created_at][$gte]=2015-08-13T00:00:00.000Z&find[created_at][$lt]=2015-08-14T00:00:00.000Z': [{'enteredBy':'Mom ','eventType':'Correction Bolus','glucose':250,'glucoseType':'Sensor','insulin':0.75,'units':'mg/dl','created_at':'2015-08-13T23:45:56.927Z','_id':'55cd2c3497fa97ac5d8bc53b'},{'enteredBy':'Mom ','eventType':'Correction Bolus','glucose':198,'glucoseType':'Sensor','insulin':1.1,'units':'mg/dl','created_at':'2015-08-13T23:11:00.293Z','_id':'55cd240497fa97ac5d8bc535'}], + '/api/v1/entries.json?find[date][$gte]=1439510400000&find[date][$lt]=1439596800000&count=10000': [{'_id':'55ce80e338a8d88ad1b49397','unfiltered':179936,'filtered':202080,'direction':'SingleDown','device':'dexcom','rssi':179,'sgv':182,'dateString':'Fri Aug 14 16:58:20 PDT 2015','type':'sgv','date':1439596700000,'noise':1},{'_id':'55ce7fb738a8d88ad1b4938d','unfiltered':192288,'filtered':213792,'direction':'SingleDown','device':'dexcom','rssi':180,'sgv':197,'dateString':'Fri Aug 14 16:53:20 PDT 2015','type':'sgv','date':1439596400000,'noise':1}], + '/api/v1/treatments.json?find[created_at][$gte]=2015-08-14T00:00:00.000Z&find[created_at][$lt]=2015-08-15T00:00:00.000Z': [{'enteredBy':'Dad','eventType':'Site Change','glucose':268,'glucoseType':'Finger','insulin':1.75,'units':'mg/dl','created_at':'2015-08-14T00:00:00.000Z','_id':'55ce78fe925aa80e7071e5d6'},{'enteredBy':'Mom ','eventType':'Meal Bolus','glucose':89,'glucoseType':'Finger','carbs':54,'insulin':3.15,'units':'mg/dl','created_at':'2015-08-14T21:00:00.000Z','_id':'55ce59bb925aa80e7071e5ba'}], + '/api/v1/entries.json?find[date][$gte]=1439596800000&find[date][$lt]=1439683200000&count=10000': [{'_id':'55cfd25f38a8d88ad1b49931','unfiltered':283136,'filtered':304768,'direction':'SingleDown','device':'dexcom','rssi':185,'sgv':306,'dateString':'Sat Aug 15 16:58:16 PDT 2015','type':'sgv','date':1439683096000,'noise':1},{'_id':'55cfd13338a8d88ad1b4992e','unfiltered':302528,'filtered':312576,'direction':'FortyFiveDown','device':'dexcom','rssi':179,'sgv':329,'dateString':'Sat Aug 15 16:53:16 PDT 2015','type':'sgv','date':1439682796000,'noise':1}], + '/api/v1/food/regular.json': [{'_id':'552ece84a6947ea011db35bb','type':'food','category':'Zakladni','subcategory':'Sladkosti','name':'Bebe male','portion':18,'carbs':12,'gi':1,'unit':'pcs','created_at':'2015-04-15T20:48:04.966Z'}], + '/api/v1/treatments.json?find[eventType]=/BG Check/i&find[created_at][$gte]=2015-08-08T00:00:00.000Z&find[created_at][$lt]=2015-09-07T23:59:59.000Z': [ + {'created_at':'2015-08-08T00:00:00.000Z'}, + {'created_at':'2015-08-09T00:00:00.000Z'}, + {'created_at':'2015-08-10T00:00:00.000Z'}, + {'created_at':'2015-08-11T00:00:00.000Z'}, + {'created_at':'2015-08-12T00:00:00.000Z'}, + {'created_at':'2015-08-13T00:00:00.000Z'}, + {'created_at':'2015-08-14T00:00:00.000Z'}, + {'created_at':'2015-08-15T00:00:00.000Z'}, + {'created_at':'2015-08-16T00:00:00.000Z'}, + {'created_at':'2015-08-17T00:00:00.000Z'}, + {'created_at':'2015-08-18T00:00:00.000Z'}, + {'created_at':'2015-08-19T00:00:00.000Z'}, + {'created_at':'2015-08-20T00:00:00.000Z'}, + {'created_at':'2015-08-21T00:00:00.000Z'}, + {'created_at':'2015-08-22T00:00:00.000Z'}, + {'created_at':'2015-08-23T00:00:00.000Z'}, + {'created_at':'2015-08-24T00:00:00.000Z'}, + {'created_at':'2015-08-25T00:00:00.000Z'}, + {'created_at':'2015-08-26T00:00:00.000Z'}, + {'created_at':'2015-08-27T00:00:00.000Z'}, + {'created_at':'2015-08-28T00:00:00.000Z'}, + {'created_at':'2015-08-29T00:00:00.000Z'}, + {'created_at':'2015-08-30T00:00:00.000Z'}, + {'created_at':'2015-08-31T00:00:00.000Z'}, + {'created_at':'2015-09-01T00:00:00.000Z'}, + {'created_at':'2015-09-02T00:00:00.000Z'}, + {'created_at':'2015-09-03T00:00:00.000Z'}, + {'created_at':'2015-09-04T00:00:00.000Z'}, + {'created_at':'2015-09-05T00:00:00.000Z'}, + {'created_at':'2015-09-06T00:00:00.000Z'}, + {'created_at':'2015-09-07T00:00:00.000Z'} + ], + '/api/v1/treatments.json?find[notes]=/something/i&find[created_at][$gte]=2015-08-08T00:00:00.000Z&find[created_at][$lt]=2015-09-07T23:59:59.000Z': [ + {'created_at':'2015-08-08T00:00:00.000Z'}, + {'created_at':'2015-08-09T00:00:00.000Z'}, + {'created_at':'2015-08-10T00:00:00.000Z'}, + {'created_at':'2015-08-11T00:00:00.000Z'}, + {'created_at':'2015-08-12T00:00:00.000Z'}, + {'created_at':'2015-08-13T00:00:00.000Z'}, + {'created_at':'2015-08-14T00:00:00.000Z'}, + {'created_at':'2015-08-15T00:00:00.000Z'}, + {'created_at':'2015-08-16T00:00:00.000Z'}, + {'created_at':'2015-08-17T00:00:00.000Z'}, + {'created_at':'2015-08-18T00:00:00.000Z'}, + {'created_at':'2015-08-19T00:00:00.000Z'}, + {'created_at':'2015-08-20T00:00:00.000Z'}, + {'created_at':'2015-08-21T00:00:00.000Z'}, + {'created_at':'2015-08-22T00:00:00.000Z'}, + {'created_at':'2015-08-23T00:00:00.000Z'}, + {'created_at':'2015-08-24T00:00:00.000Z'}, + {'created_at':'2015-08-25T00:00:00.000Z'}, + {'created_at':'2015-08-26T00:00:00.000Z'}, + {'created_at':'2015-08-27T00:00:00.000Z'}, + {'created_at':'2015-08-28T00:00:00.000Z'}, + {'created_at':'2015-08-29T00:00:00.000Z'}, + {'created_at':'2015-08-30T00:00:00.000Z'}, + {'created_at':'2015-08-31T00:00:00.000Z'}, + {'created_at':'2015-09-01T00:00:00.000Z'}, + {'created_at':'2015-09-02T00:00:00.000Z'}, + {'created_at':'2015-09-03T00:00:00.000Z'}, + {'created_at':'2015-09-04T00:00:00.000Z'}, + {'created_at':'2015-09-05T00:00:00.000Z'}, + {'created_at':'2015-09-06T00:00:00.000Z'}, + {'created_at':'2015-09-07T00:00:00.000Z'} + ] + }; + +var exampleProfile = [ + { + //General values + 'dia':3, + + // Simple style values, 'from' are in minutes from midnight + 'carbratio': [ + { + 'time': '00:00', + 'value': 30 + }], + 'carbs_hr':30, + 'delay': 20, + 'sens': [ + { + 'time': '00:00', + 'value': 100 + } + , { + 'time': '8:00', + 'value': 80 + }], + 'startDate': new Date(), + 'timezone': 'UTC', + + //perGIvalues style values + 'perGIvalues': false, + 'carbs_hr_high': 30, + 'carbs_hr_medium': 30, + 'carbs_hr_low': 30, + 'delay_high': 15, + 'delay_medium': 20, + 'delay_low': 20, + + 'basal':[ + { + 'time': '00:00', + 'value': 0.1 + }], + 'target_low':[ + { + 'time': '00:00', + 'value': 0 + }], + 'target_high':[ + { + 'time': '00:00', + 'value': 0 + }] + } +]; + +exampleProfile[0].startDate.setSeconds(0); +exampleProfile[0].startDate.setMilliseconds(0); + + +describe('reports', function ( ) { + var self = this; + + before(function (done) { + benv.setup(function() { + self.$ = require('jquery'); + self.$.localStorage = require('./fixtures/localstorage'); + + self.$.fn.tipsy = function mockTipsy ( ) { }; + + self.$.fn.dialog = function mockDialog (opts) { + function maybeCall (name, obj) { + if (obj[name] && obj[name].call) { + obj[name](); + } + + } + maybeCall('open', opts); + + _.forEach(opts.buttons, function (button) { + maybeCall('click', button); + }); + }; + + var indexHtml = read(__dirname + '/../static/report/index.html', 'utf8'); + self.$('body').html(indexHtml); + + //var filesys = require('fs'); + //var logfile = filesys.createWriteStream('out.txt', { flags: 'a'} ) + + self.$.ajax = function mockAjax (url, opts) { + //logfile.write(url+'\n'); + return { + done: function mockDone (fn) { + if (opts && opts.success && opts.success.call) { + if (someData[url]) { + //console.log('+++++Data for ' + url + ' sent'); + opts.success(someData[url]); + } else { + //console.log('-----Data for ' + url + ' missing'); + opts.success([]); + } + } + fn(); + return self.$.ajax(); + }, + fail: function mockFail (fn) { + fn({status: 400}); + return self.$.ajax(); + } + }; + }; + + self.$.plot = function mockPlot () { + }; + + var d3 = require('d3'); + //disable all d3 transitions so most of the other code can run with jsdom + d3.timer = function mockTimer() { }; + + benv.expose({ + $: self.$ + , jQuery: self.$ + , d3: d3 + , serverSettings: serverSettings + , io: { + connect: function mockConnect ( ) { + return { + on: function mockOn ( ) { } + }; + } + } + }); + + benv.require(__dirname + '/../bundle/bundle.source.js'); + benv.require(__dirname + '/../static/report/js/report.js'); + + done(); + }); + }); + + after(function (done) { + benv.teardown(true); + done(); + }); + + it ('should produce some html', function (done) { + var plugins = require('../lib/plugins/')().registerClientDefaults(); + var client = require('../lib/client'); + + var hashauth = require('../lib/hashauth'); + hashauth.init(client,$); + hashauth.verifyAuthentication = function mockVerifyAuthentication(next) { + hashauth.authenticated = true; + next(true); + }; + + window.confirm = function mockConfirm () { + return true; + }; + + window.alert = function mockAlert () { + return true; + }; + + client.init(serverSettings, plugins); + client.dataUpdate(nowData); + + // Load profile, we need to operate in UTC + client.sbx.data.profile.loadData(exampleProfile); + + $('a.presetdates :first').click(); + $('#rp_notes').val('something'); + $('#rp_eventtype').val('BG Check'); + $('#rp_from').val('2015/08/08'); + $('#rp_to').val('2015/09/07'); + $('#rp_optionsraw').prop('checked',true); + $('#rp_optionsiob').prop('checked',true); + $('#rp_optionscob').prop('checked',true); + $('#rp_enableeventtype').click(); + $('#rp_enablenotes').click(); + $('#rp_enablefood').click(); + $('#rp_enablefood').click(); + $('#rp_log').prop('checked',true); + $('#rp_show').click(); + + $('#rp_linear').prop('checked',true); + $('#rp_show').click(); + $('#dailystats').click(); + + $('img.deleteTreatment:first').click(); + $('img.editTreatment:first').click(); + $('.ui-button:contains("Save")').click(); + + var result = $('body').html(); + //var filesys = require('fs'); + //var logfile = filesys.createWriteStream('out.txt', { flags: 'a'} ) + //logfile.write($('body').html()); + + //console.log(result); + + result.indexOf('Milk now').should.be.greaterThan(-1); // daytoday + result.indexOf('50 g (1.67U)').should.be.greaterThan(-1); // daytoday + result.indexOf('0%100%0%28.3').should.be.greaterThan(-1); //dailystats + result.indexOf('td class="tdborder" style="background-color:#8f8">Normal: 38%68.78.80.2').should.be.greaterThan(-1); // distribution + result.indexOf('16 (100%)118.38.910.611.718.32.7').should.be.greaterThan(-1); // hourlystats + result.indexOf('
').should.be.greaterThan(-1); //success + result.indexOf('CAL: Scale: 1.10 Intercept: 31102 Slope: 776.91').should.be.greaterThan(-1); //calibrations + result.indexOf('Correction Bolus250 (Sensor)0.75Mom ').should.be.greaterThan(-1); //treatments + + done(); + }); + +}); diff --git a/tests/sandbox.test.js b/tests/sandbox.test.js new file mode 100644 index 00000000000..b6c491d85a6 --- /dev/null +++ b/tests/sandbox.test.js @@ -0,0 +1,85 @@ +var should = require('should'); + +describe('sandbox', function ( ) { + var sandbox = require('../lib/sandbox')(); + + var now = Date.now(); + + it('init on client', function (done) { + var clientSettings = { + units: 'mg/dl' + , thresholds:{ + bgHigh: 260 + , bgTargetTop: 180 + , bgTargetBottom: 80 + , bgLow: 55 + } + }; + + var pluginBase = {}; + var data = {sgvs: [{mgdl: 100, mills: now}]}; + + var sbx = sandbox.clientInit(clientSettings, Date.now(), pluginBase, data); + + sbx.pluginBase.should.equal(pluginBase); + sbx.data.should.equal(data); + sbx.lastSGVMgdl().should.equal(100); + + done(); + }); + + function createServerSandbox() { + var env = require('../env')(); + var ctx = {}; + ctx.data = require('../lib/data')(env, ctx); + ctx.notifications = require('../lib/notifications')(env, ctx); + + return sandbox.serverInit(env, ctx); + } + + it('init on server', function (done) { + var sbx = createServerSandbox(); + sbx.data.sgvs = [{mgdl: 100, mills: now}]; + + should.exist(sbx.notifications.requestNotify); + should.not.exist(sbx.notifications.process); + should.not.exist(sbx.notifications.ack); + sbx.lastSGVMgdl().should.equal(100); + + done(); + }); + + it('display 39 as LOW and 401 as HIGH', function () { + var sbx = createServerSandbox(); + + sbx.displayBg({mgdl: 39}).should.equal('LOW'); + sbx.displayBg({mgdl: '39'}).should.equal('LOW'); + sbx.displayBg({mgdl: 401}).should.equal('HIGH'); + sbx.displayBg({mgdl: '401'}).should.equal('HIGH'); + }); + + it('build BG Now line using properties', function ( ) { + var sbx = createServerSandbox(); + sbx.data.sgvs = [{mgdl: 99, mills: now}]; + sbx.properties = { delta: {display: '+5' }, direction: {value: 'FortyFiveUp', label: '↗', entity: '↗'} }; + + sbx.buildBGNowLine().should.equal('BG Now: 99 +5 ↗ mg/dl'); + + }); + + it('build default message using properties', function ( ) { + var sbx = createServerSandbox(); + sbx.data.sgvs = [{mgdl: 99, mills: now}]; + sbx.properties = { + delta: {display: '+5' } + , direction: {value: 'FortyFiveUp', label: '↗', entity: '↗'} + , rawbg: {displayLine: 'Raw BG: 100 mg/dl'} + , iob: {displayLine: 'IOB: 1.25U'} + , cob: {displayLine: 'COB: 15g'} + }; + + sbx.buildDefaultMessage().should.equal('BG Now: 99 +5 ↗ mg/dl\nRaw BG: 100 mg/dl\nIOB: 1.25U\nCOB: 15g'); + + }); + +}); diff --git a/tests/security.test.js b/tests/security.test.js index 42cfa4215e2..7f692b6a31f 100644 --- a/tests/security.test.js +++ b/tests/security.test.js @@ -6,24 +6,18 @@ var load = require('./fixtures/load'); describe('API_SECRET', function ( ) { var api = require('../lib/api/'); - api.should.be.ok; var scope = this; function setup_app (env, fn) { - var ctx = { }; - ctx.wares = require('../lib/middleware/')(env); - ctx.store = require('../lib/storage')(env); - ctx.archive = require('../lib/entries').storage(env.mongo_collection, ctx.store); - ctx.settings = require('../lib/settings')(env.settings_collection, ctx.store); - - ctx.store(function ( ) { - ctx.app = api(env, ctx.wares, ctx.archive, ctx.settings); + require('../lib/bootevent')(env).boot(function booted (ctx) { + var wares = require('../lib/middleware/')(env); + ctx.app = api(env, wares, ctx); scope.app = ctx.app; - ctx.archive.create(load('json'), fn); - scope.archive = ctx.archive; + scope.entries = ctx.entries; + ctx.entries.create(load('json'), function () { + fn(ctx); + }); }); - - return ctx; } /* before(function (done) { @@ -31,15 +25,16 @@ describe('API_SECRET', function ( ) { }); */ after(function (done) { - scope.archive( ).remove({ }, done); + scope.entries( ).remove({ }, done); }); it('should work fine absent', function (done) { delete process.env.API_SECRET; var env = require('../env')( ); should.not.exist(env.api_secret); - var ctx = setup_app(env, function ( ) { - ctx.app.enabled('api').should.be.false; + setup_app(env, function (ctx) { + + ctx.app.enabled('api').should.equal(false); ping_status(ctx.app, again); function again ( ) { ping_authorized_endpoint(ctx.app, 404, done); @@ -54,9 +49,9 @@ describe('API_SECRET', function ( ) { process.env.API_SECRET = 'this is my long pass phrase'; var env = require('../env')( ); env.api_secret.should.equal(known); - var ctx = setup_app(env, function ( ) { + setup_app(env, function (ctx) { // console.log(this.app.enabled('api')); - ctx.app.enabled('api').should.be.true; + ctx.app.enabled('api').should.equal(true); // ping_status(ctx.app, done); // ping_authorized_endpoint(ctx.app, 200, done); ping_status(ctx.app, again); @@ -75,9 +70,9 @@ describe('API_SECRET', function ( ) { process.env.API_SECRET = 'this is my long pass phrase'; var env = require('../env')( ); env.api_secret.should.equal(known); - var ctx = setup_app(env, function ( ) { + setup_app(env, function (ctx) { // console.log(this.app.enabled('api')); - ctx.app.enabled('api').should.be.true; + ctx.app.enabled('api').should.equal(true); // ping_status(ctx.app, done); // ping_authorized_endpoint(ctx.app, 200, done); ping_status(ctx.app, again); @@ -90,7 +85,6 @@ describe('API_SECRET', function ( ) { }); it('should not work short', function ( ) { - var known = 'c1d117818a97e847bdf286aa02d9dc8e8f7148f5'; delete process.env.API_SECRET; process.env.API_SECRET = 'tooshort'; var env; @@ -109,7 +103,7 @@ describe('API_SECRET', function ( ) { res.body.status.should.equal('ok'); fn( ); // console.log('err', err, 'res', res); - }) + }); } function ping_authorized_endpoint (app, fails, fn) { @@ -123,7 +117,7 @@ describe('API_SECRET', function ( ) { } fn( ); // console.log('err', err, 'res', res); - }) + }); } }); diff --git a/tests/settings.test.js b/tests/settings.test.js new file mode 100644 index 00000000000..7183fae1bc1 --- /dev/null +++ b/tests/settings.test.js @@ -0,0 +1,237 @@ +'use strict'; + +var _ = require('lodash'); +var should = require('should'); +var levels = require('../lib/levels'); + +describe('settings', function ( ) { + var settings = require('../lib/settings')(); + + it('have defaults ready', function () { + settings.timeFormat.should.equal(12); + settings.nightMode.should.equal(false); + settings.showRawbg.should.equal('never'); + settings.customTitle.should.equal('Nightscout'); + settings.theme.should.equal('default'); + settings.alarmUrgentHigh.should.equal(true); + settings.alarmUrgentHighMins.should.eql([30, 60, 90, 120]); + settings.alarmHigh.should.equal(true); + settings.alarmHighMins.should.eql([30, 60, 90, 120]); + settings.alarmLow.should.equal(true); + settings.alarmLowMins.should.eql([15, 30, 45, 60]); + settings.alarmUrgentLow.should.equal(true); + settings.alarmUrgentLowMins.should.eql([15, 30, 45]); + settings.alarmUrgentMins.should.eql([30, 60, 90, 120]); + settings.alarmWarnMins.should.eql([30, 60, 90, 120]); + settings.alarmTimeagoWarn.should.equal(true); + settings.alarmTimeagoWarnMins.should.equal(15); + settings.alarmTimeagoUrgent.should.equal(true); + settings.alarmTimeagoUrgentMins.should.equal(30); + settings.language.should.equal('en'); + settings.showPlugins.should.equal(''); + }); + + it('support setting from env vars', function () { + var expected = [ + 'ENABLE' + , 'DISABLE' + , 'UNITS' + , 'TIME_FORMAT' + , 'NIGHT_MODE' + , 'SHOW_RAWBG' + , 'CUSTOM_TITLE' + , 'THEME' + , 'ALARM_TYPES' + , 'ALARM_URGENT_HIGH' + , 'ALARM_HIGH' + , 'ALARM_LOW' + , 'ALARM_URGENT_LOW' + , 'ALARM_TIMEAGO_WARN' + , 'ALARM_TIMEAGO_WARN_MINS' + , 'ALARM_TIMEAGO_URGENT' + , 'ALARM_TIMEAGO_URGENT_MINS' + , 'LANGUAGE' + , 'SHOW_PLUGINS' + , 'BG_HIGH' + , 'BG_TARGET_TOP' + , 'BG_TARGET_BOTTOM' + , 'BG_LOW' + ]; + + expected.length.should.equal(23); + + var seen = { }; + settings.eachSettingAsEnv(function markSeenNames(name) { + seen[name] = true; + }); + + + var expectedAndSeen = _.filter(expected, function (name) { + return seen[name]; + }); + + expectedAndSeen.length.should.equal(expected.length); + }); + + it('support setting each', function () { + var expected = [ + 'enable' + , 'disable' + , 'units' + , 'timeFormat' + , 'nightMode' + , 'showRawbg' + , 'customTitle' + , 'theme' + , 'alarmTypes' + , 'alarmUrgentHigh' + , 'alarmHigh' + , 'alarmLow' + , 'alarmUrgentLow' + , 'alarmTimeagoWarn' + , 'alarmTimeagoWarnMins' + , 'alarmTimeagoUrgent' + , 'alarmTimeagoUrgentMins' + , 'language' + , 'showPlugins' + ]; + + expected.length.should.equal(19); + + var seen = { }; + settings.eachSetting(function markSeenNames(name) { + seen[name] = true; + }); + + + var expectedAndSeen = _.filter(expected, function (name) { + return seen[name]; + }); + + expectedAndSeen.length.should.equal(expected.length); + + }); + + it('have default features', function () { + var fresh = require('../lib/settings')(); + fresh.eachSettingAsEnv(function () { + return undefined; + }); + + _.each(fresh.DEFAULT_FEATURES, function eachDefault (feature) { + fresh.enable.should.containEql(feature); + }); + + }); + + it('support disabling default features', function () { + var fresh = require('../lib/settings')(); + fresh.eachSettingAsEnv(function (name) { + return name === 'DISABLE' ? + fresh.DEFAULT_FEATURES.join(' ') + ' ar2' //need to add ar2 here since it will be auto enabled + : undefined; + }); + + fresh.enable.length.should.equal(0); + }); + + it('parse custom snooze mins', function () { + var userSetting = { + ALARM_URGENT_LOW_MINS: '5 10 15' + }; + + var fresh = require('../lib/settings')(); + fresh.eachSettingAsEnv(function (name) { + return userSetting[name]; + }); + + fresh.alarmUrgentLowMins.should.eql([5, 10, 15]); + + fresh.snoozeMinsForAlarmEvent({eventName: 'low', level: levels.URGENT}).should.eql([5, 10, 15]); + fresh.snoozeFirstMinsForAlarmEvent({eventName: 'low', level: levels.URGENT}).should.equal(5); + }); + + it('set thresholds', function () { + var userThresholds = { + BG_HIGH: '200' + , BG_TARGET_TOP: '170' + , BG_TARGET_BOTTOM: '70' + , BG_LOW: '60' + }; + + var fresh = require('../lib/settings')(); + fresh.eachSettingAsEnv(function (name) { + return userThresholds[name]; + }); + + fresh.thresholds.bgHigh.should.equal(200); + fresh.thresholds.bgTargetTop.should.equal(170); + fresh.thresholds.bgTargetBottom.should.equal(70); + fresh.thresholds.bgLow.should.equal(60); + + should.deepEqual(fresh.alarmTypes, ['simple']); + }); + + it('default to predict if no thresholds are set', function () { + var fresh = require('../lib/settings')(); + fresh.eachSettingAsEnv(function ( ) { + return undefined; + }); + + should.deepEqual(fresh.alarmTypes, ['predict']); + }); + + it('ignore junk alarm types', function () { + var fresh = require('../lib/settings')(); + fresh.eachSettingAsEnv(function (name) { + return name === 'ALARM_TYPES' ? 'beep bop' : undefined; + }); + + should.deepEqual(fresh.alarmTypes, ['predict']); + }); + + it('allow multiple alarm types to be set', function () { + var fresh = require('../lib/settings')(); + fresh.eachSettingAsEnv(function (name) { + return name === 'ALARM_TYPES' ? 'predict simple' : undefined; + }); + + should.deepEqual(fresh.alarmTypes, ['predict', 'simple']); + }); + + it('handle screwed up thresholds in a way that will display something that looks wrong', function () { + var screwedUp = { + BG_HIGH: '89' + , BG_TARGET_TOP: '90' + , BG_TARGET_BOTTOM: '95' + , BG_LOW: '96' + }; + + var fresh = require('../lib/settings')(); + fresh.eachSettingAsEnv(function (name) { + return screwedUp[name]; + }); + + fresh.thresholds.bgHigh.should.equal(91); + fresh.thresholds.bgTargetTop.should.equal(90); + fresh.thresholds.bgTargetBottom.should.equal(89); + fresh.thresholds.bgLow.should.equal(88); + + should.deepEqual(fresh.alarmTypes, ['simple']); + }); + + it('check if a feature isEnabled', function () { + var fresh = require('../lib/settings')(); + fresh.enable = ['feature1']; + fresh.isEnabled('feature1').should.equal(true); + fresh.isEnabled('feature2').should.equal(false); + }); + + it('check if any listed feature isEnabled', function () { + var fresh = require('../lib/settings')(); + fresh.enable = ['feature1']; + fresh.isEnabled(['unknown', 'feature1']).should.equal(true); + fresh.isEnabled(['unknown', 'feature2']).should.equal(false); + }); + +}); diff --git a/tests/simplealarms.test.js b/tests/simplealarms.test.js new file mode 100644 index 00000000000..845bb1bd73a --- /dev/null +++ b/tests/simplealarms.test.js @@ -0,0 +1,76 @@ +var should = require('should'); +var levels = require('../lib/levels'); + +describe('simplealarms', function ( ) { + + var simplealarms = require('../lib/plugins/simplealarms')(); + var delta = require('../lib/plugins/delta')(); + + var env = require('../env')(); + var ctx = {}; + ctx.data = require('../lib/data')(env, ctx); + ctx.notifications = require('../lib/notifications')(env, ctx); + + var now = Date.now(); + var before = now - (5 * 60 * 1000); + + + it('Not trigger an alarm when in range', function (done) { + ctx.notifications.initRequests(); + ctx.data.sgvs = [{mills: now, mgdl: 100}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + simplealarms.checkNotifications(sbx); + should.not.exist(ctx.notifications.findHighestAlarm()); + + done(); + }); + + it('should trigger a warning when above target', function (done) { + ctx.notifications.initRequests(); + ctx.data.sgvs = [{mills: before, mgdl: 171}, {mills: now, mgdl: 181}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + delta.setProperties(sbx); + simplealarms.checkNotifications(sbx); + var highest = ctx.notifications.findHighestAlarm(); + highest.level.should.equal(levels.WARN); + highest.message.should.equal('BG Now: 181 +10 mg/dl'); + done(); + }); + + it('should trigger a urgent alarm when really high', function (done) { + ctx.notifications.initRequests(); + ctx.data.sgvs = [{mills: now, mgdl: 400}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + simplealarms.checkNotifications(sbx); + ctx.notifications.findHighestAlarm().level.should.equal(levels.URGENT); + + done(); + }); + + it('should trigger a warning when below target', function (done) { + ctx.notifications.initRequests(); + ctx.data.sgvs = [{mills: now, mgdl: 70}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + simplealarms.checkNotifications(sbx); + ctx.notifications.findHighestAlarm().level.should.equal(levels.WARN); + + done(); + }); + + it('should trigger a urgent alarm when really low', function (done) { + ctx.notifications.initRequests(); + ctx.data.sgvs = [{mills: now, mgdl: 40}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + simplealarms.checkNotifications(sbx); + ctx.notifications.findHighestAlarm().level.should.equal(levels.URGENT); + + done(); + }); + + +}); \ No newline at end of file diff --git a/tests/storage.test.js b/tests/storage.test.js new file mode 100644 index 00000000000..de9d265325a --- /dev/null +++ b/tests/storage.test.js @@ -0,0 +1,55 @@ +'use strict'; + +var should = require('should'); +var assert = require('assert'); + +describe('STORAGE', function () { + var env = require('../env')(); + + before(function (done) { + delete env.api_secret; + done(); + }); + + it('The storage class should be OK.', function (done) { + should.exist(require('../lib/storage')); + done(); + }); + + it('After initializing the storage class it should re-use the open connection', function (done) { + var store = require('../lib/storage'); + store(env, function (err1, db1) { + should.not.exist(err1); + + store(env, function (err2, db2) { + should.not.exist(err2); + assert(db1.db, db2.db, 'Check if the handlers are the same.'); + + done(); + }); + }); + }); + + it('When no connection-string is given the storage-class should throw an error.', function (done) { + delete env.mongo; + should.not.exist(env.mongo); + + (function () { + return require('../lib/storage')(env, false, true); + }).should.throw('MongoDB connection string is missing'); + + done(); + }); + + it('An invalid connection-string should throw an error.', function (done) { + env.mongo = 'This is not a MongoDB connection-string'; + + (function () { + return require('../lib/storage')(env, false, true); + }).should.throw('URL must be in the format mongodb://user:pass@host:port/dbname'); + + done(); + }); + +}); + diff --git a/tests/times.test.js b/tests/times.test.js new file mode 100644 index 00000000000..5bb52bb39f5 --- /dev/null +++ b/tests/times.test.js @@ -0,0 +1,30 @@ +'use strict'; + +require('should'); + +describe('times', function ( ) { + var times = require('../lib/times'); + + it('hours to mins, secs, and msecs', function () { + times.hour().mins.should.equal(60); + times.hour().secs.should.equal(3600); + times.hour().msecs.should.equal(3600000); + times.hours(3).mins.should.equal(180); + times.hours(3).secs.should.equal(10800); + times.hours(3).msecs.should.equal(10800000); + }); + + it('mins to secs and msecs', function () { + times.min().secs.should.equal(60); + times.min().msecs.should.equal(60000); + times.mins(2).secs.should.equal(120); + times.mins(2).msecs.should.equal(120000); + }); + + it('secs as msecs', function () { + times.sec().msecs.should.equal(1000); + times.secs(15).msecs.should.equal(15000); + }); + + +}); diff --git a/tests/treatmentnotify.test.js b/tests/treatmentnotify.test.js new file mode 100644 index 00000000000..8803d87abd4 --- /dev/null +++ b/tests/treatmentnotify.test.js @@ -0,0 +1,126 @@ +var _ = require('lodash'); +var should = require('should'); +var levels = require('../lib/levels'); + +describe('treatmentnotify', function ( ) { + + var treatmentnotify = require('../lib/plugins/treatmentnotify')(); + + var env = require('../env')(); + var ctx = {}; + ctx.data = require('../lib/data')(env, ctx); + ctx.notifications = require('../lib/notifications')(env, ctx); + + var now = Date.now(); + + it('Request a snooze for a recent treatment and request an info notify', function (done) { + ctx.notifications.initRequests(); + ctx.data.sgvs = [{mills: now, mgdl: 100}]; + ctx.data.treatments = [{eventType: 'BG Check', glucose: '100', mills: now}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + treatmentnotify.checkNotifications(sbx); + should.not.exist(ctx.notifications.findHighestAlarm()); + should.exist(ctx.notifications.snoozedBy({level: levels.URGENT})); + + _.first(ctx.notifications.findUnSnoozeable()).level.should.equal(levels.INFO); + + done(); + }); + + it('Not Request a snooze for an older treatment and not request an info notification', function (done) { + ctx.notifications.initRequests(); + ctx.data.sgvs = [{mills: now, mgdl: 100}]; + ctx.data.treatments = [{mills: now - (15 * 60 * 1000)}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + treatmentnotify.checkNotifications(sbx); + should.not.exist(ctx.notifications.findHighestAlarm()); + should.exist(ctx.notifications.snoozedBy({level: levels.URGENT})); + + should.not.exist(_.first(ctx.notifications.findUnSnoozeable())); + + done(); + }); + + it('Request a snooze for a recent calibration and request an info notify', function (done) { + ctx.notifications.initRequests(); + ctx.data.sgvs = [{mills: now, mgdl: 100}]; + ctx.data.mbgs = [{mgdl: '100', mills: now}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + treatmentnotify.checkNotifications(sbx); + should.not.exist(ctx.notifications.findHighestAlarm()); + should.exist(ctx.notifications.snoozedBy({level: levels.URGENT})); + + _.first(ctx.notifications.findUnSnoozeable()).level.should.equal(levels.INFO); + + done(); + }); + + it('Not Request a snooze for an older calibration treatment and not request an info notification', function (done) { + ctx.notifications.initRequests(); + ctx.data.sgvs = [{mills: now, mgdl: 100}]; + ctx.data.mbgs = [{mgdl: '100', mills: now - (15 * 60 * 1000)}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + treatmentnotify.checkNotifications(sbx); + should.not.exist(ctx.notifications.findHighestAlarm()); + should.exist(ctx.notifications.snoozedBy({level: levels.URGENT})); + + should.not.exist(_.first(ctx.notifications.findUnSnoozeable())); + + done(); + }); + + it('Request a notification for an announcement even there is an active snooze', function (done) { + ctx.notifications.initRequests(); + ctx.data.treatments = [{mills: now, mgdl: 40, eventType: 'Announcement', isAnnouncement: true, notes: 'This not an alarm'}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + + var fakeSnooze = { + level: levels.URGENT + , title: 'Snoozing alarms for the test' + , message: 'testing...' + , lengthMills: 60000 + }; + + sbx.notifications.requestSnooze(fakeSnooze); + + treatmentnotify.checkNotifications(sbx); + + var announcement = _.first(ctx.notifications.findUnSnoozeable()); + + should.exist(announcement); + announcement.title.should.equal('Urgent Announcement'); + announcement.level.should.equal(levels.URGENT); + announcement.pushoverSound.should.equal('persistent'); + should.deepEqual(ctx.notifications.findHighestAlarm(), announcement); + ctx.notifications.snoozedBy(announcement).should.equal(false); + + + done(); + }); + + it('Request a notification for a non-error announcement', function (done) { + ctx.notifications.initRequests(); + ctx.data.treatments = [{mills: now, mgdl: 100, eventType: 'Announcement', isAnnouncement: true, notes: 'This not an alarm'}]; + + var sbx = require('../lib/sandbox')().serverInit(env, ctx); + + treatmentnotify.checkNotifications(sbx); + + var announcement = _.first(ctx.notifications.findUnSnoozeable()); + + should.exist(announcement); + announcement.title.should.equal('Announcement'); + announcement.level.should.equal(levels.INFO); + should.not.exist(announcement.pushoverSound); + should.not.exist(ctx.notifications.findHighestAlarm()); + ctx.notifications.snoozedBy(announcement).should.equal(false); + + done(); + }); + +}); \ No newline at end of file diff --git a/tests/units.test.js b/tests/units.test.js new file mode 100644 index 00000000000..b6e8a9faa8f --- /dev/null +++ b/tests/units.test.js @@ -0,0 +1,16 @@ +'use strict'; + +require('should'); + +describe('units', function ( ) { + var units = require('../lib/units')(); + + it('should convert 99 to 5.5', function () { + units.mgdlToMMOL(99).should.equal('5.5'); + }); + + it('should convert 180 to 10.0', function () { + units.mgdlToMMOL(180).should.equal('10.0'); + }); + +}); diff --git a/tests/upbat.test.js b/tests/upbat.test.js new file mode 100644 index 00000000000..27f227f7f19 --- /dev/null +++ b/tests/upbat.test.js @@ -0,0 +1,78 @@ +'use strict'; + +require('should'); + +describe('Uploader Battery', function ( ) { + var data = {uploaderBattery: 20}; + var clientSettings = {}; + + it('display uploader battery status', function (done) { + var sandbox = require('../lib/sandbox')(); + var sbx = sandbox.clientInit(clientSettings, Date.now(), {}, data); + + sbx.offerProperty = function mockedOfferProperty (name, setter) { + name.should.equal('upbat'); + var result = setter(); + result.value.should.equal(20); + result.display.should.equal('20%'); + result.status.should.equal('urgent'); + result.level.should.equal(25); + done(); + }; + + var upbat = require('../lib/plugins/upbat')(); + upbat.setProperties(sbx); + + }); + + it('set a pill to the uploader battery status', function (done) { + var pluginBase = { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.value.should.equal('20%'); + options.labelClass.should.equal('icon-battery-25'); + options.pillClass.should.equal('urgent'); + done(); + } + }; + + var sandbox = require('../lib/sandbox')(); + var sbx = sandbox.clientInit(clientSettings, Date.now(), pluginBase, data); + var upbat = require('../lib/plugins/upbat')(); + upbat.setProperties(sbx); + upbat.updateVisualisation(sbx); + + }); + + it('hide the pill if there is no uploader battery status', function (done) { + var pluginBase = { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.hide.should.equal(true); + done(); + } + }; + + var sandbox = require('../lib/sandbox')(); + var sbx = sandbox.clientInit(clientSettings, Date.now(), pluginBase, {}); + var upbat = require('../lib/plugins/upbat')(); + upbat.setProperties(sbx); + upbat.updateVisualisation(sbx); + }); + + it('hide the pill if there is uploader battery status is -1', function (done) { + var pluginBase = { + updatePillText: function mockedUpdatePillText (plugin, options) { + options.hide.should.equal(true); + done(); + } + }; + + var sandbox = require('../lib/sandbox')(); + var sbx = sandbox.clientInit(clientSettings, Date.now(), pluginBase, {uploaderBattery: -1}); + var upbat = require('../lib/plugins/upbat')(); + upbat.setProperties(sbx); + upbat.updateVisualisation(sbx); + }); + + + +}); diff --git a/tests/update-throttle.test.js b/tests/update-throttle.test.js new file mode 100644 index 00000000000..384af352be6 --- /dev/null +++ b/tests/update-throttle.test.js @@ -0,0 +1,52 @@ +'use strict'; + +var _ = require('lodash'); +var request = require('supertest'); +require('should'); + +describe('Throttle', function ( ) { + var self = this; + + var api = require('../lib/api/'); + before(function (done) { + delete process.env.API_SECRET; + process.env.API_SECRET = 'this is my long pass phrase'; + self.env = require('../env')(); + this.wares = require('../lib/middleware/')(self.env); + self.app = require('express')(); + self.app.enable('api'); + require('../lib/bootevent')(self.env).boot(function booted(ctx) { + self.ctx = ctx; + self.app.use('/api', api(self.env, ctx)); + done(); + }); + }); + + after(function () { + delete process.env.API_SECRET; + }); + + it('only update once when there are multiple posts', function (done) { + + //if the data-loaded event is triggered more than once the test will fail + self.ctx.bus.on('data-loaded', function dataWasLoaded ( ) { + done(); + }); + + function post () { + request(self.app) + .post('/api/entries/') + .set('api-secret', self.env.api_secret || '') + .send({type: 'sgv', sgv: 100, date: Date.now()}) + .expect(200) + .end(function(err) { + if (err) { + done(err); + } + }); + } + + _.times(10, post); + }); + +}); diff --git a/tests/utils.test.js b/tests/utils.test.js new file mode 100644 index 00000000000..d107622070b --- /dev/null +++ b/tests/utils.test.js @@ -0,0 +1,33 @@ +'use strict'; + +require('should'); + +describe('utils', function ( ) { + var settings = { + alarmTimeagoUrgentMins: 30 + , alarmTimeagoWarnMins: 15 + }; + + var utils = require('../lib/utils')(settings); + + it('format numbers', function () { + utils.toFixed(5.499999999).should.equal('5.50'); + }); + + it('show format recent times to 1 minute', function () { + var result = utils.timeAgo(Date.now() - 30000); + result.value.should.equal(1); + result.label.should.equal('min ago'); + result.status.should.equal('current'); + }); + + it('merge date and time', function () { + var result = utils.mergeInputTime('22:35', '2015-07-14'); + result.hours().should.equal(22); + result.minutes().should.equal(35); + result.year().should.equal(2015); + result.format('MMM').should.equal('Jul'); + result.date().should.equal(14); + }); + +}); diff --git a/tests/verifyauth.test.js b/tests/verifyauth.test.js new file mode 100644 index 00000000000..0e635ec304c --- /dev/null +++ b/tests/verifyauth.test.js @@ -0,0 +1,68 @@ +'use strict'; + +var request = require('supertest'); + +describe('verifyauth', function ( ) { + var api = require('../lib/api/'); + + var scope = this; + function setup_app (env, fn) { + require('../lib/bootevent')(env).boot(function booted (ctx) { + var wares = require('../lib/middleware/')(env); + ctx.app = api(env, wares, ctx); + scope.app = ctx.app; + fn(ctx); + }); + } + + after(function (done) { + done(); + }); + + it('should fail unauthorized', function (done) { + var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; + delete process.env.API_SECRET; + process.env.API_SECRET = 'this is my long pass phrase'; + var env = require('../env')( ); + env.api_secret.should.equal(known); + setup_app(env, function (ctx) { + ctx.app.enabled('api').should.equal(true); + ctx.app.api_secret = ''; + ping_authorized_endpoint(ctx.app, 401, done); + }); + + }); + + + it('should work fine authorized', function (done) { + var known = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; + delete process.env.API_SECRET; + process.env.API_SECRET = 'this is my long pass phrase'; + var env = require('../env')( ); + env.api_secret.should.equal(known); + setup_app(env, function (ctx) { + ctx.app.enabled('api').should.equal(true); + ctx.app.api_secret = env.api_secret; + ping_authorized_endpoint(ctx.app, 200, done); + }); + + }); + + + function ping_authorized_endpoint (app, fails, fn) { + request(app) + .get('/verifyauth') + .set('api-secret', app.api_secret || '') + .expect(fails) + .end(function (err, res) { + //console.log(res.body); + if (fails < 400) { + res.body.status.should.equal(200); + } + fn( ); + // console.log('err', err, 'res', res); + }); + } + +}); +