diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md new file mode 100644 index 0000000..6bc732a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -0,0 +1,50 @@ +--- +name: Bug report +about: Create a report to help us improve +labels: bug + +--- + + + +## Steps to reproduce + + + +## Current Behavior + + + +## Expected Behavior + + + +## Link to reproduction sandbox + + + +## Additional information + + + +## Related Issues + + + +_See [Reporting Issues](http://loopback.io/doc/en/contrib/Reporting-issues.html) for more tips on writing good issues_ diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md new file mode 100644 index 0000000..1fd76ba --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Feature_request.md @@ -0,0 +1,25 @@ +--- +name: Feature request +about: Suggest an idea for this project +labels: feature + +--- + +## Suggestion + + + +## Use Cases + + + +## Examples + + + +## Acceptance criteria + +TBD - will be filled by the team. diff --git a/.github/ISSUE_TEMPLATE/Question.md b/.github/ISSUE_TEMPLATE/Question.md new file mode 100644 index 0000000..eb25195 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Question.md @@ -0,0 +1,27 @@ +--- +name: Question +about: The issue tracker is not for questions. Please use Stack Overflow or other resources for help. +labels: question + +--- + + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..9204746 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Report a security vulnerability + url: https://loopback.io/doc/en/contrib/Reporting-issues.html#security-issues + about: Do not report security vulnerabilities using GitHub issues. Please send an email to `reachsl@us.ibm.com` instead. + - name: Get help on StackOverflow + url: https://stackoverflow.com/tags/loopbackjs + about: Please ask and answer questions on StackOverflow. + - name: Join our mailing list + url: https://groups.google.com/forum/#!forum/loopbackjs + about: You can also post your question to our mailing list. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..1446583 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,18 @@ + + +## Checklist + +👉 [Read and sign the CLA (Contributor License Agreement)](https://cla.strongloop.com/agreements/strongloop/loopback-component-push) 👈 + +- [ ] `npm test` passes on your machine +- [ ] New tests added or existing tests modified to cover all changes +- [ ] Code conforms with the [style guide](https://loopback.io/doc/en/contrib/style-guide-es6.html) +- [ ] Commit messages are following our [guidelines](https://loopback.io/doc/en/contrib/git-commit-messages.html) diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000..bebe60a --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,23 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 60 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 14 +# Issues with these labels will never be considered stale +exemptLabels: + - pinned + - security + - critical + - p1 + - major +# Label to use when marking an issue as stale +staleLabel: stale +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: > + This issue has been closed due to continued inactivity. Thank you for your understanding. + If you believe this to be in error, please contact one of the code owners, + listed in the `CODEOWNERS` file at the top-level of this repository. diff --git a/.travis.yml b/.travis.yml index a7f3157..437ee5d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,4 @@ language: node_js node_js: - - "0.12" - - "0.11" - - "iojs" + - "8" + - "10" diff --git a/CHANGES.md b/CHANGES.md index f05ee1b..438d5e0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,106 @@ -2016-10-14, Version 1.7.0 +2020-03-06, Version 3.5.0 ========================= + * Update LTS status in README (Miroslav Bajtoš) + + * chore: update copyright year (Diana Lau) + + * chore: improve issue and PR templates (Nora) + + * update loopback-connector-mongodb (Diana Lau) + + * update eslint-config-loopback and fix violations (Nora) + + * drop support for node.js 6 (Nora) + + * update eslint dependency (Nora) + + +2019-07-09, Version 3.4.1 +========================= + + * chore: update copyrights years (Agnes Lin) + + * fix: update lodash (jannyHou) + + * update dependency (jannyHou) + + +2018-07-11, Version 3.4.0 +========================= + + * fix lint errors (Diana Lau) + + * chore: update node dependencies (Diana Lau) + + * [WebFM] cs/pl/ru translation (candytangnb) + + * chore:update dependencies (Diana Lau) + + +2018-05-08, Version 3.3.1 +========================= + + * chore: update lodash version (Diana Lau) + + +2018-04-26, Version 3.3.0 +========================= + + * Update README.md (Serge Bornow) + + * Fix missing title on iOS notifications (Zak Barbuto) + + * Add support for data-only notifications (Zak Barbuto) + + +2017-10-17, Version 3.2.0 +========================= + + * translation return for Q4 drop1 (tangyinb) + + * CODEOWNERS: add zbarbuto (Miroslav Bajtoš) + + * add globalize string (Diana Lau) + + +2017-09-18, Version 3.1.0 +========================= + + * Add support for additional notification props (Zak Barbuto) + + * Add stalebot configuration (Kevin Delisle) + + * create pr template (Sakib Hasan) + + * create issue template (Sakib Hasan) + + * Add CODEOWNERS file (Diana Lau) + + * update node version in travis (Diana Lau) + + * Add fcm tests (Zak Barbuto) + + * Add fcm support with addNotification (Zak Barbuto) + + +2016-12-21, Version 3.0.0 +========================= + + * Update paid support URL (Siddhi Pai) + + * add: HTTP2 APNS (Ilir Nuhiu) + + * Fix timeout in tests on Windows (Miroslav Bajtos) + + * Drop support for Node v0.10 & v0.12 (Miroslav Bajtoš) + + * Start the development of the next major version (Miroslav Bajtoš) + + * Remove Gruntfile and grunt deps (Miroslav Bajtoš) + + * Update README.md (Simon Ho) + * Use eslint in favour of JSHint (#129) (Simon Ho) * Update translation files - round#2 (Candy) diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..454854f --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,6 @@ +# Lines starting with '#' are comments. +# Each line is a file pattern followed by one or more owners, +# the last matching pattern has the most precendence. + +# Core team members from IBM +* @superkhau @raymondfeng @zbarbuto diff --git a/README.md b/README.md index 9527401..b28c103 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,15 @@ # LoopBack Push Notification Component -![StrongLoop Labs](http://docs.strongloop.com/download/thumbnails/5310165/StrongLoop%20Labs%20Logo%20Cropped.png "StrongLoop Labs") +**⚠️ LoopBack 3 is in Maintenance LTS mode, only critical bugs and critical +security fixes will be provided. (See +[Module Long Term Support Policy](#module-long-term-support-policy) below.)** + +We urge all LoopBack 3 users to migrate their applications to LoopBack 4 as +soon as possible. Refer to our +[Migration Guide](https://loopback.io/doc/en/lb4/migration-overview.html) +for more information on how to upgrade. + +## Overview > StrongLoop Labs projects provide early access to advanced or experimental functionality. In general, these projects may lack usability, completeness, documentation, and robustness, and may be outdated. However, StrongLoop supports these projects: Paying customers can open issues using the StrongLoop customer support system (Zendesk), and community users can report bugs on GitHub. @@ -31,7 +40,7 @@ providers such as APNS, GCM, and MPNS ### Node.js server -This module includes an [example LoopBack server application](https://github.com/strongloop/loopback-component-push/tree/master/example/server-2.0). +This module includes an [example LoopBack server application](https://github.com/strongloop/loopback-example-push). To run it, use these commands: @@ -53,12 +62,12 @@ MONGODB=mongodb://localhost/demo node app ### iOS client -The [iOS example app](https://github.com/strongloop/loopback-component-push/tree/master/example/ios) +The [iOS example app](https://github.com/strongloop/loopback-example-push/tree/master/ios) uses the LoopBack iOS SDK to enable and handle push notifications. ### Android client -The [Android example app](https://github.com/strongloop/loopback-component-push/tree/master/example/android) +The [Android example app](https://github.com/strongloop/loopback-example-push/tree/master/android) uses the LoopBack Android SDK to enable and handle push notifications. ## References @@ -69,3 +78,15 @@ uses the LoopBack Android SDK to enable and handle push notifications. - https://github.com/argon/node-apn - https://github.com/logicalparadox/apnagent-ios - https://blog.engineyard.com/2013/developing-ios-push-notifications-nodejs + +## Module Long Term Support Policy + +This module adopts the [ +Module Long Term Support (LTS)](http://github.com/CloudNativeJS/ModuleLTS) policy, + with the following End Of Life (EOL) dates: + +| Version | Status | Published | EOL | +| ------- | --------------- | --------- | -------- | +| 3.x | Maintenance LTS | Dec 2016 | Dec 2020 | + +Learn more about our LTS plan in [docs](https://loopback.io/doc/en/contrib/Long-term-support.html). diff --git a/index.js b/index.js index 94befd9..1d706e1 100644 --- a/index.js +++ b/index.js @@ -1,18 +1,18 @@ -// Copyright IBM Corp. 2013,2015. All Rights Reserved. +// Copyright IBM Corp. 2013,2019. All Rights Reserved. // Node module: loopback-component-push // This file is licensed under the Artistic License 2.0. // License text available at https://opensource.org/licenses/Artistic-2.0 'use strict'; -var SG = require('strong-globalize'); +const SG = require('strong-globalize'); SG.SetRootDir(__dirname); /** * Export the connector */ -var loopback = require('loopback'); -var PushConnector = require('./lib/push-connector'); +const loopback = require('loopback'); +const PushConnector = require('./lib/push-connector'); exports = module.exports = PushConnector; /** @@ -24,7 +24,7 @@ exports.Notification = require('./models').Notification; exports.createPushModel = function(options) { options = options || {}; - var pushDataSource = loopback.createDataSource({ + const pushDataSource = loopback.createDataSource({ connector: PushConnector, installation: options.installation, application: options.application, @@ -33,7 +33,7 @@ exports.createPushModel = function(options) { checkPeriodInSeconds: options.checkPeriodInSeconds, }); - var PushModel = pushDataSource.createModel(options.name || 'Push', {}, + const PushModel = pushDataSource.createModel(options.name || 'Push', {}, {plural: options.plural || 'push'}); return PushModel; }; diff --git a/intl/cs/messages.json b/intl/cs/messages.json new file mode 100644 index 0000000..21a920e --- /dev/null +++ b/intl/cs/messages.json @@ -0,0 +1,14 @@ +{ + "033d31e7bd62a420f23e4b154945a2b8": "oznámení musí být objekt", + "0a11bfdd7655e825fbd1f998b2e24db9": "Prázdný informační obsah", + "1ec12f42dab8979b9fc90a95a023419e": "deviceTokens musí být pole", + "704e7bf9910b8532f7c4889366fdcbac": "Kód chyby {{GCM}}: {0}, deviceToken: {1}", + "71a2b601d1e750aa05c7a28ed76b9973": "Neplatné pole (prázdné)", + "8bc87eb582af546d32505f2839234119": "Neplatný informační obsah", + "db1e3eb9bf0934d72eb8b0b110281f39": "Neplatné pole: {0}", + "e254b20a16461c7379202f6a5f8350cc": "Neplatný typ proměnné pro ${{0}}", + "e9023b2ab0a052c9ccbd257198c7429f": "Nelze odeslat oznámení {{APNS}}: {0}", + "f5146dece5cd70db519daf8c7e6d8478": "${{0}} neexistuje", + "f95c75ae3da1e88794fff4be7188f3dc": "Neplatná hodnota pro `{0}`" +} + diff --git a/intl/de/messages.json b/intl/de/messages.json index e37a797..ca09701 100644 --- a/intl/de/messages.json +++ b/intl/de/messages.json @@ -1,14 +1,14 @@ { + "033d31e7bd62a420f23e4b154945a2b8": "notification muss ein Objekt sein", "0a11bfdd7655e825fbd1f998b2e24db9": "Leere Nutzlast", + "1ec12f42dab8979b9fc90a95a023419e": "deviceTokens muss ein Array sein", + "704e7bf9910b8532f7c4889366fdcbac": "{{GCM}}-Fehlercode: {0}, deviceToken: {1}", "71a2b601d1e750aa05c7a28ed76b9973": "Ungültiges Feld (leer)", "8bc87eb582af546d32505f2839234119": "Ungültige Nutzlast", "db1e3eb9bf0934d72eb8b0b110281f39": "Ungültiges Feld: {0}", "e254b20a16461c7379202f6a5f8350cc": "Ungültiger Variablentyp für ${{0}}", - "f5146dece5cd70db519daf8c7e6d8478": "${{0}} ist nicht vorhanden", - "f95c75ae3da1e88794fff4be7188f3dc": "Ungültiger Wert für `{0}`", "e9023b2ab0a052c9ccbd257198c7429f": "{{APNS}}-Benachrichtigung kann nicht gesendet werden: {0}", - "704e7bf9910b8532f7c4889366fdcbac": "{{GCM}}-Fehlercode: {0}, deviceToken: {1}", - "033d31e7bd62a420f23e4b154945a2b8": "notification muss ein Objekt sein", - "1ec12f42dab8979b9fc90a95a023419e": "deviceTokens muss ein Array sein" + "f5146dece5cd70db519daf8c7e6d8478": "${{0}} ist nicht vorhanden", + "f95c75ae3da1e88794fff4be7188f3dc": "Ungültiger Wert für `{0}`" } diff --git a/intl/en/messages.json b/intl/en/messages.json index 349d436..a155121 100644 --- a/intl/en/messages.json +++ b/intl/en/messages.json @@ -1,13 +1,13 @@ { + "033d31e7bd62a420f23e4b154945a2b8": "notification must be an object", "0a11bfdd7655e825fbd1f998b2e24db9": "Empty payload", + "1ec12f42dab8979b9fc90a95a023419e": "deviceTokens must be an array", + "704e7bf9910b8532f7c4889366fdcbac": "{{GCM}} error code: {0}, deviceToken: {1}", "71a2b601d1e750aa05c7a28ed76b9973": "Invalid field (empty)", "8bc87eb582af546d32505f2839234119": "Invalid payload", "db1e3eb9bf0934d72eb8b0b110281f39": "Invalid field: {0}", "e254b20a16461c7379202f6a5f8350cc": "Invalid variable type for ${{0}}", - "f5146dece5cd70db519daf8c7e6d8478": "The ${{0}} does not exist", - "f95c75ae3da1e88794fff4be7188f3dc": "Invalid value for `{0}`", "e9023b2ab0a052c9ccbd257198c7429f": "Cannot send {{APNS}} notification: {0}", - "704e7bf9910b8532f7c4889366fdcbac": "{{GCM}} error code: {0}, deviceToken: {1}", - "033d31e7bd62a420f23e4b154945a2b8": "notification must be an object", - "1ec12f42dab8979b9fc90a95a023419e": "deviceTokens must be an array" + "f5146dece5cd70db519daf8c7e6d8478": "The ${{0}} does not exist", + "f95c75ae3da1e88794fff4be7188f3dc": "Invalid value for `{0}`" } diff --git a/intl/es/messages.json b/intl/es/messages.json index ab47565..4a9249c 100644 --- a/intl/es/messages.json +++ b/intl/es/messages.json @@ -1,14 +1,14 @@ { + "033d31e7bd62a420f23e4b154945a2b8": "notification debe ser un objeto", "0a11bfdd7655e825fbd1f998b2e24db9": "Carga útil vacía", + "1ec12f42dab8979b9fc90a95a023419e": "deviceTokens debe ser una matriz", + "704e7bf9910b8532f7c4889366fdcbac": "Código de error de {{GCM}}: {0}, deviceToken: {1}", "71a2b601d1e750aa05c7a28ed76b9973": "Campo no válido (vacío)", "8bc87eb582af546d32505f2839234119": "Carga útil no válida", "db1e3eb9bf0934d72eb8b0b110281f39": "Campo no válido: {0}", "e254b20a16461c7379202f6a5f8350cc": "Tipo de variable no válido para ${{0}}", - "f5146dece5cd70db519daf8c7e6d8478": "${{0}} no existe", - "f95c75ae3da1e88794fff4be7188f3dc": "Valor no válido para `{0}`", "e9023b2ab0a052c9ccbd257198c7429f": "No se puede enviar la notificación {{APNS}}: {0}", - "704e7bf9910b8532f7c4889366fdcbac": "Código de error de {{GCM}}: {0}, deviceToken: {1}", - "033d31e7bd62a420f23e4b154945a2b8": "notification debe ser un objeto", - "1ec12f42dab8979b9fc90a95a023419e": "deviceTokens debe ser una matriz" + "f5146dece5cd70db519daf8c7e6d8478": "${{0}} no existe", + "f95c75ae3da1e88794fff4be7188f3dc": "Valor no válido para `{0}`" } diff --git a/intl/fr/messages.json b/intl/fr/messages.json index b0fdae9..a2b740b 100644 --- a/intl/fr/messages.json +++ b/intl/fr/messages.json @@ -1,14 +1,14 @@ { + "033d31e7bd62a420f23e4b154945a2b8": "la notification doit être un objet", "0a11bfdd7655e825fbd1f998b2e24db9": "Contenu vide", + "1ec12f42dab8979b9fc90a95a023419e": "les jetons d'unité doivent être un tableau", + "704e7bf9910b8532f7c4889366fdcbac": "Code d'erreur {{GCM}} : {0}, jeton d'unité : {1}", "71a2b601d1e750aa05c7a28ed76b9973": "Zone non valide (vide)", "8bc87eb582af546d32505f2839234119": "Contenu non valide", "db1e3eb9bf0934d72eb8b0b110281f39": "Zone non valide : {0}", "e254b20a16461c7379202f6a5f8350cc": "Type de variable non valide pour ${{0}}", - "f5146dece5cd70db519daf8c7e6d8478": "${{0}} n'existe pas", - "f95c75ae3da1e88794fff4be7188f3dc": "Valeur non valide pour `{0}`", "e9023b2ab0a052c9ccbd257198c7429f": "Impossible d'envoyer la notification {{APNS}} : {0}", - "704e7bf9910b8532f7c4889366fdcbac": "Code d'erreur {{GCM}} : {0}, jeton d'unité : {1}", - "033d31e7bd62a420f23e4b154945a2b8": "la notification doit être un objet", - "1ec12f42dab8979b9fc90a95a023419e": "les jetons d'unité doivent être un tableau" + "f5146dece5cd70db519daf8c7e6d8478": "${{0}} n'existe pas", + "f95c75ae3da1e88794fff4be7188f3dc": "Valeur non valide pour `{0}`" } diff --git a/intl/it/messages.json b/intl/it/messages.json index c6f07b7..cdf9ff8 100644 --- a/intl/it/messages.json +++ b/intl/it/messages.json @@ -1,14 +1,14 @@ { + "033d31e7bd62a420f23e4b154945a2b8": "la notifica deve essere un oggetto", "0a11bfdd7655e825fbd1f998b2e24db9": "Payload vuoto", + "1ec12f42dab8979b9fc90a95a023419e": "deviceTokens deve essere un array", + "704e7bf9910b8532f7c4889366fdcbac": "Codice di errore {{GCM}}: {0}, deviceToken: {1}", "71a2b601d1e750aa05c7a28ed76b9973": "Campo non valido (vuoto)", "8bc87eb582af546d32505f2839234119": "Payload non valido", "db1e3eb9bf0934d72eb8b0b110281f39": "Campo non valido: {0}", "e254b20a16461c7379202f6a5f8350cc": "Tipo di variabile non valido per ${{0}}", - "f5146dece5cd70db519daf8c7e6d8478": "${{0}} non esiste", - "f95c75ae3da1e88794fff4be7188f3dc": "Valore non valido per `{0}`", "e9023b2ab0a052c9ccbd257198c7429f": "Impossibile inviare la notifica {{APNS}}: {0}", - "704e7bf9910b8532f7c4889366fdcbac": "Codice di errore {{GCM}}: {0}, deviceToken: {1}", - "033d31e7bd62a420f23e4b154945a2b8": "la notifica deve essere un oggetto", - "1ec12f42dab8979b9fc90a95a023419e": "deviceTokens deve essere un array" + "f5146dece5cd70db519daf8c7e6d8478": "${{0}} non esiste", + "f95c75ae3da1e88794fff4be7188f3dc": "Valore non valido per `{0}`" } diff --git a/intl/ja/messages.json b/intl/ja/messages.json index 47f2375..d6e0de2 100644 --- a/intl/ja/messages.json +++ b/intl/ja/messages.json @@ -1,14 +1,14 @@ { + "033d31e7bd62a420f23e4b154945a2b8": "通知はオブジェクトでなければなりません", "0a11bfdd7655e825fbd1f998b2e24db9": "空のペイロード", + "1ec12f42dab8979b9fc90a95a023419e": "deviceTokens は配列でなければなりません", + "704e7bf9910b8532f7c4889366fdcbac": "{{GCM}} エラー・コード: {0}、deviceToken: {1}", "71a2b601d1e750aa05c7a28ed76b9973": "無効なフィールド (空)", "8bc87eb582af546d32505f2839234119": "無効なペイロード", "db1e3eb9bf0934d72eb8b0b110281f39": "無効なフィールド: {0}", "e254b20a16461c7379202f6a5f8350cc": "${{0}} の変数型が無効です", - "f5146dece5cd70db519daf8c7e6d8478": "${{0}} は存在しません", - "f95c75ae3da1e88794fff4be7188f3dc": "`{0}` の値が無効です", "e9023b2ab0a052c9ccbd257198c7429f": "{{APNS}} 通知を送信できません: {0}", - "704e7bf9910b8532f7c4889366fdcbac": "{{GCM}} エラー・コード: {0}、deviceToken: {1}", - "033d31e7bd62a420f23e4b154945a2b8": "通知はオブジェクトでなければなりません", - "1ec12f42dab8979b9fc90a95a023419e": "deviceTokens は配列でなければなりません" + "f5146dece5cd70db519daf8c7e6d8478": "${{0}} は存在しません", + "f95c75ae3da1e88794fff4be7188f3dc": "`{0}` の値が無効です" } diff --git a/intl/ko/messages.json b/intl/ko/messages.json index 627b0f4..274a1cb 100644 --- a/intl/ko/messages.json +++ b/intl/ko/messages.json @@ -1,14 +1,14 @@ { + "033d31e7bd62a420f23e4b154945a2b8": "알림은 오브젝트여야 함", "0a11bfdd7655e825fbd1f998b2e24db9": "비어 있는 페이로드", + "1ec12f42dab8979b9fc90a95a023419e": "디바이스 토큰은 배열이어야 함", + "704e7bf9910b8532f7c4889366fdcbac": "{{GCM}} 오류 코드: {0}, 디바이스 토큰: {1}", "71a2b601d1e750aa05c7a28ed76b9973": "올바르지 않은 필드(비어 있음)", "8bc87eb582af546d32505f2839234119": "올바르지 않은 페이로드", "db1e3eb9bf0934d72eb8b0b110281f39": "올바르지 않은 필드: {0}", "e254b20a16461c7379202f6a5f8350cc": "${{0}}의 올바르지 않은 변수 유형", - "f5146dece5cd70db519daf8c7e6d8478": "${{0}}이(가) 없음", - "f95c75ae3da1e88794fff4be7188f3dc": "`{0}`의 올바르지 않은 값", "e9023b2ab0a052c9ccbd257198c7429f": "{{APNS}} 알림을 보낼 수 없음: {0}", - "704e7bf9910b8532f7c4889366fdcbac": "{{GCM}} 오류 코드: {0}, 디바이스 토큰: {1}", - "033d31e7bd62a420f23e4b154945a2b8": "알림은 오브젝트여야 함", - "1ec12f42dab8979b9fc90a95a023419e": "디바이스 토큰은 배열이어야 함" + "f5146dece5cd70db519daf8c7e6d8478": "${{0}}이(가) 없음", + "f95c75ae3da1e88794fff4be7188f3dc": "`{0}`의 올바르지 않은 값" } diff --git a/intl/nl/messages.json b/intl/nl/messages.json index 1973162..be3064a 100644 --- a/intl/nl/messages.json +++ b/intl/nl/messages.json @@ -1,14 +1,14 @@ { + "033d31e7bd62a420f23e4b154945a2b8": "melding moet een object zijn", "0a11bfdd7655e825fbd1f998b2e24db9": "Lege payload", + "1ec12f42dab8979b9fc90a95a023419e": "deviceTokens moeten een array zijn", + "704e7bf9910b8532f7c4889366fdcbac": "{{GCM}}-foutcode: {0}, deviceToken: {1}", "71a2b601d1e750aa05c7a28ed76b9973": "Ongeldig veld (leeg)", "8bc87eb582af546d32505f2839234119": "Ongeldige payload", "db1e3eb9bf0934d72eb8b0b110281f39": "Ongeldig veld: {0}", "e254b20a16461c7379202f6a5f8350cc": "Ongeldig type variabele voor ${{0}}", - "f5146dece5cd70db519daf8c7e6d8478": "Het item ${{0}} bestaat niet", - "f95c75ae3da1e88794fff4be7188f3dc": "Ongeldige waarde voor '{0}'", "e9023b2ab0a052c9ccbd257198c7429f": "Kan {{APNS}}-melding niet verzenden: {0}", - "704e7bf9910b8532f7c4889366fdcbac": "{{GCM}}-foutcode: {0}, deviceToken: {1}", - "033d31e7bd62a420f23e4b154945a2b8": "melding moet een object zijn", - "1ec12f42dab8979b9fc90a95a023419e": "deviceTokens moeten een array zijn" + "f5146dece5cd70db519daf8c7e6d8478": "Het item ${{0}} bestaat niet", + "f95c75ae3da1e88794fff4be7188f3dc": "Ongeldige waarde voor '{0}'" } diff --git a/intl/pl/messages.json b/intl/pl/messages.json new file mode 100644 index 0000000..45c6dd3 --- /dev/null +++ b/intl/pl/messages.json @@ -0,0 +1,14 @@ +{ + "033d31e7bd62a420f23e4b154945a2b8": "powiadomienie musi być obiektem", + "0a11bfdd7655e825fbd1f998b2e24db9": "Pusty ładunek", + "1ec12f42dab8979b9fc90a95a023419e": "Parametr deviceTokens musi być tablicą", + "704e7bf9910b8532f7c4889366fdcbac": "Kod błędu {{GCM}}: {0}, deviceToken: {1}", + "71a2b601d1e750aa05c7a28ed76b9973": "Niepoprawne pole (puste)", + "8bc87eb582af546d32505f2839234119": "Niepoprawny ładunek", + "db1e3eb9bf0934d72eb8b0b110281f39": "Niepoprawne pole: {0}", + "e254b20a16461c7379202f6a5f8350cc": "Niepoprawny typ zmiennej dla ${{0}}", + "e9023b2ab0a052c9ccbd257198c7429f": "Nie można wysłać powiadomienia {{APNS}}: {0}", + "f5146dece5cd70db519daf8c7e6d8478": "${{0}} nie istnieje", + "f95c75ae3da1e88794fff4be7188f3dc": "Niepoprawna wartość `{0}`" +} + diff --git a/intl/pt/messages.json b/intl/pt/messages.json index 0e771d8..2b09250 100644 --- a/intl/pt/messages.json +++ b/intl/pt/messages.json @@ -1,14 +1,14 @@ { + "033d31e7bd62a420f23e4b154945a2b8": "notificação deve ser um objeto", "0a11bfdd7655e825fbd1f998b2e24db9": "Carga útil vazia", + "1ec12f42dab8979b9fc90a95a023419e": "deviceTokens deve ser uma matriz", + "704e7bf9910b8532f7c4889366fdcbac": "Código de erro de {{GCM}}: {0}, deviceToken: {1}", "71a2b601d1e750aa05c7a28ed76b9973": "Campo inválido (vazio)", "8bc87eb582af546d32505f2839234119": "Carga útil inválida", "db1e3eb9bf0934d72eb8b0b110281f39": "Campo inválido: {0}", "e254b20a16461c7379202f6a5f8350cc": "Tipo de variável inválido para ${{0}}", - "f5146dece5cd70db519daf8c7e6d8478": "O ${{0}} não existe", - "f95c75ae3da1e88794fff4be7188f3dc": "Valor inválido para `{0}`", "e9023b2ab0a052c9ccbd257198c7429f": "Não é possível enviar notificação de {{APNS}}: {0}", - "704e7bf9910b8532f7c4889366fdcbac": "Código de erro de {{GCM}}: {0}, deviceToken: {1}", - "033d31e7bd62a420f23e4b154945a2b8": "notificação deve ser um objeto", - "1ec12f42dab8979b9fc90a95a023419e": "deviceTokens deve ser uma matriz" + "f5146dece5cd70db519daf8c7e6d8478": "O ${{0}} não existe", + "f95c75ae3da1e88794fff4be7188f3dc": "Valor inválido para `{0}`" } diff --git a/intl/ru/messages.json b/intl/ru/messages.json new file mode 100644 index 0000000..49328e6 --- /dev/null +++ b/intl/ru/messages.json @@ -0,0 +1,14 @@ +{ + "033d31e7bd62a420f23e4b154945a2b8": "уведомление должно быть объектом", + "0a11bfdd7655e825fbd1f998b2e24db9": "Пустое значение полезной нагрузки", + "1ec12f42dab8979b9fc90a95a023419e": "deviceTokens должен быть массивом", + "704e7bf9910b8532f7c4889366fdcbac": "Код ошибки {{GCM}}: {0}, deviceToken: {1}", + "71a2b601d1e750aa05c7a28ed76b9973": "Недопустимое поле (пустое)", + "8bc87eb582af546d32505f2839234119": "Недопустимая полезная нагрузка", + "db1e3eb9bf0934d72eb8b0b110281f39": "Недопустимое поле: {0}", + "e254b20a16461c7379202f6a5f8350cc": "Недопустимый тип переменной для ${{0}}", + "e9023b2ab0a052c9ccbd257198c7429f": "Не удалось отправить уведомление {{APNS}}: {0}", + "f5146dece5cd70db519daf8c7e6d8478": "${{0}} не существует", + "f95c75ae3da1e88794fff4be7188f3dc": "Недопустимое значение для `{0}`" +} + diff --git a/intl/tr/messages.json b/intl/tr/messages.json index 692a568..b9f2cf5 100644 --- a/intl/tr/messages.json +++ b/intl/tr/messages.json @@ -1,14 +1,14 @@ { + "033d31e7bd62a420f23e4b154945a2b8": "bildirim bir nesne olmalıdır", "0a11bfdd7655e825fbd1f998b2e24db9": "Boş bilgi yükü", + "1ec12f42dab8979b9fc90a95a023419e": "aygıt belirteçleri bir dizi olmalıdır", + "704e7bf9910b8532f7c4889366fdcbac": "{{GCM}} hata kodu: {0}, aygıt belirteci: {1}", "71a2b601d1e750aa05c7a28ed76b9973": "Geçersiz alan (empty)", "8bc87eb582af546d32505f2839234119": "Geçersiz bilgi yükü", "db1e3eb9bf0934d72eb8b0b110281f39": "Geçersiz alan: {0}", "e254b20a16461c7379202f6a5f8350cc": "${{0}} için geçersiz değişken tipi", - "f5146dece5cd70db519daf8c7e6d8478": "${{0}} yok", - "f95c75ae3da1e88794fff4be7188f3dc": "`{0}` için geçersiz değer", "e9023b2ab0a052c9ccbd257198c7429f": "{{APNS}} bildirimi gönderilemiyor: {0}", - "704e7bf9910b8532f7c4889366fdcbac": "{{GCM}} hata kodu: {0}, aygıt belirteci: {1}", - "033d31e7bd62a420f23e4b154945a2b8": "bildirim bir nesne olmalıdır", - "1ec12f42dab8979b9fc90a95a023419e": "aygıt belirteçleri bir dizi olmalıdır" + "f5146dece5cd70db519daf8c7e6d8478": "${{0}} yok", + "f95c75ae3da1e88794fff4be7188f3dc": "`{0}` için geçersiz değer" } diff --git a/intl/zh-Hans/messages.json b/intl/zh-Hans/messages.json index 116b36e..ee2f145 100644 --- a/intl/zh-Hans/messages.json +++ b/intl/zh-Hans/messages.json @@ -1,14 +1,14 @@ { + "033d31e7bd62a420f23e4b154945a2b8": "通知必须是对象", "0a11bfdd7655e825fbd1f998b2e24db9": "空的有效内容", + "1ec12f42dab8979b9fc90a95a023419e": "deviceTokens 必须是数组", + "704e7bf9910b8532f7c4889366fdcbac": "{{GCM}} 错误代码:{0},deviceToken:{1}", "71a2b601d1e750aa05c7a28ed76b9973": "无效字段(空)", "8bc87eb582af546d32505f2839234119": "无效的有效内容", "db1e3eb9bf0934d72eb8b0b110281f39": "无效字段:{0}", "e254b20a16461c7379202f6a5f8350cc": "${{0}} 的变量类型无效", - "f5146dece5cd70db519daf8c7e6d8478": "${{0}} 不存在", - "f95c75ae3da1e88794fff4be7188f3dc": "“{0}”的值无效", "e9023b2ab0a052c9ccbd257198c7429f": "无法发送 {{APNS}} 通知:{0}", - "704e7bf9910b8532f7c4889366fdcbac": "{{GCM}} 错误代码:{0},deviceToken:{1}", - "033d31e7bd62a420f23e4b154945a2b8": "通知必须是对象", - "1ec12f42dab8979b9fc90a95a023419e": "deviceTokens 必须是数组" + "f5146dece5cd70db519daf8c7e6d8478": "${{0}} 不存在", + "f95c75ae3da1e88794fff4be7188f3dc": "“{0}”的值无效" } diff --git a/intl/zh-Hant/messages.json b/intl/zh-Hant/messages.json index 5954186..b6cb2cf 100644 --- a/intl/zh-Hant/messages.json +++ b/intl/zh-Hant/messages.json @@ -1,14 +1,14 @@ { + "033d31e7bd62a420f23e4b154945a2b8": "notification 必須是物件", "0a11bfdd7655e825fbd1f998b2e24db9": "有效負載空白", + "1ec12f42dab8979b9fc90a95a023419e": "deviceTokens 必須是陣列", + "704e7bf9910b8532f7c4889366fdcbac": "{{GCM}} 錯誤碼:{0},deviceToken:{1}", "71a2b601d1e750aa05c7a28ed76b9973": "欄位無效(空白)", "8bc87eb582af546d32505f2839234119": "有效負載無效", "db1e3eb9bf0934d72eb8b0b110281f39": "無效欄位:{0}", "e254b20a16461c7379202f6a5f8350cc": "${{0}} 的變數類型無效", - "f5146dece5cd70db519daf8c7e6d8478": "${{0}} 不存在", - "f95c75ae3da1e88794fff4be7188f3dc": "`{0}` 的值無效", "e9023b2ab0a052c9ccbd257198c7429f": "無法傳送 {{APNS}} 通知:{0}", - "704e7bf9910b8532f7c4889366fdcbac": "{{GCM}} 錯誤碼:{0},deviceToken:{1}", - "033d31e7bd62a420f23e4b154945a2b8": "notification 必須是物件", - "1ec12f42dab8979b9fc90a95a023419e": "deviceTokens 必須是陣列" + "f5146dece5cd70db519daf8c7e6d8478": "${{0}} 不存在", + "f95c75ae3da1e88794fff4be7188f3dc": "`{0}` 的值無效" } diff --git a/lib/payload.js b/lib/payload.js index 05e9024..487496b 100644 --- a/lib/payload.js +++ b/lib/payload.js @@ -1,20 +1,20 @@ -// Copyright IBM Corp. 2013,2015. All Rights Reserved. +// Copyright IBM Corp. 2013,2019. All Rights Reserved. // Node module: loopback-component-push // This file is licensed under the Artistic License 2.0. // License text available at https://opensource.org/licenses/Artistic-2.0 'use strict'; -var g = require('strong-globalize')(); +const g = require('strong-globalize')(); -var serial = 0; -var __hasProp = {}.hasOwnProperty; +let serial = 0; +const __hasProp = {}.hasOwnProperty; // eslint-disable-next-line camelcase Payload.prototype.locale_format = /^[a-z]{2}_[A-Z]{2}$/; function Payload(data) { - var key, prefix, subkey, sum, type, value, _i, _len, _ref, _ref1; + let key, prefix, subkey, sum, type, value, _i, _len, _ref; if (typeof data !== 'object') { throw new Error(g.f('Invalid payload')); } @@ -46,7 +46,7 @@ function Payload(data) { break; default: if ( - (_ref = key.split('.', 2), prefix = _ref[0], subkey = _ref[1], _ref) + (_ref = key.split('.', 2), prefix = _ref[0], subkey = _ref[1], _ref) .length === 2) { this[prefix][subkey] = value; } else { @@ -55,13 +55,12 @@ function Payload(data) { } } sum = 0; - _ref1 = ['title', 'msg', 'data']; + const _ref1 = ['title', 'msg', 'data']; for (_i = 0, _len = _ref1.length; _i < _len; _i++) { type = _ref1[_i]; sum += ((function() { - var _ref2, _results; - _ref2 = this[type]; - _results = []; + const _ref2 = this[type]; + const _results = []; for (key in _ref2) { if (!__hasProp.call(_ref2, key)) continue; _results.push(key); @@ -97,8 +96,8 @@ Payload.prototype.localized = function(type, lang) { }; Payload.prototype.compile = function() { - var lang, msg, type, _i, _len, _ref, _ref1; - _ref = ['title', 'msg']; + let lang, msg, type, _i, _len, _ref1; + const _ref = ['title', 'msg']; for (_i = 0, _len = _ref.length; _i < _len; _i++) { type = _ref[_i]; _ref1 = this[type]; @@ -112,14 +111,14 @@ Payload.prototype.compile = function() { }; Payload.prototype.compileTemplate = function(tmpl) { - var _this = this; + const _this = this; return tmpl.replace(/\$\{(.*?)\}/g, function(match, keyPath) { return _this.variable(keyPath); }); }; Payload.prototype.variable = function(keyPath) { - var key, prefix, _ref, _ref1, _ref2; + let _ref, _ref1; if (keyPath === 'event.name') { if ((_ref = this.event) != null ? _ref.name : undefined) { return (_ref1 = this.event) != null ? _ref1.name : undefined; @@ -127,9 +126,9 @@ Payload.prototype.variable = function(keyPath) { throw new Error(g.f('The ${%s} does not exist', keyPath)); } } - _ref2 = keyPath.split('.', 2); - prefix = _ref2[0]; - key = _ref2[1]; + const _ref2 = keyPath.split('.', 2); + const prefix = _ref2[0]; + const key = _ref2[1]; if (prefix !== 'var' && prefix !== 'data') { throw new Error(g.f('Invalid variable type for ${%s}', keyPath)); } diff --git a/lib/providers/apns.js b/lib/providers/apns.js index eab19d5..1b0451b 100644 --- a/lib/providers/apns.js +++ b/lib/providers/apns.js @@ -1,134 +1,199 @@ -// Copyright IBM Corp. 2013,2015. All Rights Reserved. +// Copyright IBM Corp. 2013,2019. All Rights Reserved. // Node module: loopback-component-push // This file is licensed under the Artistic License 2.0. // License text available at https://opensource.org/licenses/Artistic-2.0 'use strict'; -var g = require('strong-globalize')(); +const g = require('strong-globalize')(); -var inherits = require('util').inherits; -var EventEmitter = require('events').EventEmitter; -var debug = require('debug')('loopback:component:push:provider:apns'); -var apn = require('apn'); +const inherits = require('util').inherits; +const assert = require('assert'); +const EventEmitter = require('events').EventEmitter; +const debug = require('debug')('loopback:component:push:provider:apns'); +const apn = require('apn'); +/** + * Provider used to distribute push notifications through Apple Push Notification Service. + * @param pushSettings + * @constructor + */ function ApnsProvider(pushSettings) { pushSettings = pushSettings || {}; - var settings = pushSettings.apns || {}; - var pushOptions = settings.pushOptions || {}; - var feedbackOptions = settings.feedbackOptions || {}; - - // Populate the shared cert/key data - if (settings.certData) { - pushOptions.cert = pushOptions.certData || settings.certData; - feedbackOptions.cert = feedbackOptions.certData || settings.certData; - } - if (settings.keyData) { - pushOptions.key = pushOptions.keyData || settings.keyData; - feedbackOptions.key = feedbackOptions.keyData || settings.keyData; - } - // Check the push mode production vs development - if (settings.production) { - pushOptions.production = true; - feedbackOptions.production = true; + const settings = pushSettings.apns || {}; + const pushOptions = settings.pushOptions || {}; - // Always override - pushOptions.gateway = 'gateway.push.apple.com'; - feedbackOptions.gateway = 'feedback.push.apple.com'; - if (pushOptions.port !== undefined) { - pushOptions.port = 2195; - } - if (feedbackOptions.port !== undefined) { - feedbackOptions.port = 2196; - } - } else { + // is running sandbox / production + if (typeof settings.production === 'undefined') { pushOptions.production = false; - feedbackOptions.production = false; - - // Honor the gateway settings for testing - pushOptions.gateway = pushOptions.gateway || - 'gateway.sandbox.push.apple.com'; - feedbackOptions.gateway = feedbackOptions.gateway || - 'feedback.sandbox.push.apple.com'; + } else { + pushOptions.production = settings.production; } + // validate required properties + const errors = { + token: 'JWT Token must be defined, property "token" is undefined.', + bundle: 'Bundle should contain the bundle identifier of the app', + + keyId: 'Tokens property "keyId" must be set.', + key: 'Tokens property "key" must be set.', + teamId: 'Tokens property "teamId" must be set.', + }; + + const u = 'undefined'; + + assert.notStrictEqual(typeof settings.token, u, errors.token); + assert.notStrictEqual(typeof settings.bundle, u, errors.bundle); + + assert.notStrictEqual(typeof settings.token.keyId, u, errors.keyId); + assert.notStrictEqual(typeof settings.token.key, u, errors.key); + assert.notStrictEqual(typeof settings.token.teamId, u, errors.teamId); + + // handle token & bundle configuration + pushOptions.token = settings.token; + pushOptions.bundle = settings.bundle; + // Keep the options for testing verification this._pushOptions = pushOptions; - this._feedbackOptions = feedbackOptions; - this._setupPushConnection(pushOptions); - this._setupFeedback(feedbackOptions); + /** + * the connection property + * @type {null} + * @private + */ + let _connection = null; + + /** + * Sets the stored connection + * @param connect + */ + this.setConnection = function(connect) { + _connection = connect; + }; + + /** + * Gets the stored connection or null + * @returns {*} + */ + this.getConnection = function() { + return _connection; + }; + + /** + * Retrieves whether is connected or not + * @returns {boolean} + */ + this.getConnected = function() { + return _connection !== null; + }; + + debug('Initialize APNS'); } inherits(ApnsProvider, EventEmitter); exports = module.exports = ApnsProvider; -ApnsProvider.prototype._setupPushConnection = function(options) { - debug('setting up push connection', options); - var self = this; - if (options && options.port === null) { - options.port = undefined; +/** + * Ensures that the push connection is created, if not done yet. + * @param options + * @returns {*} + * @private + */ +ApnsProvider.prototype._ensurePushConnection = function(options) { + const self = this; + + debug('Check whether connected', self.getConnected()); + + // already connection running, close + if (self.getConnected()) { + debug('Connection already established, do not reconnect'); + return; } + debug('setting up push connection', self.getConnected(), options); + + /** + * Error handler for connection errors + * @param err + */ function errorHandler(err) { debug('Cannot initialize APNS connection. %s', err.stack); self.emit('error', err); } - var connection; + /** + * Error handler for transmission errors + * @param code + * @param notification + * @param recipient + */ + function transmissionErrorHandler(code, notification, recipient) { + const err = new Error(g.f('Cannot send {{APNS}} notification: %s', code)); + self.emit(err, notification, recipient); + } + + // try to connect & create the provider try { - connection = new apn.Connection(options); + self.setConnection(new apn.Provider(options)); + + debug('created connection', self.getConnected()); } catch (e) { return errorHandler(e); } - connection.on('error', errorHandler); - connection.on('socketError', errorHandler); - connection.on('transmissionError', function(code, notification, recipient) { - var err = new Error(g.f('Cannot send {{APNS}} notification: %s', code)); - self.emit(err, notification, recipient); - }); + // handle errors, attach handlers to the events + self.getConnection().on('error', errorHandler); + self.getConnection().on('socketError', errorHandler); + self.getConnection().on('transmissionError', transmissionErrorHandler); - this._connection = connection; + debug('is connected', self.getConnected()); }; -ApnsProvider.prototype._setupFeedback = function(options) { - debug('setting up feedback connection', options); - if (!options) { - debug('Feedback channel is not enabled in the application settings.'); - return; - } - if (options && options.port === null) { - options.port = undefined; - } +/*** + * Send push notification through APNs + * @param notification + * @param deviceToken + */ +ApnsProvider.prototype.pushNotification = function(notification, deviceToken) { + const self = this; + const pushOptions = self._pushOptions; - var self = this; + // node-apn has a bug rightnow.. after sending the first + // batch of notifications, the connection goes away + // so make sure we reconnect in those cases + self._ensurePushConnection(pushOptions); - function errorHandler(err) { - debug('Cannot initialize APNS feedback. %s', err.stack); - self.emit('error', err); - } + // Note parameters are described here: + // http://bit.ly/apns-notification-payload + const note = _createNotification(notification, pushOptions); - try { - this._feedback = new apn.Feedback(options); - } catch (e) { - return errorHandler(e); - } + debug('Pushing notification to %j:', deviceToken, note); - this._feedback.on('error', errorHandler); + self.getConnection().send(note, deviceToken).then(function(result) { + debug('Sent through APNs, got result', result); - this._feedback.on('feedback', function(devices) { - debug('Devices gone:', devices); - self.emit('devicesGone', devices); + // we get immediate feedback from APNs + // distribute to notify everybody that there are devices unreachable + if (result.failed.length > 0) { + self.emit('devicesGone', _extractDeviceTokens(result.failed)); + } + }, function() { + debug('There was an error while sending', arguments); }); }; -ApnsProvider.prototype.pushNotification = function(notification, deviceToken) { - // Note parameters are described here: - // http://bit.ly/apns-notification-payload - var note = new apn.Notification(); +/** + * Creates new apn notification object + * + * @param notification + * @param pushOptions + * @private + */ +function _createNotification(notification, pushOptions) { + const note = new apn.Notification(); + note.expiry = notification.getTimeToLiveInSecondsFromNow() || note.expiry; note.badge = notification.badge; note.sound = notification.sound; @@ -136,12 +201,33 @@ ApnsProvider.prototype.pushNotification = function(notification, deviceToken) { note.category = notification.category; note.contentAvailable = notification.contentAvailable; note.urlArgs = notification.urlArgs; + note.title = notification.title || notification.messageFrom; note.payload = {}; + // the topic is necessary to identify + // the app which receives this notification + note.topic = pushOptions.bundle; + // custom stuff which will be added to the payload Object.keys(notification).forEach(function(key) { note.payload[key] = notification[key]; }); - debug('Pushing notification to %j:', deviceToken, note); - this._connection.pushNotification(note, deviceToken); -}; + return note; +} + +/** + * Extract the plain tokens from a list of failed devices + * @param failed + * @private + */ +function _extractDeviceTokens(failed) { + const tokens = []; + + failed.forEach(function(device) { + const token = device.device; + + tokens.push(token); + }); + + return tokens; +} diff --git a/lib/providers/gcm.js b/lib/providers/gcm.js index fa93062..3c4b9ab 100644 --- a/lib/providers/gcm.js +++ b/lib/providers/gcm.js @@ -1,20 +1,20 @@ -// Copyright IBM Corp. 2013,2015. All Rights Reserved. +// Copyright IBM Corp. 2013,2019. All Rights Reserved. // Node module: loopback-component-push // This file is licensed under the Artistic License 2.0. // License text available at https://opensource.org/licenses/Artistic-2.0 'use strict'; -var g = require('strong-globalize')(); +const g = require('strong-globalize')(); -var inherits = require('util').inherits; -var extend = require('util')._extend; -var EventEmitter = require('events').EventEmitter; -var gcm = require('node-gcm'); -var debug = require('debug')('loopback:component:push:provider:gcm'); +const inherits = require('util').inherits; +const extend = require('util')._extend; +const EventEmitter = require('events').EventEmitter; +const gcm = require('node-gcm'); +const debug = require('debug')('loopback:component:push:provider:gcm'); function GcmProvider(pushSettings) { - var settings = pushSettings.gcm || {}; + const settings = pushSettings.gcm || {}; this._setupPushConnection(settings); } @@ -28,21 +28,21 @@ GcmProvider.prototype._setupPushConnection = function(options) { }; GcmProvider.prototype.pushNotification = function(notification, deviceToken) { - var self = this; + const self = this; - var registrationIds = (typeof deviceToken == 'string') ? + const registrationIds = (typeof deviceToken == 'string') ? [deviceToken] : deviceToken; - var message = this._createMessage(notification); + const message = this._createMessage(notification); debug('Sending message to %j: %j', registrationIds, message); this._connection.send(message, registrationIds, 3, function(err, result) { if (!err && result && result.failure) { - var devicesGoneRegistrationIds = []; - var errors = []; - var code; + const devicesGoneRegistrationIds = []; + const errors = []; + let code; result.results.forEach(function(value, index) { code = value && value.error; - if (code === 'NotRegistered' || code === 'InvalidRegistration') { + if (code === 'NotRegistered' || code === 'InvalidRegistration') { debug('Device %j is no longer registered.', registrationIds[index]); devicesGoneRegistrationIds.push(registrationIds[index]); } else if (code) { @@ -73,13 +73,13 @@ GcmProvider.prototype.pushNotification = function(notification, deviceToken) { GcmProvider.prototype._createMessage = function(notification) { // Message parameters are documented here: // https://developers.google.com/cloud-messaging/server-ref - var message = new gcm.Message({ + const message = new gcm.Message({ timeToLive: notification.getTimeToLiveInSecondsFromNow(), collapseKey: notification.collapseKey, delayWhileIdle: notification.delayWhileIdle, }); - var propNames = Object.keys(notification); + const propNames = Object.keys(notification); // GCM does not have reserved message parameters for alert or badge, adding them as data. propNames.push('alert', 'badge'); @@ -90,5 +90,24 @@ GcmProvider.prototype._createMessage = function(notification) { } }); + addKey(message, 'title', notification, 'messageFrom'); + addKey(message, 'body', notification, 'alert'); + + ['icon', 'sound', 'badge', 'tag', 'color', 'click_action'] + .forEach(function(prop) { + if (notification[prop]) { + addKey(message, prop, notification); + } + }); + return message; }; + +function addKey(message, key, notification, prop) { + prop = prop || key; + if (notification.dataOnly) { + message.addData(key, notification[prop]); + } else { + message.addNotification(key, notification[prop]); + } +} diff --git a/lib/providers/index.js b/lib/providers/index.js index 4406d33..ef81a64 100644 --- a/lib/providers/index.js +++ b/lib/providers/index.js @@ -1,4 +1,4 @@ -// Copyright IBM Corp. 2013. All Rights Reserved. +// Copyright IBM Corp. 2013,2019. All Rights Reserved. // Node module: loopback-component-push // This file is licensed under the Artistic License 2.0. // License text available at https://opensource.org/licenses/Artistic-2.0 diff --git a/lib/push-connector.js b/lib/push-connector.js index a7b5e78..60432c9 100644 --- a/lib/push-connector.js +++ b/lib/push-connector.js @@ -1,31 +1,31 @@ -// Copyright IBM Corp. 2013,2015. All Rights Reserved. +// Copyright IBM Corp. 2013,2019. All Rights Reserved. // Node module: loopback-component-push // This file is licensed under the Artistic License 2.0. // License text available at https://opensource.org/licenses/Artistic-2.0 'use strict'; -var loopback = require('loopback'); -var PushManager = require('./push-manager'); +const loopback = require('loopback'); +const PushManager = require('./push-manager'); /** * Export the initialize method to Loopback DataSource * @param {Object} dataSource Loopback dataSource (Memory, etc). * @param {Function} callback (unused) */ exports.initialize = function(dataSource, callback) { - var settings = dataSource.settings || {}; + const settings = dataSource.settings || {}; - // Create an instance of the APNSManager - var connector = new PushManager(settings); + // Create an instance of the APNSManager + const connector = new PushManager(settings); dataSource.connector = connector; dataSource.connector.dataSource = dataSource; connector.DataAccessObject = function() {}; - for (var m in PushManager.prototype) { - var method = PushManager.prototype[m]; + for (const m in PushManager.prototype) { + const method = PushManager.prototype[m]; if ('function' === typeof method) { connector.DataAccessObject[m] = method.bind(connector); - for (var k in method) { + for (const k in method) { connector.DataAccessObject[m][k] = method[k]; } } diff --git a/lib/push-manager.js b/lib/push-manager.js index c585990..a6ec392 100644 --- a/lib/push-manager.js +++ b/lib/push-manager.js @@ -1,24 +1,24 @@ -// Copyright IBM Corp. 2013,2015. All Rights Reserved. +// Copyright IBM Corp. 2013,2019. All Rights Reserved. // Node module: loopback-component-push // This file is licensed under the Artistic License 2.0. // License text available at https://opensource.org/licenses/Artistic-2.0 'use strict'; -var g = require('strong-globalize')(); +const g = require('strong-globalize')(); -var assert = require('assert'); -var inherits = require('util').inherits; -var EventEmitter = require('events').EventEmitter; -var format = require('util').format; -var async = require('async'); -var providers = require('./providers'); -var loopback = require('loopback'); -var NodeCache = require('node-cache'); -var debug = require('debug')('loopback:component:push:push-manager'); +const assert = require('assert'); +const inherits = require('util').inherits; +const EventEmitter = require('events').EventEmitter; +const format = require('util').format; +const async = require('async'); +const providers = require('./providers'); +const loopback = require('loopback'); +const NodeCache = require('node-cache'); +const debug = require('debug')('loopback:component:push:push-manager'); -var Installation = require('../models').Installation; -var Notification = require('../models').Notification; +const Installation = require('../models').Installation; +const Notification = require('../models').Notification; /*! * Exports a function to bootstrap PushManager @@ -128,12 +128,12 @@ PushManager.providers = { * matching the deviceType (android, ios) */ PushManager.prototype.configureProvider = function(deviceType, pushSettings) { - var Provider = PushManager.providers[deviceType]; + const Provider = PushManager.providers[deviceType]; if (!Provider) { return null; } - var provider = new Provider(pushSettings); + const provider = new Provider(pushSettings); provider.on('devicesGone', function(deviceTokens) { this.Installation.destroyAll({ deviceType: deviceType, @@ -156,11 +156,11 @@ PushManager.prototype.configureProvider = function(deviceType, pushSettings) { */ PushManager.prototype.configureApplication = function(appId, deviceType, cb) { assert.ok(cb, 'callback should be defined'); - var self = this; - var msg; + const self = this; + let msg; // Check the cache first - var cacheApp = self.applicationsCache.get(appId); + let cacheApp = self.applicationsCache.get(appId); if (cacheApp && cacheApp[deviceType]) { return process.nextTick(function() { cb(null, cacheApp[deviceType]); @@ -178,7 +178,8 @@ PushManager.prototype.configureApplication = function(appId, deviceType, cb) { if (!application) { msg = format( 'Cannot configure push notifications - unknown application id %j', - appId); + appId + ); debug('Error: %s', msg); err = new Error(msg); @@ -186,11 +187,12 @@ PushManager.prototype.configureApplication = function(appId, deviceType, cb) { return cb(err); } - var pushSettings = application.pushSettings; + const pushSettings = application.pushSettings; if (!pushSettings) { msg = format( 'No push settings configured for application %j (id: %j)', - application.name, application.id); + application.name, application.id + ); debug('Error: %s', msg); err = new Error(msg); @@ -209,7 +211,7 @@ PushManager.prototype.configureApplication = function(appId, deviceType, cb) { deviceType ); - var provider = self.configureProvider(deviceType, pushSettings); + const provider = self.configureProvider(deviceType, pushSettings); if (!provider) { msg = 'There is no provider registered for deviceType ' + deviceType; @@ -236,11 +238,11 @@ PushManager.prototype.configureApplication = function(appId, deviceType, cb) { */ PushManager.prototype.notifyById = function(installationId, notification, cb) { assert.ok(cb, 'callback should be defined'); - var self = this; + const self = this; self.Installation.findById(installationId, function(err, installation) { if (err) return cb(err); if (!installation) { - var msg = 'Installation id ' + installationId + ' not found'; + const msg = 'Installation id ' + installationId + ' not found'; debug('notifyById failed: ' + msg); err = new Error(msg); err.details = {installationId: installationId}; @@ -258,10 +260,10 @@ PushManager.prototype.notifyById = function(installationId, notification, cb) { * @param {function(Error=)} cb */ PushManager.prototype.notifyByQuery = function(installationQuery, notification, -cb) { + cb) { assert.ok(cb, 'callback should be defined'); - var self = this; - var filter = {where: installationQuery}; + const self = this; + const filter = {where: installationQuery}; self.Installation.find(filter, function(err, installationList) { if (err) return cb(err); async.each( @@ -288,9 +290,9 @@ PushManager.prototype.notify = function(installation, notification, cb) { return cb(new Error(g.f('notification must be an object'))); } - var appId = installation.appId; - var deviceToken = installation.deviceToken; - var deviceType = installation.deviceType; + const appId = installation.appId; + const deviceToken = installation.deviceToken; + const deviceType = installation.deviceType; // Normalize the notification from a plain object // for remote calls @@ -320,7 +322,7 @@ PushManager.prototype.notify = function(installation, notification, cb) { ); }; - /** +/** * Push notification to installations for given devices tokens, device type and app. * * @param {String} appId application id @@ -330,7 +332,7 @@ PushManager.prototype.notify = function(installation, notification, cb) { * @param {function(Error=)} cb */ PushManager.prototype.notifyMany = function(appId, deviceType, deviceTokens, -notification, cb) { + notification, cb) { assert(appId, 'appId should be defined'); assert(deviceType, 'deviceType should be defined'); assert(cb, 'callback should be defined'); @@ -343,8 +345,8 @@ notification, cb) { return cb(new Error(g.f('deviceTokens must be an array'))); } - // Normalize the notification from a plain object - // for remote calls + // Normalize the notification from a plain object + // for remote calls if (!(notification instanceof this.Notification)) { notification = new this.Notification(notification); if (!notification.isValid()) { @@ -353,15 +355,15 @@ notification, cb) { } this.configureApplication( - appId, - deviceType, - function(err, provider) { - if (err) { return cb(err); } - - provider.pushNotification(notification, deviceTokens); - cb(); - } - ); + appId, + deviceType, + function(err, provider) { + if (err) { return cb(err); } + + provider.pushNotification(notification, deviceTokens); + cb(); + } + ); }; /*! @@ -372,7 +374,7 @@ notification, cb) { */ function setRemoting(fn, options) { options = options || {}; - for (var opt in options) { + for (const opt in options) { if (options.hasOwnProperty(opt)) { fn[opt] = options[opt]; } diff --git a/models/index.js b/models/index.js index 64073f0..bde9fbd 100644 --- a/models/index.js +++ b/models/index.js @@ -1,4 +1,4 @@ -// Copyright IBM Corp. 2015. All Rights Reserved. +// Copyright IBM Corp. 2015,2019. All Rights Reserved. // Node module: loopback-component-push // This file is licensed under the Artistic License 2.0. // License text available at https://opensource.org/licenses/Artistic-2.0 @@ -7,16 +7,16 @@ // mostly borrowed from // https://github.com/strongloop/loopback-component-passport/blob/master/lib/index.js -var loopback = require('loopback'); -var DataModel = loopback.PersistedModel || loopback.DataModel; +const loopback = require('loopback'); +const DataModel = loopback.PersistedModel || loopback.DataModel; function loadModel(jsonFile) { - var modelDefinition = require(jsonFile); + const modelDefinition = require(jsonFile); return DataModel.extend(modelDefinition.name, modelDefinition.properties); } -var InstallationModel = loadModel('./installation.json'); -var NotificationModel = loadModel('./notification.json'); +const InstallationModel = loadModel('./installation.json'); +const NotificationModel = loadModel('./notification.json'); /** * Export two model classes as properties diff --git a/models/installation.js b/models/installation.js index 3fb80e2..a6f7cb3 100644 --- a/models/installation.js +++ b/models/installation.js @@ -1,11 +1,11 @@ -// Copyright IBM Corp. 2013,2015. All Rights Reserved. +// Copyright IBM Corp. 2013,2019. All Rights Reserved. // Node module: loopback-component-push // This file is licensed under the Artistic License 2.0. // License text available at https://opensource.org/licenses/Artistic-2.0 'use strict'; -var _ = require('lodash'); +const _ = require('lodash'); /** * Installation Model connects a mobile application to a device, the user and @@ -31,7 +31,7 @@ var _ = require('lodash'); */ module.exports = function(Installation) { Installation.observe('before save', function trip(ctx, next) { - var install = ctx.instance || ctx.data; + const install = ctx.instance || ctx.data; install.modified = new Date(); next(); }); @@ -48,7 +48,7 @@ module.exports = function(Installation) { cb = appVersion; appVersion = undefined; } - var filter = {where: { + const filter = {where: { appId: appId, appVersion: appVersion, deviceType: deviceType}, @@ -63,7 +63,7 @@ module.exports = function(Installation) { * @param {function(Error=,Installation[])} cb Callback function passed to find() with `cb(err, obj[])` signature. */ Installation.findByUser = function(deviceType, userId, cb) { - var filter = {where: {userId: userId, deviceType: deviceType}}; + const filter = {where: {userId: userId, deviceType: deviceType}}; this.find(filter, cb); }; @@ -77,7 +77,7 @@ module.exports = function(Installation) { if (typeof subscriptions === 'string') { subscriptions = subscriptions.split(/[\s,]+/); } - var filter = {where: { + const filter = {where: { subscriptions: {inq: subscriptions}, deviceType: deviceType, }, @@ -99,7 +99,7 @@ module.exports = function(Installation) { fn.shared = true; } - var aDefs = {type: 'string', http: {source: 'query'}}; + const aDefs = {type: 'string', http: {source: 'query'}}; setRemoting(Installation.findByApp, { description: 'Find installations by application id', diff --git a/models/notification.js b/models/notification.js index 16b7dda..10c01c0 100644 --- a/models/notification.js +++ b/models/notification.js @@ -1,4 +1,4 @@ -// Copyright IBM Corp. 2013,2015. All Rights Reserved. +// Copyright IBM Corp. 2013,2019. All Rights Reserved. // Node module: loopback-component-push // This file is licensed under the Artistic License 2.0. // License text available at https://opensource.org/licenses/Artistic-2.0 @@ -45,7 +45,7 @@ module.exports = function(Notification) { Notification.hideInternalProperties = true; Notification.observe('before save', function trip(ctx, next) { - var notification = ctx.instance || ctx.data; + const notification = ctx.instance || ctx.data; notification.modified = notification.scheduledTime = new Date(); next(); }); diff --git a/package.json b/package.json index 8dba21d..dda34b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-component-push", - "version": "3.0.0-alpha.1", + "version": "3.5.0", "description": "Loopback Push Notification", "keywords": [ "StrongLoop Labs", @@ -12,30 +12,31 @@ "main": "index.js", "scripts": { "lint": "eslint .", + "lint:fix": "eslint . --fix", "test": "mocha", "posttest": "npm run lint" }, "engines": { - "node": ">=4" + "node": ">=8" }, "dependencies": { - "apn": "^1.7.5", + "apn": "^v2.1.2", "async": "^1.5.2", - "debug": "^2.2.0", - "lodash": "^3.10.1", + "debug": "^3.1.0", + "lodash": "^4.17.11", "mpns": "^2.1.0", "node-cache": "^3.2.1", - "node-gcm": "^0.14.0", - "strong-globalize": "^2.6.2" + "node-gcm": "^1.0.0", + "strong-globalize": "^4.1.1" }, "devDependencies": { "chai": "^2.3.0", - "deep-extend": "^0.4.0", - "eslint": "^2.13.1", - "eslint-config-loopback": "^4.0.0", + "deep-extend": "^0.5.1", + "eslint": "^4.18.2", + "eslint-config-loopback": "^13.1.0", "loopback": "^3.0.0", - "loopback-connector-mongodb": "^1.8.0", - "mocha": "^2.2.0", + "loopback-connector-mongodb": "^5.0.0", + "mocha": "^4.0.0", "should": "^6.0.0", "sinon": "^1.14.0" }, @@ -43,5 +44,6 @@ "type": "git", "url": "https://github.com/strongloop/loopback-component-push.git" }, - "license": "Artistic-2.0" + "license": "Artistic-2.0", + "author": "IBM Corp." } diff --git a/test/apns.provider.test.js b/test/apns.provider.test.js index 8cc7fc3..484f366 100644 --- a/test/apns.provider.test.js +++ b/test/apns.provider.test.js @@ -1,20 +1,48 @@ -// Copyright IBM Corp. 2013,2015. All Rights Reserved. +// Copyright IBM Corp. 2013,2019. All Rights Reserved. // Node module: loopback-component-push // This file is licensed under the Artistic License 2.0. // License text available at https://opensource.org/licenses/Artistic-2.0 'use strict'; -var fs = require('fs'); -var path = require('path'); -var ApnsProvider = require('../lib/providers/apns'); -var mockery = require('./helpers/mockery').apns; -var objectMother = require('./helpers/object-mother'); +const fs = require('fs'); +const path = require('path'); +const expect = require('chai').expect; +const assert = require('assert'); +const sinon = require('sinon'); +const loopback = require('loopback'); +const ApnsProvider = require('../lib/providers/apns'); +const mockery = require('./helpers/mockery').apns; +const objectMother = require('./helpers/object-mother'); + +const aDeviceToken = 'a-device-token'; +const defaultConfiguration = { + apns: { + token: { + keyId: 'key_id', + key: 'key', + teamId: 'team_id', + }, + bundle: 'ch.test.app', + }, +}; + +const ds = loopback.createDataSource('db', { + connector: loopback.Memory, +}); + +const Application = loopback.Application; +Application.attachTo(ds); + +const PushConnector = require('../'); +const Installation = PushConnector.Installation; +Installation.attachTo(ds); -var aDeviceToken = 'a-device-token'; +const Notification = PushConnector.Notification; +Notification.attachTo(ds); describe('APNS provider', function() { - var provider; + let provider; describe('in sandbox', function() { beforeEach(mockery.setUp); @@ -26,14 +54,14 @@ describe('APNS provider', function() { it('sends Notification as an APN message', function(done) { givenProviderWithConfig(); - var notification = aNotification({ + const notification = aNotification({ aKey: 'a-value', }); provider.pushNotification(notification, aDeviceToken); - var apnArgs = mockery.firstPushNotificationArgs(); + const apnArgs = mockery.firstPushNotificationArgs(); - var note = apnArgs[0]; + const note = apnArgs[0]; expect(note.expiry, 'expiry').to.equal(0); expect(note.alert, 'alert').to.equal(undefined); expect(note.badge, 'badge').to.equal(undefined); @@ -49,7 +77,9 @@ describe('APNS provider', function() { it('passes through special APN parameters', function(done) { givenProviderWithConfig(); - var notification = aNotification({ + const notification = aNotification({ + alert: 'You have a message from StrongLoop', + messageFrom: 'StrongLoop', contentAvailable: true, category: 'my-category', urlArgs: ['foo', 'bar'], @@ -57,269 +87,219 @@ describe('APNS provider', function() { }); provider.pushNotification(notification, aDeviceToken); - var apnArgs = mockery.firstPushNotificationArgs(); + const apnArgs = mockery.firstPushNotificationArgs(); - var note = apnArgs[0]; - var payload = note.toJSON(); - expect(payload.aps['content-available'], 'aps.content-available').to - .equal(1); + const note = apnArgs[0]; + const payload = note.toJSON(); + expect( + payload.aps['content-available'], + 'aps.content-available' + ).to.equal(1); expect(payload.aps.category, 'aps.category').to.equal('my-category'); expect(payload.aps['url-args'], 'aps.url-args').to.have.length(2); expect(payload.arbitrary, 'arbitrary').to.equal('baz'); + expect(payload.aps.alert.title, 'title').to.equal('StrongLoop'); + expect(payload.aps.alert.body, 'body').to.equal( + 'You have a message from StrongLoop' + ); done(); }); it('raises "devicesGone" event when feedback arrives', function(done) { - givenProviderWithConfig({ - apns: { - feedbackOptions: {}, - }, + givenProviderWithConfig(); + + const notification = aNotification({ + aKey: 'a-value', }); - var eventSpy = sinon.spy(); + + const eventSpy = sinon.spy(); + provider.on('devicesGone', eventSpy); + provider.pushNotification(notification, aDeviceToken); - var devices = [aDeviceToken]; - mockery.emitFeedback(devices); + // HACK: Timeout does not work at this point + Promise.resolve(true).then( + function() { + assert(eventSpy.called); + expect(eventSpy.args[0]).to.deep.equal([ + ['some_failing_device_token'], + ]); - expect(eventSpy.args[0]).to.deep.equal([devices]); - done(); + done(); + }, + function() {} + ); }); it('converts expirationInterval to APNS expiry', function() { givenProviderWithConfig(); - var notification = aNotification({ - expirationInterval: 1, /* second */ + const notification = aNotification({ + expirationInterval: 1, + /* second */ }); provider.pushNotification(notification, aDeviceToken); - var note = mockery.firstPushNotificationArgs()[0]; + const note = mockery.firstPushNotificationArgs()[0]; expect(note.expiry).to.equal(1); }); it('converts expirationTime to APNS expiry relative to now', function() { givenProviderWithConfig(); - var notification = aNotification({ + const notification = aNotification({ expirationTime: new Date(this.clock.now + 1000 /* 1 second */), }); provider.pushNotification(notification, aDeviceToken); - var note = mockery.firstPushNotificationArgs()[0]; + const note = mockery.firstPushNotificationArgs()[0]; expect(note.expiry).to.equal(1); }); it('ignores Notification properties not applicable', function() { givenProviderWithConfig(); - var notification = aNotification( - objectMother.allNotificationProperties()); + const notification = aNotification( + objectMother.allNotificationProperties() + ); provider.pushNotification(notification, aDeviceToken); - var note = mockery.firstPushNotificationArgs()[0]; + const note = mockery.firstPushNotificationArgs()[0]; expect(note.payload).to.eql({}); }); }); - describe('in dev env', function() { - var notification; - - beforeEach(function setUp() { - notification = new Notification(); - }); - - it('emits "error" event when certData is invalid', function(done) { + describe('APNS settings', function() { + it('populates bundle/token data', function(done) { givenProviderWithConfig({ apns: { - certData: 'invalid-data', - pushOptions: { - gateway: '127.0.0.1', + token: { + keyId: 'my_key_id', + key: 'my_key', + teamId: 'team_id', }, + bundle: 'my_bundle_id', }, }); - var eventSpy = sinon.spy(); - provider.on('error', eventSpy); - provider.pushNotification(notification, aDeviceToken); + expect(provider._pushOptions).to.deep.equal({ + token: { + keyId: 'my_key_id', + key: 'my_key', + teamId: 'team_id', + }, + bundle: 'my_bundle_id', + production: false, + }); - // wait for the provider to attempt to connect - setTimeout(function() { - expect(eventSpy.called, 'error event should be emitted') - .to.equal(true); - var args = eventSpy.firstCall.args; - expect(args[0]).to.be.instanceOf(Error); - done(); - }, 50); + done(); }); - it('emits "error" when gateway cannot be reached', function(done) { - var CONNECT_TIMEOUT = 2000; - this.timeout(1.5 * CONNECT_TIMEOUT); + it('uses by default the sandbox mode', function(done) { givenProviderWithConfig({ apns: { - certData: objectMother.apnsDevCert(), - keyData: objectMother.apnsDevKey(), - pushOptions: { - gateway: '127.0.0.1', + token: { + keyId: 'my_key_id', + key: 'my_key', + teamId: 'team_id', }, + bundle: 'my_bundle_id', }, }); - var eventSpy = sinon.spy(); - provider.on('error', eventSpy); - - provider.pushNotification(notification, aDeviceToken); - - var start = Date.now(); - - // wait for the provider to attempt to connect - // periodically check whether it has happened yet - var interval = setInterval(function() { - var elapsed = Date.now() - start; - if (!eventSpy.called && elapsed < CONNECT_TIMEOUT) - return; // still connecting - - clearInterval(interval); - - expect(eventSpy.called, 'error event should be emitted') - .to.equal(true); - - var args = eventSpy.firstCall.args; - expect(args[0]).to.be.instanceOf(Error); - expect(args[0].code).to.equal('ECONNREFUSED'); - done(); - }, 50); + expect(provider._pushOptions.production === false); + done(); }); - }); - describe('APNS settings', function() { - it('populates cert/key data', function(done) { + it('uses production mode when set', function(done) { givenProviderWithConfig({ apns: { - certData: objectMother.apnsDevCert(), - keyData: objectMother.apnsDevKey(), - pushOptions: { - gateway: '127.0.0.1', + token: { + keyId: 'my_key_id', + key: 'my_key', + teamId: 'team_id', }, + bundle: 'my_bundle_id', }, + production: true, }); - expect(provider._pushOptions).to.deep.equal({ - cert: objectMother.apnsDevCert(), - key: objectMother.apnsDevKey(), - gateway: '127.0.0.1', - production: false, - }); - expect(provider._feedbackOptions).to.deep.equal({ - cert: objectMother.apnsDevCert(), - key: objectMother.apnsDevKey(), - gateway: 'feedback.sandbox.push.apple.com', - production: false, - }); + + expect(provider._pushOptions.production === true); done(); }); - it('populates dev gateways without overriding', function(done) { + + it('uses sandbox mode when set', function(done) { givenProviderWithConfig({ apns: { - pushOptions: { - gateway: 'push.test.com', - port: 1111, - }, - feedbackOptions: { - gateway: 'feedback.test.com', - port: 1112, + token: { + keyId: 'my_key_id', + key: 'my_key', + teamId: 'team_id', }, + bundle: 'my_bundle_id', }, - }); - expect(provider._pushOptions).to.deep.equal({ - gateway: 'push.test.com', - port: 1111, - production: false, - }); - expect(provider._feedbackOptions).to.deep.equal({ - gateway: 'feedback.test.com', - port: 1112, production: false, }); + + expect(provider._pushOptions.production === false); done(); }); - it('populates dev gateways', function(done) { - givenProviderWithConfig({ - apns: { - // intentionally omit the pushOptions for test - /* - pushOptions: { - }, - */ - feedbackOptions: { - interval: 300, + + it('reports error when bundle is not specified', function(done) { + const test = function() { + givenProviderWithConfig({ + apns: { + token: { + keyId: 'my_key_id', + key: 'my_key', + teamId: 'team_id', + }, }, - }, - }); - expect(provider._pushOptions).to.deep.equal({ - gateway: 'gateway.sandbox.push.apple.com', - production: false, - }); - expect(provider._feedbackOptions).to.deep.equal({ - gateway: 'feedback.sandbox.push.apple.com', - interval: 300, - production: false, - }); + }); + }; + + assert.throws(test, Error, 'Error thrown'); done(); }); - it('populates prod gateways', function(done) { - givenProviderWithConfig({ - apns: { - production: true, - pushOptions: {}, - feedbackOptions: { - interval: 300, - production: false, - }, - }, - }); - expect(provider._pushOptions).to.deep.equal({ - gateway: 'gateway.push.apple.com', - production: true, - }); - expect(provider._feedbackOptions).to.deep.equal({ - gateway: 'feedback.push.apple.com', - interval: 300, - production: true, - }); + + it('reports error when token is not specified', function(done) { + const test = function() { + givenProviderWithConfig({ + bundle: 'the_bundle', + }); + }; + + assert.throws(test, Error, 'Error thrown'); done(); }); - it('override prod gateways', function(done) { - givenProviderWithConfig({ - apns: { - production: true, - pushOptions: { - gateway: 'invalid', - port: 1111, - }, - feedbackOptions: { - gateway: 'invalid', - port: 1112, - interval: 300, + + it('reports error when token is missing a property', function(done) { + const test = function() { + givenProviderWithConfig({ + token: { + keyId: 'key_id', + key: 'key', }, - }, - }); - expect(provider._pushOptions).to.deep.equal({ - gateway: 'gateway.push.apple.com', - port: 2195, - production: true, - }); - expect(provider._feedbackOptions).to.deep.equal({ - gateway: 'feedback.push.apple.com', - port: 2196, - interval: 300, - production: true, - }); + bundle: 'the_bundle', + }); + }; + + assert.throws(test, Error, 'Error thrown'); done(); }); }); + /** + * Creates a provider with specified configuration. If configuration is left empty, a default one is created. + * @param pushSettings + */ function givenProviderWithConfig(pushSettings) { + // use a sensible default if nothing was specified + if (typeof pushSettings === 'undefined') { + pushSettings = defaultConfiguration; + } + provider = new ApnsProvider(pushSettings); } diff --git a/test/common.js b/test/common.js deleted file mode 100644 index 0ac57e6..0000000 --- a/test/common.js +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright IBM Corp. 2015. All Rights Reserved. -// Node module: loopback-component-push -// This file is licensed under the Artistic License 2.0. -// License text available at https://opensource.org/licenses/Artistic-2.0 - -'use strict'; - -/* exported global */ -global.chai = require('chai'); -global.should = require('chai').should(); -global.expect = require('chai').expect; -global.AssertionError = require('chai').AssertionError; -global.loopback = require('loopback'); -global.assert = require('assert'); - -global.sinon = require('sinon'); - -global.ds = global.loopback.createDataSource('db', { - connector: global.loopback.Memory, -}); - -global.Application = global.loopback.Application; -global.Application.attachTo(global.ds); - -global.PushConnector = require('../'); -global.Installation = PushConnector.Installation; -global.Installation.attachTo(global.ds); - -global.Notification = PushConnector.Notification; -global.Notification.attachTo(global.ds); diff --git a/test/device-registration.test.js b/test/device-registration.test.js index c74d1ba..ea421c1 100644 --- a/test/device-registration.test.js +++ b/test/device-registration.test.js @@ -1,53 +1,69 @@ -// Copyright IBM Corp. 2013,2015. All Rights Reserved. +// Copyright IBM Corp. 2013,2019. All Rights Reserved. // Node module: loopback-component-push // This file is licensed under the Artistic License 2.0. // License text available at https://opensource.org/licenses/Artistic-2.0 'use strict'; +const assert = require('assert'); +const loopback = require('loopback'); + +const ds = loopback.createDataSource('db', { + connector: loopback.Memory, +}); +const PushConnector = require('../'); +const Installation = PushConnector.Installation; +Installation.attachTo(ds); describe('Installation', function() { - var registration = null; + let registration = null; it('registers a new installation', function(done) { - var token = '75624450 3c9f95b4 9d7ff821 20dc193c a1e3a7cb 56f60c2e ' + + const token = + '75624450 3c9f95b4 9d7ff821 20dc193c a1e3a7cb 56f60c2e ' + 'f2a19241 e8f33305'; - Installation.create({ - appId: 'MyLoopbackApp', - appVersion: '1', - userId: 'raymond', - deviceToken: token, - deviceType: 'ios', - created: new Date(), - modified: new Date(), - status: 'Active', - }, function(err, result) { - if (err) { - console.error(err); - done(err, result); - return; - } else { - var reg = result; - assert.equal(reg.appId, 'MyLoopbackApp'); - assert.equal(reg.userId, 'raymond'); - assert.equal(reg.deviceType, 'ios'); - assert.equal(reg.deviceToken, token); - - assert(reg.created); - assert(reg.modified); - - registration = reg; - - Installation.findByApp('ios', 'MyLoopbackApp', function(err, results) { - assert(!err); - assert.equal(results.length, 1); - var reg = results[0]; + Installation.create( + { + appId: 'MyLoopbackApp', + appVersion: '1', + userId: 'raymond', + deviceToken: token, + deviceType: 'ios', + created: new Date(), + modified: new Date(), + status: 'Active', + }, + function(err, result) { + if (err) { + console.error(err); + done(err, result); + return; + } else { + const reg = result; assert.equal(reg.appId, 'MyLoopbackApp'); assert.equal(reg.userId, 'raymond'); assert.equal(reg.deviceType, 'ios'); assert.equal(reg.deviceToken, token); - done(err, results); - }); + + assert(reg.created); + assert(reg.modified); + + registration = reg; + + Installation.findByApp('ios', 'MyLoopbackApp', function( + err, + results + ) { + assert(!err); + assert.equal(results.length, 1); + const reg = results[0]; + assert.equal(reg.appId, 'MyLoopbackApp'); + assert.equal(reg.userId, 'raymond'); + assert.equal(reg.deviceType, 'ios'); + assert.equal(reg.deviceToken, token); + done(err, results); + }); + } } - }); + ); }); }); diff --git a/test/gcm.provider.test.js b/test/gcm.provider.test.js index 5c56eb2..4e57191 100644 --- a/test/gcm.provider.test.js +++ b/test/gcm.provider.test.js @@ -1,17 +1,20 @@ -// Copyright IBM Corp. 2013,2015. All Rights Reserved. +// Copyright IBM Corp. 2013,2019. All Rights Reserved. // Node module: loopback-component-push // This file is licensed under the Artistic License 2.0. // License text available at https://opensource.org/licenses/Artistic-2.0 'use strict'; -var extend = require('util')._extend; -var GcmProvider = require('../lib/providers/gcm'); -var mockery = require('./helpers/mockery').gcm; -var objectMother = require('./helpers/object-mother'); +const expect = require('chai').expect; +const sinon = require('sinon'); +const extend = require('util')._extend; +const GcmProvider = require('../lib/providers/gcm'); +const mockery = require('./helpers/mockery').gcm; +const objectMother = require('./helpers/object-mother'); +const loopback = require('loopback'); -var aDeviceToken = 'a-device-token'; -var aDeviceTokenList = [ +const aDeviceToken = 'a-device-token'; +const aDeviceTokenList = [ 'first-device-token', 'second-device-token', 'third-device-token', @@ -19,77 +22,99 @@ var aDeviceTokenList = [ 'fifth-device-token', ]; +const ds = loopback.createDataSource('db', { + connector: loopback.Memory, +}); + +const Application = loopback.Application; +Application.attachTo(ds); + +const PushConnector = require('../'); +const Installation = PushConnector.Installation; +Installation.attachTo(ds); + +const Notification = PushConnector.Notification; +Notification.attachTo(ds); + describe('GCM provider', function() { - var provider; + let provider; beforeEach(mockery.setUp); beforeEach(setUpFakeTimers); - beforeEach(function() { givenProviderWithConfig(); }); + beforeEach(function() { + givenProviderWithConfig(); + }); afterEach(tearDownFakeTimers); afterEach(mockery.tearDown); describe('for single device token', function() { it('sends Notification as a GCM message', function(done) { - var notification = aNotification({aKey: 'a-value'}); + const notification = aNotification({aKey: 'a-value'}); notification.alert = 'alert message'; notification.badge = 1; provider.pushNotification(notification, aDeviceToken); - var gcmArgs = mockery.firstPushNotificationArgs(); + const gcmArgs = mockery.firstPushNotificationArgs(); - var msg = gcmArgs[0]; - expect(msg.params.collapseKey, 'collapseKey').to - .equal(undefined); + const msg = gcmArgs[0]; + expect(msg.params.collapseKey, 'collapseKey').to.equal(undefined); expect(msg.params.delayWhileIdle, 'delayWhileIdle').to.equal(undefined); expect(msg.params.timeToLive, 'timeToLive').to.equal(undefined); - expect(msg.params.data, 'data').to - .deep.equal({aKey: 'a-value', alert: 'alert message', badge: 1}); + expect(msg.params.data, 'data').to.deep.equal({ + aKey: 'a-value', + alert: 'alert message', + badge: 1, + }); expect(gcmArgs[1]).to.deep.equal([aDeviceToken]); done(); }); it('emits "error" when GCM send fails', function() { - var anError = new Error('test-error'); + const anError = new Error('test-error'); mockery.givenPushNotificationFailsWith(anError); - var eventSpy = spyOnProviderError(); + const eventSpy = spyOnProviderError(); provider.pushNotification(aNotification(), aDeviceToken); - expect(eventSpy.calledOnce, 'error should be emitted once').to - .equal(true); + expect(eventSpy.calledOnce, 'error should be emitted once').to.equal( + true + ); expect(eventSpy.args[0]).to.deep.equal([anError]); }); it('emits "error" event when GCM returns error result', function() { // This is a real result returned by GCM - var errorResult = aGcmResult([{'error': 'MismatchSenderId'}]); + const errorResult = aGcmResult([{error: 'MismatchSenderId'}]); mockery.pushNotificationCallbackArgs = [null, errorResult]; - var eventSpy = spyOnProviderError(); + const eventSpy = spyOnProviderError(); provider.pushNotification(aNotification(), aDeviceToken); - expect(eventSpy.calledOnce, 'error should be emitted once').to - .equal(true); + expect(eventSpy.calledOnce, 'error should be emitted once').to.equal( + true + ); expect(eventSpy.firstCall.args[0].message).to.contain('MismatchSenderId'); }); it('emits "devicesGone" when GCM returns NotRegistered', function(done) { - var errorResult = aGcmResult([{'error': 'NotRegistered'}]); + const errorResult = aGcmResult([{error: 'NotRegistered'}]); mockery.pushNotificationCallbackArgs = [null, errorResult]; - var eventSpy = sinon.spy(); + const eventSpy = sinon.spy(); provider.on('devicesGone', eventSpy); - provider.on('error', function(err) { throw err; }); + provider.on('error', function(err) { + throw err; + }); provider.pushNotification(aNotification(), aDeviceToken); - var expectedIds = [aDeviceToken]; + const expectedIds = [aDeviceToken]; expect(eventSpy.args[0]).to.deep.equal([expectedIds]); done(); }); @@ -97,12 +122,12 @@ describe('GCM provider', function() { describe('for multiple device tokens', function() { it('sends Notification as a GCM message', function(done) { - var notification = aNotification({aKey: 'a-value'}); + const notification = aNotification({aKey: 'a-value'}); provider.pushNotification(notification, aDeviceTokenList); - var gcmArgs = mockery.pushNotification.args[0]; + const gcmArgs = mockery.pushNotification.args[0]; - var msg = gcmArgs[0]; + const msg = gcmArgs[0]; expect(msg.params.collapseKey, 'collapseKey').to.equal(undefined); expect(msg.params.delayWhileIdle, 'delayWhileIdle').to.equal(undefined); expect(msg.params.timeToLive, 'timeToLive').to.equal(undefined); @@ -113,21 +138,24 @@ describe('GCM provider', function() { }); it('handles GCM response for multiple device tokens', function(done) { - var gcmError = new Error('GCM error code: MismatchSenderId, ' + + const gcmError = new Error( + 'GCM error code: MismatchSenderId, ' + 'deviceToken: third-device-token\nGCM error code: ' + - 'MismatchSenderId, deviceToken: fifth-device-token'); - - var gcmResult = aGcmResult([ - {'error': 'InvalidRegistration'}, - {'message_id': '1234567890'}, - {'error': 'MismatchSenderId'}, - {'error': 'NotRegistered'}, - {'error': 'MismatchSenderId'}, + 'MismatchSenderId, deviceToken: fifth-device-token' + ); + + const gcmResult = aGcmResult([ + {error: 'InvalidRegistration'}, + // eslint-disable-next-line + { message_id: '1234567890' }, + {error: 'MismatchSenderId'}, + {error: 'NotRegistered'}, + {error: 'MismatchSenderId'}, ]); mockery.pushNotificationCallbackArgs = [null, gcmResult]; - var eventSpy = sinon.spy(); + const eventSpy = sinon.spy(); provider.on('devicesGone', eventSpy); provider.on('error', function(err) { expect(err.message).to.equal(gcmError.message); @@ -135,56 +163,90 @@ describe('GCM provider', function() { provider.pushNotification(aNotification(), aDeviceTokenList); - var expectedIds = [aDeviceTokenList[0], aDeviceTokenList[3]]; - expect(eventSpy.calledOnce, 'error should be emitted once').to - .equal(true); + const expectedIds = [aDeviceTokenList[0], aDeviceTokenList[3]]; + expect(eventSpy.calledOnce, 'error should be emitted once').to.equal( + true + ); expect(eventSpy.args[0][0]).to.deep.equal(expectedIds); done(); }); }); it('converts expirationInterval to GCM timeToLive', function() { - var notification = aNotification({expirationInterval: 1}); + const notification = aNotification({expirationInterval: 1}); provider.pushNotification(notification, aDeviceToken); - var message = mockery.firstPushNotificationArgs()[0]; + const message = mockery.firstPushNotificationArgs()[0]; expect(message.params.timeToLive).to.equal(1); }); it('converts expirationTime to GCM timeToLive relative to now', function() { - var notification = aNotification({ + const notification = aNotification({ expirationTime: new Date(this.clock.now + 1000 /* 1 second */), }); provider.pushNotification(notification, aDeviceToken); - var message = mockery.firstPushNotificationArgs()[0]; + const message = mockery.firstPushNotificationArgs()[0]; expect(message.params.timeToLive).to.equal(1); }); it('forwards android parameters', function() { - var notification = aNotification({ + const notification = aNotification({ collapseKey: 'a-collapse-key', delayWhileIdle: true, }); provider.pushNotification(notification, aDeviceToken); - var message = mockery.firstPushNotificationArgs()[0]; + const message = mockery.firstPushNotificationArgs()[0]; expect(message.params.collapseKey).to.equal('a-collapse-key'); expect(message.params.delayWhileIdle, 'delayWhileIdle').to.equal(true); }); + it('adds appropriate fcm properties to the notification', function() { + const note = { + messageFrom: 'StrongLoop', + alert: 'Hello from StrongLoop', + icon: 'logo.png', + sound: 'ping.tiff', + badge: 5, + tag: 'alerts', + color: '#ff0000', + // eslint-disable-next-line + click_action: 'OPEN_ACTIVITY_1', + }; + const notification = aNotification(note); + provider.pushNotification(notification, aDeviceToken); + + const message = mockery.firstPushNotificationArgs()[0]; + expect(message.params.notification).to.eql({ + title: note.messageFrom, + body: note.alert, + icon: note.icon, + sound: note.sound, + badge: note.badge, + tag: note.tag, + color: note.color, + // eslint-disable-next-line + click_action: note.click_action, + }); + }); + it('ignores Notification properties not applicable', function() { - var notification = aNotification(objectMother.allNotificationProperties()); + const notification = aNotification( + objectMother.allNotificationProperties() + ); provider.pushNotification(notification, aDeviceToken); - var message = mockery.firstPushNotificationArgs()[0]; - expect(message.params.data).to - .deep.equal({alert: 'an-alert', badge: 1230001}); + const message = mockery.firstPushNotificationArgs()[0]; + expect(message.params.data).to.deep.equal({ + alert: 'an-alert', + badge: 1230001, + }); }); it('ignores Notification properties null or undefined', function() { - var notification = aNotification({ + const notification = aNotification({ aFalse: false, aTrue: true, aNull: null, @@ -192,16 +254,42 @@ describe('GCM provider', function() { }); provider.pushNotification(notification, aDeviceToken); - var message = mockery.firstPushNotificationArgs()[0]; + const message = mockery.firstPushNotificationArgs()[0]; expect(message.params.data).to.deep.equal({aFalse: false, aTrue: true}); }); + it('supports data-only notifications', function() { + const note = { + messageFrom: 'StrongLoop', + alert: 'Hello from StrongLoop', + icon: 'logo.png', + sound: 'ping.tiff', + badge: 5, + dataOnly: true, + }; + const notification = aNotification(note); + provider.pushNotification(notification, aDeviceToken); + + const message = mockery.firstPushNotificationArgs()[0]; + expect(message.params.data).to.eql({ + messageFrom: 'StrongLoop', + alert: 'Hello from StrongLoop', + title: 'StrongLoop', + body: 'Hello from StrongLoop', + icon: 'logo.png', + sound: 'ping.tiff', + badge: 5, + dataOnly: true, + }); + }); + function givenProviderWithConfig(pushSettings) { pushSettings = extend({}, pushSettings); pushSettings.gcm = extend({}, pushSettings.gcm); pushSettings.gcm.pushOptions = extend( {serverKey: 'a-test-server-key'}, - pushSettings.gcm.pushOptions); + pushSettings.gcm.pushOptions + ); provider = new GcmProvider(pushSettings); } @@ -211,20 +299,22 @@ describe('GCM provider', function() { } function aGcmResult(results) { - var success = results.filter(function(item) { + const success = results.filter(function(item) { return item.message_id; }).length; - var failure = results.filter(function(item) { + const failure = results.filter(function(item) { return item.error; }).length; return { - 'multicast_id': 5504081219335647631, - 'success': success, - 'failure': failure, - 'canonical_ids': 0, - 'results': results, + // eslint-disable-next-line + multicast_id: 5504081219335647631, + success: success, + failure: failure, + // eslint-disable-next-line + canonical_ids: 0, + results: results, }; } @@ -237,7 +327,7 @@ describe('GCM provider', function() { } function spyOnProviderError() { - var eventSpy = sinon.spy(); + const eventSpy = sinon.spy(); provider.on('error', eventSpy); return eventSpy; } diff --git a/test/helpers/mockery/apns.mockery.js b/test/helpers/mockery/apns.mockery.js index 50aa80a..2522d8e 100644 --- a/test/helpers/mockery/apns.mockery.js +++ b/test/helpers/mockery/apns.mockery.js @@ -1,4 +1,4 @@ -// Copyright IBM Corp. 2013. All Rights Reserved. +// Copyright IBM Corp. 2013,2019. All Rights Reserved. // Node module: loopback-component-push // This file is licensed under the Artistic License 2.0. // License text available at https://opensource.org/licenses/Artistic-2.0 @@ -9,11 +9,11 @@ // are calling callbacks provided by tests instead of communicating with // the real service -var EventEmitter = require('events').EventEmitter; -var apn = require('apn'); -var sinon = require('sinon'); +const EventEmitter = require('events').EventEmitter; +const apn = require('apn'); +const sinon = require('sinon'); -var mockery = exports; +const mockery = exports; /** * The options passed to `apn.Connection` constructor. @@ -48,17 +48,17 @@ mockery.emitFeedback = function(devices) { * @returns {Array.} */ mockery.firstPushNotificationArgs = function() { - return mockery.pushNotification.firstCall.args; + return mockery.send.firstCall.args; }; -var apnsSnapshot = {}; -var defaultExports = {}; +const apnsSnapshot = {}; +const defaultExports = {}; /** * Setup the mockery. This method should be called before each test. */ exports.setUp = function() { - var key; + let key; for (key in apn) { apnsSnapshot[key] = apn[key]; } @@ -67,23 +67,25 @@ exports.setUp = function() { defaultExports[key] = exports[key]; } - mockery.pushNotification = sinon.spy(); + const expectedResponse = { + failed: [ + { + device: 'some_failing_device_token', + }, + ], + }; + + mockery.send = sinon.spy(function() { + return Promise.resolve(expectedResponse); + }); - apn.Connection = apn.connection = function(opts) { + apn.Provider = apn.provider = function(opts) { mockery.connectionOptions = opts; - var conn = new EventEmitter(); - conn.pushNotification = mockery.pushNotification; - return conn; - }; - apn.Feedback = apn.feedback = function(opts) { - mockery.feedbackOptions = opts; - var feedback = new EventEmitter(); - mockery.emitFeedback = function(devices) { - if (!(devices instanceof Array)) devices = [devices]; - feedback.emit('feedback', devices); - }; - return feedback; + const conn = new EventEmitter(); + conn.send = mockery.send; + + return conn; }; }; @@ -92,7 +94,7 @@ exports.setUp = function() { * This method should be called after each test. */ exports.tearDown = function() { - var key; + let key; for (key in apnsSnapshot) { apn[key] = apnsSnapshot[key]; diff --git a/test/helpers/mockery/gcm.mockery.js b/test/helpers/mockery/gcm.mockery.js index adab9f8..e101dc9 100644 --- a/test/helpers/mockery/gcm.mockery.js +++ b/test/helpers/mockery/gcm.mockery.js @@ -1,4 +1,4 @@ -// Copyright IBM Corp. 2013,2015. All Rights Reserved. +// Copyright IBM Corp. 2013,2019. All Rights Reserved. // Node module: loopback-component-push // This file is licensed under the Artistic License 2.0. // License text available at https://opensource.org/licenses/Artistic-2.0 @@ -9,11 +9,11 @@ // are calling callbacks provided by tests instead of communicating with // the real service -var EventEmitter = require('events').EventEmitter; -var gcm = require('node-gcm'); -var sinon = require('sinon'); +const EventEmitter = require('events').EventEmitter; +const gcm = require('node-gcm'); +const sinon = require('sinon'); -var mockery = exports; +const mockery = exports; /** * The options passed to `gcm.Sender` constructor. @@ -50,14 +50,14 @@ mockery.givenPushNotificationFailsWith = function(err) { mockery.pushNotificationCallbackArgs = [err]; }; -var gcmSnapshot = {}; -var defaultExports = {}; +const gcmSnapshot = {}; +const defaultExports = {}; /** * Setup the mockery. This method should be called before each test. */ exports.setUp = function() { - var key; + let key; for (key in gcm) { gcmSnapshot[key] = gcm[key]; } @@ -71,7 +71,7 @@ exports.setUp = function() { gcm.Sender = function(opts) { mockery.senderOptions = Array.prototype.slice.call(arguments); - var sender = {}; + const sender = {}; sender.send = function(message, registrationId, retries, callback) { mockery.pushNotification.apply(this, arguments); callback.apply(null, mockery.pushNotificationCallbackArgs); @@ -85,7 +85,7 @@ exports.setUp = function() { * This method should be called after each test. */ exports.tearDown = function() { - var key; + let key; for (key in gcmSnapshot) { gcm[key] = gcmSnapshot[key]; diff --git a/test/helpers/mockery/index.js b/test/helpers/mockery/index.js index 6e891b8..a091715 100644 --- a/test/helpers/mockery/index.js +++ b/test/helpers/mockery/index.js @@ -1,4 +1,4 @@ -// Copyright IBM Corp. 2013. All Rights Reserved. +// Copyright IBM Corp. 2013,2019. All Rights Reserved. // Node module: loopback-component-push // This file is licensed under the Artistic License 2.0. // License text available at https://opensource.org/licenses/Artistic-2.0 diff --git a/test/helpers/mockery/stub.mockery.js b/test/helpers/mockery/stub.mockery.js index 36932b8..94eb3c3 100644 --- a/test/helpers/mockery/stub.mockery.js +++ b/test/helpers/mockery/stub.mockery.js @@ -1,4 +1,4 @@ -// Copyright IBM Corp. 2013,2015. All Rights Reserved. +// Copyright IBM Corp. 2013,2019. All Rights Reserved. // Node module: loopback-component-push // This file is licensed under the Artistic License 2.0. // License text available at https://opensource.org/licenses/Artistic-2.0 @@ -8,13 +8,13 @@ // This module provides a mocked environment with a special "stub" // provider that does not depend on any real implementation (GCM, APNS) -var inherits = require('util').inherits; -var EventEmitter = require('events').EventEmitter; -var expect = require('chai').expect; -var sinon = require('sinon'); -var PushManager = require('../../../lib/push-manager'); +const inherits = require('util').inherits; +const EventEmitter = require('events').EventEmitter; +const expect = require('chai').expect; +const sinon = require('sinon'); +const PushManager = require('../../../lib/push-manager'); -var mockery = exports; +const mockery = exports; /** * The device type used for registration with PushManager. @@ -79,13 +79,13 @@ StubProvider.prototype.emitDevicesGone = function(devices) { this.emit('devicesGone', devices); }; -var defaultExports = {}; +const defaultExports = {}; /** * Setup the mockery. This method should be called before each test. */ exports.setUp = function() { - for (var key in exports) { + for (const key in exports) { defaultExports[key] = exports[key]; } @@ -98,7 +98,7 @@ exports.setUp = function() { * This method should be called after each test. */ exports.tearDown = function() { - for (var key in defaultExports) { + for (const key in defaultExports) { exports[key] = defaultExports[key]; } diff --git a/test/helpers/object-mother.js b/test/helpers/object-mother.js index 467cf16..57b90c0 100644 --- a/test/helpers/object-mother.js +++ b/test/helpers/object-mother.js @@ -1,4 +1,4 @@ -// Copyright IBM Corp. 2013,2015. All Rights Reserved. +// Copyright IBM Corp. 2013,2019. All Rights Reserved. // Node module: loopback-component-push // This file is licensed under the Artistic License 2.0. // License text available at https://opensource.org/licenses/Artistic-2.0 @@ -35,10 +35,10 @@ exports.apnsDevKey = function() { return readCredentialsSync('apns_key_dev.pem'); }; -var path = require('path'); -var fs = require('fs'); +const path = require('path'); +const fs = require('fs'); function readCredentialsSync(fileName) { - var relativePath = '../fixtures'; - var credentialsDir = path.resolve(__dirname, relativePath); + const relativePath = '../fixtures'; + const credentialsDir = path.resolve(__dirname, relativePath); return fs.readFileSync(path.join(credentialsDir, fileName), 'UTF-8'); } diff --git a/test/helpers/test-data-builder.js b/test/helpers/test-data-builder.js index e9a4e27..d767c4d 100644 --- a/test/helpers/test-data-builder.js +++ b/test/helpers/test-data-builder.js @@ -1,11 +1,11 @@ -// Copyright IBM Corp. 2015. All Rights Reserved. +// Copyright IBM Corp. 2015,2019. All Rights Reserved. // Node module: loopback-component-push // This file is licensed under the Artistic License 2.0. // License text available at https://opensource.org/licenses/Artistic-2.0 'use strict'; -var extend = require('util')._extend; -var async = require('async'); +const extend = require('util')._extend; +const async = require('async'); module.exports = exports = TestDataBuilder; @@ -82,13 +82,14 @@ TestDataBuilder.prototype.buildTo = function(context, callback) { async.eachSeries( this._definitions, this._buildObject.bind(this), - callback); + callback + ); }; TestDataBuilder.prototype._buildObject = function(definition, callback) { - var defaultValues = this._gatherDefaultPropertyValues(definition.model); - var values = extend(defaultValues, definition.properties || {}); - var resolvedValues = this._resolveValues(values); + const defaultValues = this._gatherDefaultPropertyValues(definition.model); + const values = extend(defaultValues, definition.properties || {}); + const resolvedValues = this._resolveValues(values); definition.model.create(resolvedValues, function(err, result) { if (err) { @@ -96,7 +97,8 @@ TestDataBuilder.prototype._buildObject = function(definition, callback) { 'Cannot build object %j - %s\nDetails: %j', definition, err.message, - err.details); + err.details + ); } else { this._context[definition.name] = result; } @@ -106,9 +108,9 @@ TestDataBuilder.prototype._buildObject = function(definition, callback) { }; TestDataBuilder.prototype._resolveValues = function(values) { - var result = {}; - for (var key in values) { - var val = values[key]; + const result = {}; + for (const key in values) { + let val = values[key]; if (val instanceof Reference) { val = values[key].resolveFromContext(this._context); } @@ -117,23 +119,24 @@ TestDataBuilder.prototype._resolveValues = function(values) { return result; }; -var valueCounter = 0; +let valueCounter = 0; TestDataBuilder.prototype._gatherDefaultPropertyValues = function(Model) { - var result = {}; + const result = {}; Model.forEachProperty(function createDefaultPropertyValue(name) { - var prop = Model.definition.properties[name]; + const prop = Model.definition.properties[name]; if (!prop.required) return; switch (prop.type) { case String: - var generatedString = 'a test ' + name + ' #' + (++valueCounter); + let generatedString = 'a test ' + name + ' #' + (++valueCounter); // If this property has a maximum length, ensure that the generated // string is not longer than the property's max length if (prop.length) { // Chop off the front part of the string so it is equal to the length generatedString = generatedString.substring( - generatedString.length - prop.length); + generatedString.length - prop.length + ); } result[name] = generatedString; break; @@ -143,7 +146,7 @@ TestDataBuilder.prototype._gatherDefaultPropertyValues = function(Model) { case Date: result[name] = new Date( 2222, 12, 12, // yyyy, mm, dd - 12, 12, 12, // hh, MM, ss + 12, 12, 12, // hh, MM, ss ++valueCounter // milliseconds ); break; @@ -170,9 +173,9 @@ function Reference(path) { } Reference.prototype.resolveFromContext = function(context) { - var elements = this._path.split('.'); + const elements = this._path.split('.'); - var result = elements.reduce( + const result = elements.reduce( function(obj, prop) { return obj[prop]; }, diff --git a/test/mocha.opts b/test/mocha.opts index 8382147..7b9581e 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,3 +1,2 @@ --timeout 30000 --reporter spec ---require test/common diff --git a/test/push-manager.test.js b/test/push-manager.test.js index e5de71f..1c533e5 100644 --- a/test/push-manager.test.js +++ b/test/push-manager.test.js @@ -1,17 +1,36 @@ -// Copyright IBM Corp. 2013,2015. All Rights Reserved. +// Copyright IBM Corp. 2013,2019. All Rights Reserved. // Node module: loopback-component-push // This file is licensed under the Artistic License 2.0. // License text available at https://opensource.org/licenses/Artistic-2.0 'use strict'; -var async = require('async'); +const async = require('async'); +const expect = require('chai').expect; +const loopback = require('loopback'); +const sinon = require('sinon'); +const PushManager = require('../lib/push-manager'); +const NodeCache = require('node-cache'); + +const mockery = require('./helpers/mockery').stub; +const TestDataBuilder = require('./helpers/test-data-builder'); +const ref = TestDataBuilder.ref; + +const ds = loopback.createDataSource('db', { + connector: loopback.Memory, +}); + +// Application +const Application = loopback.Application; +Application.attachTo(ds); -var PushManager = require('../lib/push-manager'); -var NodeCache = require('node-cache'); +// Push Connector +const PushConnector = require('../'); +const Installation = PushConnector.Installation; +Installation.attachTo(ds); -var mockery = require('./helpers/mockery').stub; -var TestDataBuilder = require('./helpers/test-data-builder'); -var ref = TestDataBuilder.ref; +// Notification +const Notification = PushConnector.Notification; +Notification.attachTo(ds); describe('PushManager', function() { beforeEach(mockery.setUp); @@ -19,7 +38,7 @@ describe('PushManager', function() { beforeEach(Installation.deleteAll.bind(Installation)); afterEach(mockery.tearDown); - var pushManager, context; + let pushManager, context; beforeEach(function(done) { pushManager = new PushManager(); @@ -30,91 +49,95 @@ describe('PushManager', function() { }); it('deletes devices no longer registered', function(done) { - async.series([ - function arrange(cb) { - new TestDataBuilder() - .define('application', Application, { - pushSettings: {stub: { }}, - }) - .define('installation', Installation, { - appId: ref('application.id'), - deviceType: mockery.deviceType, - }) - .buildTo(context, cb); - }, - - function configureProvider(cb) { - pushManager.configureApplication( - context.installation.appId, - context.installation.deviceType, - cb); - }, - - function act(cb) { - mockery.emitDevicesGone(context.installation.deviceToken); - - // Wait until the feedback is processed - // We can use process.nextTick because Memory store - // deletes the data within this event loop - process.nextTick(cb); - }, - - function verify(cb) { - Installation.find(function(err, result) { - if (err) return cb(err); - expect(result).to.have.length(0); - cb(); - }); - }, - ], done); - }); - - describe('.notify', function() { - it('should set device type/token from installation', function(done) { - async.series([ + async.series( + [ function arrange(cb) { new TestDataBuilder() .define('application', Application, { - pushSettings: {stub: { }}, - }) - // Note: the order in which the installations are created - // is important. - // The installation that should not receive the notification must - // be created first. This way the test fails when PushManager - // looks up the installation via - // `Installation.findOne({ deviceToken: token })` - .define('anotherDevice', Installation, { - appId: ref('application.id'), - deviceToken: 'a-device-token', - deviceType: 'another-device-type', + pushSettings: {stub: {}}, }) .define('installation', Installation, { appId: ref('application.id'), - deviceToken: 'a-device-token', deviceType: mockery.deviceType, }) .buildTo(context, cb); }, - function act(cb) { - pushManager.notify( - context.installation, - context.notification, + function configureProvider(cb) { + pushManager.configureApplication( + context.installation.appId, + context.installation.deviceType, cb ); }, + function act(cb) { + mockery.emitDevicesGone(context.installation.deviceToken); + + // Wait until the feedback is processed + // We can use process.nextTick because Memory store + // deletes the data within this event loop + process.nextTick(cb); + }, + function verify(cb) { - // Wait with the check to give the push manager some time - // to load all data and push the message - setTimeout(function() { - expect(mockery.firstPushNotificationArgs()).to.deep.equal( - [context.notification, context.installation.deviceToken] - ); + Installation.find(function(err, result) { + if (err) return cb(err); + expect(result).to.have.length(0); cb(); - }, 50); + }); }, - ], done); + ], + done + ); + }); + + describe('.notify', function() { + it('should set device type/token from installation', function(done) { + async.series( + [ + function arrange(cb) { + new TestDataBuilder() + .define('application', Application, { + pushSettings: {stub: {}}, + }) + // Note: the order in which the installations are created + // is important. + // The installation that should not receive the notification must + // be created first. This way the test fails when PushManager + // looks up the installation via + // `Installation.findOne({ deviceToken: token })` + .define('anotherDevice', Installation, { + appId: ref('application.id'), + deviceToken: 'a-device-token', + deviceType: 'another-device-type', + }) + .define('installation', Installation, { + appId: ref('application.id'), + deviceToken: 'a-device-token', + deviceType: mockery.deviceType, + }) + .buildTo(context, cb); + }, + + function act(cb) { + pushManager.notify(context.installation, context.notification, cb); + }, + + function verify(cb) { + // Wait with the check to give the push manager some time + // to load all data and push the message + setTimeout(function() { + expect(mockery.firstPushNotificationArgs()).to.deep.equal([ + context.notification, + context.installation.deviceToken, + ]); + cb(); + }, 50); + }, + ], + done + ); }); it('reports error on invalid notification', function(done) { @@ -131,369 +154,410 @@ describe('PushManager', function() { describe('.notifyById', function() { it('sends notification to the correct installation', function(done) { - async.series([ - function arrange(cb) { - new TestDataBuilder() - .define('application', Application, { - pushSettings: {stub: { }}, - }) - // Note: the order in which the installations are created - // is important. - // The installation that should not receive the notification must - // be created first. This way the test fails when PushManager - // looks up the installation via - // `Installation.findOne({ deviceToken: token })` - .define('anotherDevice', Installation, { - appId: ref('application.id'), - deviceToken: 'a-device-token', - deviceType: 'another-device-type', - }) - .define('installation', Installation, { - appId: ref('application.id'), - deviceToken: 'a-device-token', - deviceType: mockery.deviceType, - }) - .buildTo(context, cb); - }, - - function act(cb) { - pushManager.notifyById( - context.installation.id, - context.notification, - cb - ); - }, - - function verify(cb) { - // Wait with the check to give the push manager some time - // to load all data and push the message - setTimeout(function() { - expect(mockery.firstPushNotificationArgs()).to.deep.equal( - [context.notification, context.installation.deviceToken] + async.series( + [ + function arrange(cb) { + new TestDataBuilder() + .define('application', Application, { + pushSettings: {stub: {}}, + }) + // Note: the order in which the installations are created + // is important. + // The installation that should not receive the notification must + // be created first. This way the test fails when PushManager + // looks up the installation via + // `Installation.findOne({ deviceToken: token })` + .define('anotherDevice', Installation, { + appId: ref('application.id'), + deviceToken: 'a-device-token', + deviceType: 'another-device-type', + }) + .define('installation', Installation, { + appId: ref('application.id'), + deviceToken: 'a-device-token', + deviceType: mockery.deviceType, + }) + .buildTo(context, cb); + }, + + function act(cb) { + pushManager.notifyById( + context.installation.id, + context.notification, + cb ); - cb(); - }, 50); - }, - ], done); + }, + + function verify(cb) { + // Wait with the check to give the push manager some time + // to load all data and push the message + setTimeout(function() { + expect(mockery.firstPushNotificationArgs()).to.deep.equal([ + context.notification, + context.installation.deviceToken, + ]); + cb(); + }, 50); + }, + ], + done + ); }); it('reports error when installation was not found', function(done) { - async.series([ - function actAndVerify(cb) { - pushManager.notifyById( - 'unknown-installation-id', - context.notification, - verify - ); + async.series( + [ + function actAndVerify(cb) { + pushManager.notifyById( + 'unknown-installation-id', + context.notification, + verify + ); - function verify(err) { - expect(err).to.be.instanceOf(Error); - expect(err.details) - .to.have.property('installationId', 'unknown-installation-id'); - cb(); - } - }, - ], done); + function verify(err) { + expect(err).to.be.instanceOf(Error); + expect(err.details).to.have.property( + 'installationId', + 'unknown-installation-id' + ); + cb(); + } + }, + ], + done + ); }); it('reports error when application was not found', function(done) { - async.series([ - function arrange(cb) { - new TestDataBuilder() - .define('installation', Installation, {appId: 'unknown-app-id'}) - .buildTo(context, cb); - }, - - function actAndVerify(cb) { - pushManager.notifyById( - context.installation.id, - context.notification, - verify - ); + async.series( + [ + function arrange(cb) { + new TestDataBuilder() + .define('installation', Installation, {appId: 'unknown-app-id'}) + .buildTo(context, cb); + }, + + function actAndVerify(cb) { + pushManager.notifyById( + context.installation.id, + context.notification, + verify + ); - function verify(err) { - expect(err).to.be.instanceOf(Error); - expect(err.details) - .to.have.property('appId', 'unknown-app-id'); - cb(); - } - }, - ], done); + function verify(err) { + expect(err).to.be.instanceOf(Error); + expect(err.details).to.have.property('appId', 'unknown-app-id'); + cb(); + } + }, + ], + done + ); }); it('reports error when application has no pushSettings', function(done) { - async.series([ - function arrange(cb) { - new TestDataBuilder() - .define('application', Application, {pushSettings: null}) - .define('installation', Installation, { - appId: ref('application.id'), - deviceType: 'unknown-device-type', - }) - .buildTo(context, cb); - }, - - function actAndVerify(cb) { - pushManager.notifyById( - context.installation.id, - context.notification, - verify - ); + async.series( + [ + function arrange(cb) { + new TestDataBuilder() + .define('application', Application, {pushSettings: null}) + .define('installation', Installation, { + appId: ref('application.id'), + deviceType: 'unknown-device-type', + }) + .buildTo(context, cb); + }, + + function actAndVerify(cb) { + pushManager.notifyById( + context.installation.id, + context.notification, + verify + ); - function verify(err) { - expect(err).to.be.instanceOf(Error); - expect(err.details).to.have.property('application'); - cb(); - } - }, - ], done); + function verify(err) { + expect(err).to.be.instanceOf(Error); + expect(err.details).to.have.property('application'); + cb(); + } + }, + ], + done + ); }); it('reports error for unknown device type', function(done) { - async.series([ - function arrange(cb) { - new TestDataBuilder() - .define('application', Application, {pushSettings: {}}) - .define('installation', Installation, { - appId: ref('application.id'), - deviceType: 'unknown-device-type', - }) - .buildTo(context, cb); - }, - - function actAndVerify(cb) { - pushManager.notifyById( - context.installation.id, - context.notification, - verify - ); + async.series( + [ + function arrange(cb) { + new TestDataBuilder() + .define('application', Application, {pushSettings: {}}) + .define('installation', Installation, { + appId: ref('application.id'), + deviceType: 'unknown-device-type', + }) + .buildTo(context, cb); + }, + + function actAndVerify(cb) { + pushManager.notifyById( + context.installation.id, + context.notification, + verify + ); - function verify(err) { - expect(err).to.be.instanceOf(Error); - cb(); - } - }, - ], done); + function verify(err) { + expect(err).to.be.instanceOf(Error); + cb(); + } + }, + ], + done + ); }); it('emits error when push fails inside provider', function(done) { - async.series([ - function arrange(cb) { - new TestDataBuilder() - .define('application', Application, { - pushSettings: {stub: { }}, - }) - .define('installation', Installation, { - appId: ref('application.id'), - deviceToken: 'a-device-token', - deviceType: mockery.deviceType, - }) - .buildTo(context, cb); - }, - - function actAndVerify(cb) { - var errorCallback = sinon.spy(); - pushManager.on('error', errorCallback); - - mockery.pushNotification = function() { - this.emit('error', new Error('a test error')); - }; - - pushManager.notifyById( - context.installation.id, - context.notification, - function(err) { - if (err) throw err; - expect(errorCallback.calledOnce, 'error was emitted') - .to.equal(true); - cb(); - } - ); - }, - ], done); + async.series( + [ + function arrange(cb) { + new TestDataBuilder() + .define('application', Application, { + pushSettings: {stub: {}}, + }) + .define('installation', Installation, { + appId: ref('application.id'), + deviceToken: 'a-device-token', + deviceType: mockery.deviceType, + }) + .buildTo(context, cb); + }, + + function actAndVerify(cb) { + const errorCallback = sinon.spy(); + pushManager.on('error', errorCallback); + + mockery.pushNotification = function() { + this.emit('error', new Error('a test error')); + }; + + pushManager.notifyById( + context.installation.id, + context.notification, + function(err) { + if (err) throw err; + expect(errorCallback.calledOnce, 'error was emitted').to.equal( + true + ); + cb(); + } + ); + }, + ], + done + ); }); }); describe('.notifyByQuery', function() { it('sends notifications to the correct installations', function(done) { - async.series([ - function arrange(cb) { - new TestDataBuilder() - .define('application', Application, { - pushSettings: {stub: { }}, - }) - .define('myPhone', Installation, { - appId: ref('application.id'), - deviceToken: 'my-phone-token', - deviceType: mockery.deviceType, - userId: 'myself', - }) - .define('myOtherPhone', Installation, { - appId: ref('application.id'), - deviceToken: 'my-other-phone-token', - deviceType: mockery.deviceType, - userId: 'myself', - }) - .define('friendsPhone', Installation, { - appId: ref('application.id'), - deviceToken: 'friends-phone-token', - deviceType: mockery.deviceType, - userId: 'somebody else', - }) - .buildTo(context, cb); - }, - - function act(cb) { - pushManager.notifyByQuery( - {userId: 'myself'}, - context.notification, - cb - ); - }, - - function verify(cb) { - // Wait with the check to give the push manager some time - // to load all data and push the message - setTimeout(function() { - var callsArgs = mockery.pushNotification.args; - expect(callsArgs, 'number of notifications').to.have.length(2); - expect(callsArgs[0]).to.deep.equal( - [context.notification, context.myPhone.deviceToken] - ); - expect(callsArgs[1]).to.deep.equal( - [context.notification, context.myOtherPhone.deviceToken] + async.series( + [ + function arrange(cb) { + new TestDataBuilder() + .define('application', Application, { + pushSettings: {stub: {}}, + }) + .define('myPhone', Installation, { + appId: ref('application.id'), + deviceToken: 'my-phone-token', + deviceType: mockery.deviceType, + userId: 'myself', + }) + .define('myOtherPhone', Installation, { + appId: ref('application.id'), + deviceToken: 'my-other-phone-token', + deviceType: mockery.deviceType, + userId: 'myself', + }) + .define('friendsPhone', Installation, { + appId: ref('application.id'), + deviceToken: 'friends-phone-token', + deviceType: mockery.deviceType, + userId: 'somebody else', + }) + .buildTo(context, cb); + }, + + function act(cb) { + pushManager.notifyByQuery( + {userId: 'myself'}, + context.notification, + cb ); - cb(); - }, 50); - }, - ], done); + }, + + function verify(cb) { + // Wait with the check to give the push manager some time + // to load all data and push the message + setTimeout(function() { + const callsArgs = mockery.pushNotification.args; + expect(callsArgs, 'number of notifications').to.have.length(2); + expect(callsArgs[0]).to.deep.equal([ + context.notification, + context.myPhone.deviceToken, + ]); + expect(callsArgs[1]).to.deep.equal([ + context.notification, + context.myOtherPhone.deviceToken, + ]); + cb(); + }, 50); + }, + ], + done + ); }); it('reports error on non-object notifications', function(done) { - async.series([ - function arrange(cb) { - new TestDataBuilder() - .define('myPhone', Installation, { - userId: 'myself', - }) - .buildTo(context, cb); - }, - - function act(cb) { - pushManager.notifyByQuery( - {userId: 'myself'}, - 'invalid notification', // invalid - function(err) { - expect(err.message).to.equal('notification must be an object'); - cb(); - } - ); - }, - ], done); + async.series( + [ + function arrange(cb) { + new TestDataBuilder() + .define('myPhone', Installation, { + userId: 'myself', + }) + .buildTo(context, cb); + }, + + function act(cb) { + pushManager.notifyByQuery( + {userId: 'myself'}, + 'invalid notification', // invalid + function(err) { + expect(err.message).to.equal('notification must be an object'); + cb(); + } + ); + }, + ], + done + ); }); }); describe('.notifyMany', function() { it('sends notifications to the correct installations', function(done) { - async.series([ - function arrange(cb) { - new TestDataBuilder() - .define('application', Application, { - pushSettings: {stub: { }}, - }) - .define('firstPhone', Installation, { - appId: ref('application.id'), - deviceToken: 'first-phone-token', - deviceType: mockery.deviceType, - userId: 'myself', - }) - .define('secondPhone', Installation, { - appId: ref('application.id'), - deviceToken: 'second-phone-token', - deviceType: mockery.deviceType, - userId: 'myself', - }) - .define('thirdPhone', Installation, { - appId: ref('application.id'), - deviceToken: 'third-phone-token', - deviceType: mockery.deviceType, - userId: 'somebody else', - }) - .buildTo(context, cb); - }, - function act(cb) { - pushManager.notifyMany( + async.series( + [ + function arrange(cb) { + new TestDataBuilder() + .define('application', Application, { + pushSettings: {stub: {}}, + }) + .define('firstPhone', Installation, { + appId: ref('application.id'), + deviceToken: 'first-phone-token', + deviceType: mockery.deviceType, + userId: 'myself', + }) + .define('secondPhone', Installation, { + appId: ref('application.id'), + deviceToken: 'second-phone-token', + deviceType: mockery.deviceType, + userId: 'myself', + }) + .define('thirdPhone', Installation, { + appId: ref('application.id'), + deviceToken: 'third-phone-token', + deviceType: mockery.deviceType, + userId: 'somebody else', + }) + .buildTo(context, cb); + }, + function act(cb) { + pushManager.notifyMany( context.application.id, mockery.deviceType, ['first-phone-token', 'second-phone-token'], context.notification, cb - ); - }, - function verify(cb) { - // Wait with the check to give the push manager some time - // to load all data and push the message - setTimeout(function() { - var callsArgs = mockery.pushNotification.args; - - expect(callsArgs[0], 'number of arguments').to.have.length(2); - expect(callsArgs[0]).to.deep.equal([ - context.notification, - [context.firstPhone.deviceToken, context.secondPhone.deviceToken], - ]); - cb(); - }, 50); - }, - ], done); + ); + }, + function verify(cb) { + // Wait with the check to give the push manager some time + // to load all data and push the message + setTimeout(function() { + const callsArgs = mockery.pushNotification.args; + + expect(callsArgs[0], 'number of arguments').to.have.length(2); + expect(callsArgs[0]).to.deep.equal([ + context.notification, + [ + context.firstPhone.deviceToken, + context.secondPhone.deviceToken, + ], + ]); + cb(); + }, 50); + }, + ], + done + ); }); it('reports error if device token is not an array', function(done) { - async.series([ - function arrange(cb) { - new TestDataBuilder() - .define('myPhone', Installation, { - userId: 'myself', - }) - .buildTo(context, cb); - }, - function act(cb) { - pushManager.notifyMany( - '1', - 'ios', - 'invalid-phone-token', - context.notification, - function(err) { - expect(err.message).to.equal('deviceTokens must be an array'); - cb(); - } - ); - }, - ], done); + async.series( + [ + function arrange(cb) { + new TestDataBuilder() + .define('myPhone', Installation, { + userId: 'myself', + }) + .buildTo(context, cb); + }, + function act(cb) { + pushManager.notifyMany( + '1', + 'ios', + 'invalid-phone-token', + context.notification, + function(err) { + expect(err.message).to.equal('deviceTokens must be an array'); + cb(); + } + ); + }, + ], + done + ); }); it('reports error on non-object notifications', function(done) { - async.series([ - function verify(cb) { - pushManager.notifyMany( - '1', - 'ios', - ['phone-token'], - 'invalid-notification', - function(err) { - expect(err.message).to.equal('notification must be an object'); - cb(); - } - ); - }, - ], done); + async.series( + [ + function verify(cb) { + pushManager.notifyMany( + '1', + 'ios', + ['phone-token'], + 'invalid-notification', + function(err) { + expect(err.message).to.equal('notification must be an object'); + cb(); + } + ); + }, + ], + done + ); }); }); describe('PushManager applicationsCache', function() { it('settings', function() { - var ttlInSeconds, checkPeriodInSeconds; - ttlInSeconds = checkPeriodInSeconds = 10; - var pm = new PushManager({ + let checkPeriodInSeconds; + const ttlInSeconds = checkPeriodInSeconds = 10; + const pm = new PushManager({ ttlInSeconds: 10, checkPeriodInSeconds: 10, }); @@ -502,13 +566,13 @@ describe('PushManager', function() { }); it('is NodeCache instance', function() { - var pm = new PushManager(); + const pm = new PushManager(); expect(pm.applicationsCache).to.be.a('Object'); expect(pm.applicationsCache).to.be.instanceOf(NodeCache); }); it('has set and get methods', function() { - var pm = new PushManager(); + const pm = new PushManager(); expect(pm.applicationsCache).to.have.property('set'); expect(pm.applicationsCache).to.have.property('get'); }); @@ -518,7 +582,7 @@ describe('PushManager', function() { function arrange(cb) { new TestDataBuilder() .define('application', Application, { - pushSettings: {stub: { }}, + pushSettings: {stub: {}}, }) .define('installation', Installation, { appId: ref('application.id'), @@ -531,13 +595,14 @@ describe('PushManager', function() { pushManager.configureApplication( context.application.id, context.installation.deviceType, - cb); + cb + ); }, function verify(cb) { - var cacheApp = pushManager - .applicationsCache - .get(context.installation.appId); + const cacheApp = pushManager.applicationsCache.get( + context.installation.appId + ); expect(cacheApp).to.have.property(context.installation.appId); }, ]); @@ -561,33 +626,33 @@ describe('PushManager model dependencies', function() { }); it('creates properties for dependent models', function() { - var pm = new PushManager(); + const pm = new PushManager(); expect(pm.Installation).to.be.equal(Installation); expect(pm.Notification).to.be.equal(Notification); expect(pm.Application).to.be.equal(Application); }); it('uses subclasses for the dependent models', function() { - var pm = new PushManager(); - var installationModel = Installation.extend('installation', {}); + const pm = new PushManager(); + const installationModel = Installation.extend('installation', {}); expect(pm.Installation).to.be.equal(installationModel); }); it('honors settings', function() { - var pm = new PushManager({ + const pm = new PushManager({ installation: 'myInstallation', }); - var myInstallation = Installation.extend('myInstallation', {}); - var otherInstallation = Installation.extend('otherInstallation', {}); + const myInstallation = Installation.extend('myInstallation', {}); + const otherInstallation = Installation.extend('otherInstallation', {}); expect(pm.Installation).to.be.equal(myInstallation); }); it('supports setters', function() { - var pm = new PushManager({ + const pm = new PushManager({ installation: 'myInstallation', }); - var myInstallation = Installation.extend('myInstallation', {}); - var otherInstallation = Installation.extend('otherInstallation', {}); + const myInstallation = Installation.extend('myInstallation', {}); + const otherInstallation = Installation.extend('otherInstallation', {}); expect(pm.Installation).to.be.equal(myInstallation); pm.Installation = otherInstallation; expect(pm.Installation).to.be.equal(otherInstallation); diff --git a/test/push-notification.test.js b/test/push-notification.test.js index e51a5ea..79a8489 100644 --- a/test/push-notification.test.js +++ b/test/push-notification.test.js @@ -1,84 +1,87 @@ -// Copyright IBM Corp. 2013,2015. All Rights Reserved. +// Copyright IBM Corp. 2013,2019. All Rights Reserved. // Node module: loopback-component-push // This file is licensed under the Artistic License 2.0. // License text available at https://opensource.org/licenses/Artistic-2.0 'use strict'; -var PushModel = PushConnector.createPushModel({dataSource: ds}); +const expect = require('chai').expect; +const loopback = require('loopback'); -var objectMother = require('./helpers/object-mother'); +const ds = loopback.createDataSource('db', { + connector: loopback.Memory, +}); + +// Application +const Application = loopback.Application; +Application.attachTo(ds); + +// Push Connector +const PushConnector = require('../'); +const PushModel = PushConnector.createPushModel({ + dataSource: ds, +}); + +// Installation +const Installation = PushConnector.Installation; +Installation.attachTo(ds); describe('PushNotification', function() { it('registers a new installation', function(done) { - // Sign up an application - Application.register('test-user', 'TestApp', + // Sign up an application + Application.register( + 'test-user', + 'TestApp', { description: 'My test mobile application', pushSettings: { apns: { - certData: objectMother.apnsDevCert(), - keyData: objectMother.apnsDevKey(), - pushOptions: { - }, - feedbackOptions: { - batchFeedback: true, - interval: 300, + token: { + keyId: 'key_id', + key: 'test/fixtures/APNs_token_key.p8', + teamId: 'team_id', }, + bundle: 'ch.test.app', }, }, - }, function(err, result) { + }, + function(err, result) { if (err) { throw err; } - var application = result; - - Installation.destroyAll(function(err, result) { - // console.log('Adding a test record'); - Installation.create({ - appId: application.id, - userId: 'raymond', - deviceToken: '75624450 3c9f95b4 9d7ff821 20dc193c a1e3a7cb ' + - '56f60c2e f2a19241 e8f33305', - deviceType: 'ios', - created: new Date(), - modified: new Date(), - status: 'Active', - }, function(err, result) { - if (err) { - console.error(err); - } else { - // console.log('Registration record is created: ', result); - } - PushModel.dataSource.connector.applicationsCache.set( - application.id, - { - memory: { - pushNotification: function(notification, deviceToken) { - // console.log(notification, deviceToken); - assert.equal(deviceToken, result.deviceToken); - done(); - }, - }, - } - ); + const application = result; + const deviceToken = + '6676119dc1ee264f7a32429c56c4e51b0a8b5673d1' + + 'd55c431d720bb60b0381d3'; - var note = new Notification(); + Installation.destroyAll(function(err, result) { + // console.log('Adding a test record'); + Installation.create( + { + appId: application.id, + userId: 'raymond', + deviceToken: deviceToken, + deviceType: 'ios', + created: new Date(), + modified: new Date(), + status: 'Active', + }, + function(err, result) { + if (err) { + console.error(err); - // Expires 1 hour from now. - note.expirationInterval = Math.floor(Date.now() / 1000) + 3600; - note.badge = 5; - note.sound = 'ping.aiff'; - note.alert = '\uD83D\uDCE7 \u2709 ' + 'Hello'; - note.messageFrom = 'Ray'; + throw err; + } else { + expect(result.userId === 'raymond'); + expect(result.deviceToken === deviceToken); + expect(result.deviceType === 'ios'); - PushModel.notifyById( - result.id, - note, - function(err) { if (err) throw err; done(); } - ); - }); + done(); + } + } + ); }); - }); + } + ); }); });