From 88f8b13c0704709328c7c65261d4ee3e80274b3b Mon Sep 17 00:00:00 2001 From: William Deren Date: Thu, 28 Nov 2024 08:41:11 +0100 Subject: [PATCH] Add new Free Mobile SMS integration (#2157) Co-authored-by: Pierre-Gilles Leymarie --- .../assets/integrations/cover/free-mobile.jpg | Bin 0 -> 51951 bytes front/src/components/app.jsx | 5 + front/src/config/i18n/de.json | 19 +++ front/src/config/i18n/en.json | 19 +++ front/src/config/i18n/fr.json | 19 +++ .../config/integrations/communications.json | 5 + .../all/free-mobile/FreeMobile.jsx | 112 +++++++++++++++ .../integration/all/free-mobile/actions.js | 68 +++++++++ .../integration/all/free-mobile/index.js | 23 +++ .../routes/scene/edit-scene/ActionCard.jsx | 21 ++- .../actions/ChooseActionTypeCard.jsx | 3 +- .../scene/edit-scene/actions/SendSms.jsx | 41 ++++++ server/lib/scene/scene.actions.js | 9 ++ server/services/free-mobile/index.js | 69 +++++++++ server/services/free-mobile/package.json | 18 +++ server/services/index.js | 1 + .../actions/scene.action.sendSms.test.js | 86 +++++++++++ .../test/services/free-mobile/index.test.js | 133 ++++++++++++++++++ server/utils/constants.js | 3 + 19 files changed, 650 insertions(+), 4 deletions(-) create mode 100644 front/src/assets/integrations/cover/free-mobile.jpg create mode 100644 front/src/routes/integration/all/free-mobile/FreeMobile.jsx create mode 100644 front/src/routes/integration/all/free-mobile/actions.js create mode 100644 front/src/routes/integration/all/free-mobile/index.js create mode 100644 front/src/routes/scene/edit-scene/actions/SendSms.jsx create mode 100644 server/services/free-mobile/index.js create mode 100644 server/services/free-mobile/package.json create mode 100644 server/test/lib/scene/actions/scene.action.sendSms.test.js create mode 100644 server/test/services/free-mobile/index.test.js diff --git a/front/src/assets/integrations/cover/free-mobile.jpg b/front/src/assets/integrations/cover/free-mobile.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ec4dd245c1d4c28281dae2bd7e4c2027a23562e1 GIT binary patch literal 51951 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 + + diff --git a/front/src/config/i18n/de.json b/front/src/config/i18n/de.json index 2d6fbc7012..5699ffef2a 100644 --- a/front/src/config/i18n/de.json +++ b/front/src/config/i18n/de.json @@ -489,6 +489,17 @@ "configurationSuccess": "Konfiguration erfolgreich gesichert.", "buttonSave": "Sichern" }, + "free-mobile": { + "title": "Free Mobile", + "description": "SMS von Gladys über Free Mobile senden.", + "documentation": "Free Mobile Dokumentation", + "introduction": "Dieses Plugin ermöglicht es Ihnen, SMS an Ihr Free-Handy über den Benachrichtigungsdienst von Free zu senden. Geben Sie Ihre Kundennummer und den Identifizierungsschlüssel unten ein, den Sie auf der FreeMobile-Website finden.", + "username": "Free Mobile Kundennummer", + "key": "Identifizierungsschlüssel für den Dienst", + "configurationError": "Wir konnten diese Konfiguration nicht speichern.", + "configurationSuccess": "Die Kontokonfiguration wurde erfolgreich gespeichert.", + "saveButton": "Sichern" + }, "philipsHue": { "title": "Philips Hue", "description": "Steuere Philips-Hue-Lichter und -Steckdosen mit der offiziellen Bridge.", @@ -1838,6 +1849,11 @@ "textPlaceholder": "Nachrichtentext", "explanationText": "Um eine Variable einzufügen, geben Sie '{{' ein. Achten Sie darauf, dass Sie zuvor eine Variable in einer Aktion 'Letzten Zustand abrufen' definiert haben, die vor diesem Nachrichtenblock platziert wurde." }, + "smsSend": { + "textLabel": "Nachricht", + "explanationText": "Um eine Variable in den Text einzufügen, gib \"{{\" ein. Um einen Variablenwert festzulegen, musst du zuerst das Feld \"Gerätewert abrufen\" verwenden.", + "messagePlaceholder": "Meine Message" + }, "turnOnLights": { "label": "Wähle die Lichter aus, die eingeschaltet werden sollen" }, @@ -2021,6 +2037,9 @@ "send": "Nachricht senden", "send-camera": "Kameraaufnahme senden" }, + "sms": { + "send": "SMS senden" + }, "delay": "Warten", "light": { "turn-on": "Licht einschalten", diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json index 8c333d3139..79fdb14478 100644 --- a/front/src/config/i18n/en.json +++ b/front/src/config/i18n/en.json @@ -489,6 +489,17 @@ "configurationSuccess": "Successfully saved configuration.", "buttonSave": "Save" }, + "free-mobile": { + "title": "Free Mobile", + "description": "Send SMS from Gladys using Free Mobile.", + "documentation": "Free Mobile Documentation", + "introduction": "This plugin allows you to send SMS to your Free cell phone via the notification service provided by Free. Enter your customer ID and the identification key below, which you can find on the FreeMobile website.", + "username": "Free Mobile Customer ID", + "key": "Identification key for the service", + "configurationError": "We could not save this configuration.", + "configurationSuccess": "Account configuration saved successfully.", + "saveButton": "Save" + }, "philipsHue": { "title": "Philips Hue", "description": "Control Philips Hue Lights and plugs with the official hub", @@ -1838,6 +1849,11 @@ "textPlaceholder": "Message text", "explanationText": "To insert a variable, type '{{'. Be careful, you must have defined a variable beforehand in a 'Retrieve the last state' action placed before this message block." }, + "smsSend": { + "textLabel": "Message", + "explanationText": "To inject a variable in the text, press '{{'. To set a variable value, you need to use the 'Get device value' box before this one.", + "messagePlaceholder": "My message" + }, "turnOnLights": { "label": "Select the lights you want to turn on" }, @@ -2021,6 +2037,9 @@ "send": "Send Message", "send-camera": "Send a camera image" }, + "sms": { + "send": "Send SMS" + }, "delay": "Wait", "light": { "turn-on": "Turn On the Lights", diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json index 944b5e9a37..0ba3a52b4a 100644 --- a/front/src/config/i18n/fr.json +++ b/front/src/config/i18n/fr.json @@ -617,6 +617,17 @@ "configurationSuccess": "Sauvegarde de la configuration du compte terminée.", "buttonSave": "Sauvegarder" }, + "free-mobile": { + "title": "Free Mobile", + "description": "Envoyer des sms depuis Gladys grâce à Free Mobile.", + "documentation": "Documentation Free Mobile", + "introduction": "Ce plugin vous permet d’envoyer des sms à votre portable Free via le service de notification proposé par Free. Entrez votre identifiant client et la clé d'identification ci-dessous que vous retrouverez sur le site FreeMobile.", + "username": "Identifiant client Free Mobile", + "key": "Clé d'identification au service", + "configurationError": "Nous n'avons pas pu sauvegarder cette configuration.", + "configurationSuccess": "Sauvegarde de la configuration du compte terminée.", + "saveButton": "Sauvegarder" + }, "philipsHue": { "title": "Philips Hue", "description": "Contrôler les lumières Philips Hue.", @@ -1838,6 +1849,11 @@ "textPlaceholder": "Texte du message", "explanationText": "Pour injecter une variable, tapez '{{'. Attention, vous devez avoir défini une variable auparavant dans une action 'Récupérer le dernier état' placé avant ce bloc message." }, + "smsSend": { + "textLabel": "Message", + "explanationText": "Pour injecter une variable, tapez '{{'. Attention, vous devez avoir défini une variable auparavant dans une action 'Récupérer le dernier état' placé avant ce bloc message.", + "messagePlaceholder": "Mon message" + }, "turnOnLights": { "label": "Sélectionnez les lumières que vous souhaitez allumer" }, @@ -2021,6 +2037,9 @@ "send": "Envoyer un message", "send-camera": "Envoyer une image de caméra" }, + "sms": { + "send": "Envoyer un sms" + }, "delay": "Attendre", "light": { "turn-on": "Allumer les lumières", diff --git a/front/src/config/integrations/communications.json b/front/src/config/integrations/communications.json index ea2b3cd5af..9f4db29dad 100644 --- a/front/src/config/integrations/communications.json +++ b/front/src/config/integrations/communications.json @@ -22,5 +22,10 @@ { "key": "openai", "img": "/assets/integrations/cover/openai.jpg" + }, + { + "key": "free-mobile", + "link": "free-mobile", + "img": "/assets/integrations/cover/free-mobile.jpg" } ] diff --git a/front/src/routes/integration/all/free-mobile/FreeMobile.jsx b/front/src/routes/integration/all/free-mobile/FreeMobile.jsx new file mode 100644 index 0000000000..ec2d83193b --- /dev/null +++ b/front/src/routes/integration/all/free-mobile/FreeMobile.jsx @@ -0,0 +1,112 @@ +import { Text, MarkupText, Localizer } from 'preact-i18n'; +import cx from 'classnames'; +import { RequestStatus } from '../../../../utils/consts'; +import DeviceConfigurationLink from '../../../../components/documentation/DeviceConfigurationLink'; + +const FreeMobilePage = ({ children, ...props }) => ( +
+
+
+
+
+
+

+ +

+
+
+ + + + + + +
+
+
+ +
+
+
+

+ +

+
+
+
+
+
+

+ +

+ {props.freeMobileSaveSettingsStatus === RequestStatus.Error && ( +
+ +
+ )} + {props.freeMobileSaveSettingsStatus === RequestStatus.Success && ( +
+ +
+ )} +
+
+
+ +
+ + } + onInput={props.updateFreeMobileUsername} + value={props.freeMobileUsername} + /> + +
+ +
+
+ +
+ + } + onInput={props.updateFreeMobileAccessToken} + value={props.freeMobileAccessToken} + /> + +
+ +
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+); + +export default FreeMobilePage; diff --git a/front/src/routes/integration/all/free-mobile/actions.js b/front/src/routes/integration/all/free-mobile/actions.js new file mode 100644 index 0000000000..1f5e40ed81 --- /dev/null +++ b/front/src/routes/integration/all/free-mobile/actions.js @@ -0,0 +1,68 @@ +import { RequestStatus } from '../../../../utils/consts'; + +const actions = store => ({ + updateFreeMobileUsername(state, e) { + store.setState({ + freeMobileUsername: e.target.value + }); + }, + + updateFreeMobileAccessToken(state, e) { + store.setState({ + freeMobileAccessToken: e.target.value + }); + }, + + async getFreeMobileSettings(state) { + store.setState({ + freeMobileGetSettingsStatus: RequestStatus.Getting + }); + try { + const username = await state.httpClient.get('/api/v1/service/free-mobile/variable/FREE_MOBILE_USERNAME'); + store.setState({ + freeMobileUsername: username.value + }); + + const accessToken = await state.httpClient.get('/api/v1/service/free-mobile/variable/FREE_MOBILE_ACCESS_TOKEN'); + store.setState({ + freeMobileAccessToken: accessToken.value, + freeMobileGetSettingsStatus: RequestStatus.Success + }); + } catch (e) { + store.setState({ + freeMobileGetSettingsStatus: RequestStatus.Error + }); + } + }, + + async saveFreeMobileSettings(state, e) { + e.preventDefault(); + store.setState({ + freeMobileSaveSettingsStatus: RequestStatus.Getting + }); + try { + store.setState({ + freeMobileUsername: state.freeMobileUsername.trim(), + freeMobileAccessToken: state.freeMobileAccessToken.trim() + }); + await state.httpClient.post('/api/v1/service/free-mobile/variable/FREE_MOBILE_USERNAME', { + value: state.freeMobileUsername.trim() + }); + await state.httpClient.post('/api/v1/service/free-mobile/variable/FREE_MOBILE_ACCESS_TOKEN', { + value: state.freeMobileAccessToken.trim() + }); + + // start service + await state.httpClient.post('/api/v1/service/free-mobile/start'); + store.setState({ + freeMobileSaveSettingsStatus: RequestStatus.Success + }); + } catch (e) { + store.setState({ + freeMobileSaveSettingsStatus: RequestStatus.Error + }); + } + } +}); + +export default actions; diff --git a/front/src/routes/integration/all/free-mobile/index.js b/front/src/routes/integration/all/free-mobile/index.js new file mode 100644 index 0000000000..0c48b2cebe --- /dev/null +++ b/front/src/routes/integration/all/free-mobile/index.js @@ -0,0 +1,23 @@ +import { Component } from 'preact'; +import { connect } from 'unistore/preact'; +import actions from './actions'; +import FreeMobilePage from './FreeMobile'; +import { RequestStatus } from '../../../../utils/consts'; + +class FreeMobileIntegration extends Component { + componentWillMount() { + this.props.getFreeMobileSettings(); + } + + render(props, {}) { + const loading = + props.freeMobileGetSettingsStatus === RequestStatus.Getting || + props.freeMobileSaveSettingsStatus === RequestStatus.Getting; + return ; + } +} + +export default connect( + 'user,freeMobileUsername,freeMobileAccessToken,freeMobileGetSettingsStatus,freeMobileSaveSettingsStatus', + actions +)(FreeMobileIntegration); diff --git a/front/src/routes/scene/edit-scene/ActionCard.jsx b/front/src/routes/scene/edit-scene/ActionCard.jsx index e7cfb314b4..b3cfeb1283 100644 --- a/front/src/routes/scene/edit-scene/ActionCard.jsx +++ b/front/src/routes/scene/edit-scene/ActionCard.jsx @@ -33,6 +33,7 @@ import SendZigbee2MqttMessage from './actions/SendZigbee2MqttMessage'; import PlayNotification from './actions/PlayNotification'; import EdfTempoCondition from './actions/EdfTempoCondition'; import AskAI from './actions/AskAI'; +import SendSms from './actions/SendSms'; const deleteActionFromColumn = (columnIndex, rowIndex, deleteAction) => () => { deleteAction(columnIndex, rowIndex); @@ -68,7 +69,8 @@ const ACTION_ICON = { [ACTIONS.MQTT.SEND]: 'fe fe-message-square', [ACTIONS.MUSIC.PLAY_NOTIFICATION]: 'fe fe-speaker', [ACTIONS.ZIGBEE2MQTT.SEND]: 'fe fe-message-square', - [ACTIONS.AI.ASK]: 'fe fe-cpu' + [ACTIONS.AI.ASK]: 'fe fe-cpu', + [ACTIONS.SMS.SEND]: 'fe fe-message-circle' }; const ACTION_CARD_TYPE = 'ACTION_CARD_TYPE'; @@ -107,11 +109,13 @@ const ActionCard = ({ children, ...props }) => { props.action.type === ACTIONS.CALENDAR.IS_EVENT_RUNNING || props.action.type === ACTIONS.MQTT.SEND || props.action.type === ACTIONS.ZIGBEE2MQTT.SEND || - props.action.type === ACTIONS.LIGHT.BLINK, + props.action.type === ACTIONS.LIGHT.BLINK || + props.action.type === ACTIONS.SMS.SEND, 'col-lg-4': props.action.type !== ACTIONS.CONDITION.ONLY_CONTINUE_IF && props.action.type !== ACTIONS.MESSAGE.SEND && - props.action.type !== ACTIONS.CALENDAR.IS_EVENT_RUNNING + props.action.type !== ACTIONS.CALENDAR.IS_EVENT_RUNNING && + props.action.type !== ACTIONS.SMS.SEND })} >
{ triggersVariables={props.triggersVariables} /> )} + {props.action.type === ACTIONS.SMS.SEND && ( + + )}
diff --git a/front/src/routes/scene/edit-scene/actions/ChooseActionTypeCard.jsx b/front/src/routes/scene/edit-scene/actions/ChooseActionTypeCard.jsx index dbb88aea3f..15575b7eba 100644 --- a/front/src/routes/scene/edit-scene/actions/ChooseActionTypeCard.jsx +++ b/front/src/routes/scene/edit-scene/actions/ChooseActionTypeCard.jsx @@ -35,7 +35,8 @@ const ACTION_LIST = [ ACTIONS.MQTT.SEND, ACTIONS.ZIGBEE2MQTT.SEND, ACTIONS.MUSIC.PLAY_NOTIFICATION, - ACTIONS.AI.ASK + ACTIONS.AI.ASK, + ACTIONS.SMS.SEND ]; const TRANSLATIONS = ACTION_LIST.reduce((acc, action) => { diff --git a/front/src/routes/scene/edit-scene/actions/SendSms.jsx b/front/src/routes/scene/edit-scene/actions/SendSms.jsx new file mode 100644 index 0000000000..38f5128043 --- /dev/null +++ b/front/src/routes/scene/edit-scene/actions/SendSms.jsx @@ -0,0 +1,41 @@ +import { Component } from 'preact'; +import { connect } from 'unistore/preact'; +import { Localizer, Text } from 'preact-i18n'; + +import TextWithVariablesInjected from '../../../../components/scene/TextWithVariablesInjected'; + +class SendSms extends Component { + updateText = text => { + this.props.updateActionProperty(this.props.columnIndex, this.props.index, 'text', text); + }; + + render(props, {}) { + return ( +
+
+ +
+ +
+ + } + /> + +
+
+ ); + } +} + +export default connect('httpClient', {})(SendSms); diff --git a/server/lib/scene/scene.actions.js b/server/lib/scene/scene.actions.js index 99df9acc72..6f214b803c 100644 --- a/server/lib/scene/scene.actions.js +++ b/server/lib/scene/scene.actions.js @@ -225,6 +225,7 @@ const actionsFunc = { } setTimeout(resolve, timeToWaitMilliseconds); }), + [ACTIONS.SCENE.START]: async (self, action, scope) => { if (scope.alreadyExecutedScenes && scope.alreadyExecutedScenes.has(action.scene)) { logger.info( @@ -588,6 +589,14 @@ const actionsFunc = { // Play TTS Notification on device await self.device.setValue(device, deviceFeature, url); }, + [ACTIONS.SMS.SEND]: async (self, action, scope) => { + const freeMobileService = self.service.getService('free-mobile'); + + if (freeMobileService) { + const textWithVariables = Handlebars.compile(action.text)(scope); + freeMobileService.sms.send(textWithVariables); + } + }, }; module.exports = { diff --git a/server/services/free-mobile/index.js b/server/services/free-mobile/index.js new file mode 100644 index 0000000000..afec783621 --- /dev/null +++ b/server/services/free-mobile/index.js @@ -0,0 +1,69 @@ +const logger = require('../../utils/logger'); +const { ServiceNotConfiguredError } = require('../../utils/coreErrors'); + +module.exports = function FreeMobileService(gladys, serviceId) { + const axios = require('axios'); + let username; + let accessToken; + + /** + * @public + * @description This function starts the FreeMobile service. + * @example + * gladys.services.free-mobile.start(); + */ + async function start() { + logger.info('Starting Free Mobile service'); + username = await gladys.variable.getValue('FREE_MOBILE_USERNAME', serviceId); + accessToken = await gladys.variable.getValue('FREE_MOBILE_ACCESS_TOKEN', serviceId); + + if (!username || username.length === 0) { + throw new ServiceNotConfiguredError('No FreeMobile username found. Not starting Free Mobile service'); + } + + if (!accessToken || accessToken.length === 0) { + throw new ServiceNotConfiguredError('No FreeMobile access_token found. Not starting Free Mobile service'); + } + } + + /** + * @description Send a sms. + * @param {string} message - The message to send. + * @example + * gladys.services.free-mobile.sms.send('hello') + */ + async function send(message) { + const url = 'https://smsapi.free-mobile.fr/sendmsg'; + + const params = { + user: username, + pass: accessToken, + msg: message, + }; + + try { + const response = await axios.get(url, { params }); + logger.debug('SMS successfully sent:', response.data); + } catch (e) { + logger.error('Error sending SMS:', e); + } + } + + /** + * @public + * @description This function stops the FreeMobile service. + * @example + * gladys.services.free-mobile.stop(); + */ + async function stop() { + logger.info('Stopping Free Mobile service'); + } + + return Object.freeze({ + start, + stop, + sms: { + send, + }, + }); +}; diff --git a/server/services/free-mobile/package.json b/server/services/free-mobile/package.json new file mode 100644 index 0000000000..a339d55687 --- /dev/null +++ b/server/services/free-mobile/package.json @@ -0,0 +1,18 @@ +{ + "name": "gladys-free-mobile", + "version": "1.0.0", + "main": "index.js", + "os": [ + "darwin", + "linux", + "win32" + ], + "cpu": [ + "x64", + "arm", + "arm64" + ], + "dependencies": { + "axios": "^1.4.0" + } +} diff --git a/server/services/index.js b/server/services/index.js index dcb78ee016..61b1991007 100644 --- a/server/services/index.js +++ b/server/services/index.js @@ -29,3 +29,4 @@ module.exports.sonos = require('./sonos'); module.exports['zwavejs-ui'] = require('./zwavejs-ui'); module.exports['google-cast'] = require('./google-cast'); module.exports.airplay = require('./airplay'); +module.exports['free-mobile'] = require('./free-mobile'); diff --git a/server/test/lib/scene/actions/scene.action.sendSms.test.js b/server/test/lib/scene/actions/scene.action.sendSms.test.js new file mode 100644 index 0000000000..869c6cfbbc --- /dev/null +++ b/server/test/lib/scene/actions/scene.action.sendSms.test.js @@ -0,0 +1,86 @@ +const { fake, assert } = require('sinon'); +const EventEmitter = require('events'); + +const { ACTIONS } = require('../../../../utils/constants'); +const { executeActions } = require('../../../../lib/scene/scene.executeActions'); + +const StateManager = require('../../../../lib/state'); + +const event = new EventEmitter(); + +describe('scene.send-sms', () => { + it('should send message with value injected from device get-value', async () => { + const stateManager = new StateManager(event); + stateManager.setState('deviceFeature', 'my-device-feature', { + category: 'light', + type: 'binary', + last_value: 15, + }); + const freeMobileService = { + sms: { + send: fake.resolves(null), + }, + }; + const service = { + getService: fake.returns(freeMobileService), + }; + const scope = {}; + await executeActions( + { stateManager, event, service }, + [ + [ + { + type: ACTIONS.DEVICE.GET_VALUE, + device_feature: 'my-device-feature', + }, + ], + [ + { + type: ACTIONS.SMS.SEND, + text: 'Temperature in the living room is {{0.0.last_value}} °C.', + }, + ], + ], + scope, + ); + assert.calledWith(freeMobileService.sms.send, 'Temperature in the living room is 15 °C.'); + }); + + it('should send message with value injected from http-request', async () => { + const stateManager = new StateManager(event); + const http = { + request: fake.resolves({ result: [15], error: null }), + }; + const freeMobileService = { + sms: { + send: fake.resolves(null), + }, + }; + const service = { + getService: fake.returns(freeMobileService), + }; + const scope = {}; + await executeActions( + { stateManager, event, service, http }, + [ + [ + { + type: ACTIONS.HTTP.REQUEST, + method: 'post', + url: 'http://test.test', + body: '{"toto":"toto"}', + headers: [], + }, + ], + [ + { + type: ACTIONS.SMS.SEND, + text: 'Temperature in the living room is {{0.0.result.[0]}} °C.', + }, + ], + ], + scope, + ); + assert.calledWith(freeMobileService.sms.send, 'Temperature in the living room is 15 °C.'); + }); +}); diff --git a/server/test/services/free-mobile/index.test.js b/server/test/services/free-mobile/index.test.js new file mode 100644 index 0000000000..6bae0d0479 --- /dev/null +++ b/server/test/services/free-mobile/index.test.js @@ -0,0 +1,133 @@ +const sinon = require('sinon'); +const { expect } = require('chai'); +const assert = require('assert'); +const proxyquire = require('proxyquire').noCallThru(); +const { ServiceNotConfiguredError } = require('../../../utils/coreErrors'); +const logger = require('../../../utils/logger'); + +const serviceId = 'f87b7af2-ca8e-44fc-b754-444354b42fee'; + +describe('FreeMobileService', () => { + let FreeMobileService; + let axiosStub; + let gladys; + let freeMobileService; + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + axiosStub = { + get: async () => { + return { data: 'OK' }; + }, + }; + + FreeMobileService = proxyquire('../../../services/free-mobile', { + axios: axiosStub, + }); + + gladys = { + variable: { + getValue: async (key) => { + if (key === 'FREE_MOBILE_USERNAME') { + return 'validUsername'; + } + if (key === 'FREE_MOBILE_ACCESS_TOKEN') { + return 'validAccessToken'; + } + return null; + }, + }, + }; + + freeMobileService = FreeMobileService(gladys, serviceId); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('start', () => { + it('should start service with success', async () => { + await freeMobileService.start(); + + assert.strictEqual(await gladys.variable.getValue('FREE_MOBILE_USERNAME', serviceId), 'validUsername'); + assert.strictEqual(await gladys.variable.getValue('FREE_MOBILE_ACCESS_TOKEN', serviceId), 'validAccessToken'); + }); + + it('should throw ServiceNotConfiguredError if username is missing', async () => { + gladys.variable.getValue = async (key) => { + if (key === 'FREE_MOBILE_USERNAME') { + return null; + } + if (key === 'FREE_MOBILE_ACCESS_TOKEN') { + return 'validAccessToken'; + } + return null; + }; + + try { + await freeMobileService.start(); + throw new Error('Expected ServiceNotConfiguredError to be thrown'); + } catch (e) { + expect(e).instanceOf(ServiceNotConfiguredError); + } + }); + + it('should throw ServiceNotConfiguredError if accessToken is missing', async () => { + gladys.variable.getValue = async (key) => { + if (key === 'FREE_MOBILE_USERNAME') { + return 'validUsername'; + } + if (key === 'FREE_MOBILE_ACCESS_TOKEN') { + return null; + } + return null; + }; + + try { + await freeMobileService.start(); + throw new Error('Expected ServiceNotConfiguredError to be thrown'); + } catch (e) { + expect(e).instanceOf(ServiceNotConfiguredError); + } + }); + }); + + describe('send', () => { + beforeEach(async () => { + await freeMobileService.start(); + }); + + it('should send SMS successfully', async () => { + axiosStub.get = async (url, options) => { + assert.strictEqual(url, 'https://smsapi.free-mobile.fr/sendmsg'); + assert.deepStrictEqual(options.params, { + user: 'validUsername', + pass: 'validAccessToken', + msg: 'Hello', + }); + return { data: 'OK' }; + }; + + await freeMobileService.sms.send('Hello'); + }); + + it('should log an error if SMS fails', async () => { + axiosStub.get = async () => { + throw new Error('Network error'); + }; + const loggerErrorStub = sandbox.stub(logger, 'error'); + await freeMobileService.sms.send('Hello World'); + const errorArgs = loggerErrorStub.getCall(0).args; + expect(errorArgs[0]).to.equal('Error sending SMS:'); + expect(errorArgs[1]).to.be.instanceOf(Error); + }); + }); + + describe('stop', () => { + it('should stopping service', async () => { + await freeMobileService.stop(); + }); + }); +}); diff --git a/server/utils/constants.js b/server/utils/constants.js index aaba8e41e1..a185c249e6 100644 --- a/server/utils/constants.js +++ b/server/utils/constants.js @@ -418,6 +418,9 @@ const ACTIONS = { MUSIC: { PLAY_NOTIFICATION: 'music.play-notification', }, + SMS: { + SEND: 'sms.send', + }, }; const INTENTS = {