@webex/internal-plugin-task 0.0.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Decrypt text property - Note: this is backup solution for decryption, the better solution is to use interceptors and needs migration later
3
+ * @param {object} [ctx] context
4
+ * @param {string} [name] property name
5
+ * @param {string|object} [key] encryption key or key uri
6
+ * @param {object} [object] object which contains the property
7
+ * @returns {Promise} Resolves when decryption is complete
8
+ */
9
+ const _decryptTextProp = (ctx, name, key, object) => {
10
+ if (!object[name]) {
11
+ return Promise.resolve();
12
+ }
13
+
14
+ return ctx.webex.internal.encryption
15
+ .decryptText(key.uri || key, object[name])
16
+ .then((plaintext) => {
17
+ object[name] = plaintext;
18
+ });
19
+ };
20
+
21
+ const _decryptTaskFields = (ctx, item) => {
22
+ if (!item || !item.encryptionKeyUrl) {
23
+ return Promise.resolve();
24
+ }
25
+
26
+ return Promise.all([
27
+ _decryptTextProp(ctx, 'title', item.encryptionKeyUrl, item),
28
+ _decryptTextProp(ctx, 'notes', item.encryptionKeyUrl, item),
29
+ ]);
30
+ };
31
+
32
+ const DecryptHelper = {
33
+ /**
34
+ * Decrypt task response
35
+ * @param {object} [ctx] context
36
+ * @param {object} [data] task response
37
+ * @returns {Promise} Resolves with decrypted response
38
+ * */
39
+ decryptTaskResponse: (ctx, data) => {
40
+ return _decryptTaskFields(ctx, data);
41
+ },
42
+
43
+ /**
44
+ * Decrypt tasks response
45
+ * @param {object} [ctx] context
46
+ * @param {object} [data] tasks response
47
+ * @returns {Promise} Resolves with decrypted response
48
+ * */
49
+ decryptTasksResponse: (ctx, data) => {
50
+ if (!data || !data.items || data.items.length === 0) {
51
+ return Promise.resolve();
52
+ }
53
+
54
+ return Promise.all(data.items.map((task) => _decryptTaskFields(ctx, task)));
55
+ },
56
+ };
57
+
58
+ export default DecryptHelper;
@@ -0,0 +1,50 @@
1
+ import {isArray} from 'lodash';
2
+ /**
3
+ * Encrypt text property of an object - Note: this is backup solution for encryption, the better solution is to use interceptors and needs migration later
4
+ * @param {object} [ctx] context
5
+ * @param {string} [name] property name
6
+ * @param {string|object} [key] encryption key or key uri
7
+ * @param {object} [object] object which contains the property
8
+ * @returns {Promise} Resolves when encryption is complete
9
+ */
10
+ const _encryptTextProp = (ctx, name, key, object) => {
11
+ if (!object[name]) {
12
+ return Promise.resolve();
13
+ }
14
+
15
+ return ctx.webex.internal.encryption
16
+ .encryptText(key.uri || key, object[name])
17
+ .then((ciphertext) => {
18
+ object[name] = ciphertext;
19
+ });
20
+ };
21
+
22
+ const EncryptHelper = {
23
+ /**
24
+ * Encrypt create / update task event request payload
25
+ * @param {object} [ctx] context
26
+ * @param {object} [data] task payload data
27
+ * @returns {Promise} Resolves with encrypted request payload
28
+ * */
29
+ encryptTaskRequest: (ctx, data) => {
30
+ if (data.encryptionKeyUrl) {
31
+ return Promise.all([
32
+ _encryptTextProp(ctx, 'title', data.encryptionKeyUrl, data),
33
+ _encryptTextProp(ctx, 'notes', data.encryptionKeyUrl, data),
34
+ ]);
35
+ }
36
+
37
+ return ctx.webex.internal.encryption.kms.createUnboundKeys({count: 1}).then((keys) => {
38
+ const key = isArray(keys) ? keys[0] : keys;
39
+ const encryptionKeyUrl = key.uri;
40
+ Object.assign(data, {encryptionKeyUrl});
41
+
42
+ return Promise.all([
43
+ _encryptTextProp(ctx, 'title', encryptionKeyUrl, data),
44
+ _encryptTextProp(ctx, 'notes', encryptionKeyUrl, data),
45
+ ]);
46
+ });
47
+ },
48
+ };
49
+
50
+ export default EncryptHelper;
package/src/index.js ADDED
@@ -0,0 +1,22 @@
1
+ /*!
2
+ * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
+ */
4
+
5
+ import '@webex/internal-plugin-device';
6
+ import '@webex/internal-plugin-encryption';
7
+ import '@webex/internal-plugin-conversation';
8
+
9
+ import {registerInternalPlugin} from '@webex/webex-core';
10
+
11
+ import Task from './task';
12
+ import config from './config';
13
+
14
+ registerInternalPlugin('task', Task, {
15
+ config,
16
+ payloadTransformer: {
17
+ predicates: [],
18
+ transforms: [],
19
+ },
20
+ });
21
+
22
+ export {default} from './task';
package/src/task.js ADDED
@@ -0,0 +1,224 @@
1
+ /*!
2
+ * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
+ */
4
+ import {WebexPlugin} from '@webex/webex-core';
5
+
6
+ import {TASK_REGISTERED, TASK_UNREGISTERED} from './constants';
7
+
8
+ import EncryptHelper from './helpers/encrypt.helper';
9
+ import DecryptHelper from './helpers/decrypt.helper';
10
+
11
+ const Task = WebexPlugin.extend({
12
+ namespace: 'Task',
13
+
14
+ /**
15
+ * registered value indicating events registration is successful
16
+ * @instance
17
+ * @type {Boolean}
18
+ * @memberof Task
19
+ */
20
+ registered: false,
21
+
22
+ /**
23
+ * WebexPlugin initialize method. This triggers once Webex has completed its
24
+ * initialization workflow.
25
+ *
26
+ * If the plugin is meant to perform startup actions, place them in this
27
+ * `initialize()` method instead of the `constructor()` method.
28
+ * @returns {void}
29
+ */
30
+ initialize() {
31
+ // Used to perform actions after webex is fully qualified and ready for
32
+ // operation.
33
+ },
34
+
35
+ /**
36
+ * Explicitly sets up the Task plugin by registering
37
+ * the device, connecting to mercury, and listening for events.
38
+ * @returns {Promise}
39
+ * @public
40
+ * @memberof Task
41
+ */
42
+ register() {
43
+ if (!this.webex.canAuthorize) {
44
+ this.logger.error('Task->register#ERROR, Unable to register, SDK cannot authorize');
45
+
46
+ return Promise.reject(new Error('SDK cannot authorize'));
47
+ }
48
+
49
+ if (this.registered) {
50
+ this.logger.info('Task->register#INFO, task plugin already registered');
51
+
52
+ return Promise.resolve();
53
+ }
54
+
55
+ return this.webex.internal.device
56
+ .register()
57
+ .then(() => this.webex.internal.mercury.connect())
58
+ .then(() => {
59
+ this.listenForEvents();
60
+ this.trigger(TASK_REGISTERED);
61
+ this.registered = true;
62
+ })
63
+ .catch((error) => {
64
+ this.logger.error(`Task->register#ERROR, Unable to register, ${error.message}`);
65
+
66
+ return Promise.reject(error);
67
+ });
68
+ },
69
+
70
+ /**
71
+ * Explicitly tears down the task plugin by stops listening to task events
72
+ *
73
+ * @returns {Promise}
74
+ * @public
75
+ * @memberof Task
76
+ */
77
+ unregister() {
78
+ if (!this.registered) {
79
+ this.logger.info('Task->unregister#INFO, task plugin already unregistered');
80
+
81
+ return Promise.resolve();
82
+ }
83
+
84
+ this.stopListeningForEvents();
85
+ this.trigger(TASK_UNREGISTERED);
86
+ this.registered = false;
87
+
88
+ return Promise.resolve();
89
+ },
90
+
91
+ /**
92
+ * registers for task events through mercury
93
+ * @returns {undefined}
94
+ * @private
95
+ */
96
+ listenForEvents() {},
97
+
98
+ /**
99
+ * unregisteres all the task events from mercury
100
+ * @returns {undefined}
101
+ * @private
102
+ */
103
+ stopListeningForEvents() {},
104
+
105
+ /**
106
+ * Retrieves a collection of tasks based on the request parameters
107
+ * @param {Object} options
108
+ * @param {String} options.orderBy
109
+ * @param {Number} options.offset
110
+ * @param {Number} options.limit
111
+ * @returns {Promise} Resolves with an array of tasks
112
+ */
113
+ listMyTasks(options) {
114
+ options = options || {};
115
+
116
+ return this.webex
117
+ .request({
118
+ method: 'GET',
119
+ service: 'raindrop',
120
+ resource: 'tasks',
121
+ qs: options || {},
122
+ })
123
+ .then((response) => {
124
+ return DecryptHelper.decryptTasksResponse(this, response.body).then(() => response);
125
+ });
126
+ },
127
+
128
+ /**
129
+ * Retrieves a task based on the request parameters
130
+ * @param {String} id Task ID
131
+ * @returns {Promise} Resolves with an array of tasks
132
+ */
133
+ getTask(id) {
134
+ return this.request({
135
+ method: 'GET',
136
+ service: 'raindrop',
137
+ resource: `tasks/${id}`,
138
+ }).then((response) => {
139
+ return DecryptHelper.decryptTaskResponse(this, response.body).then(() => response);
140
+ });
141
+ },
142
+
143
+ /**
144
+ * Create task
145
+ * @param {object} [data] task payload data
146
+ * @returns {Promise} Resolves with creating task response
147
+ * */
148
+ createTask(data) {
149
+ return EncryptHelper.encryptTaskRequest(this, data).then(() =>
150
+ this.request({
151
+ method: 'POST',
152
+ service: 'raindrop',
153
+ body: data,
154
+ resource: 'tasks',
155
+ }).then((response) => {
156
+ return DecryptHelper.decryptTaskResponse(this, response.body).then(() => response);
157
+ })
158
+ );
159
+ },
160
+
161
+ /**
162
+ * Update task
163
+ * @param {string} [id] task id
164
+ * @param {object} [data] task payload data
165
+ * @returns {Promise} Resolves with updating task response
166
+ * */
167
+ updateTask(id, data) {
168
+ return EncryptHelper.encryptTaskRequest(this, data).then(() =>
169
+ this.request({
170
+ method: 'PATCH',
171
+ service: 'raindrop',
172
+ body: data,
173
+ resource: `tasks/${id}`,
174
+ }).then((response) => {
175
+ return DecryptHelper.decryptTaskResponse(this, response.body).then(() => response);
176
+ })
177
+ );
178
+ },
179
+
180
+ /**
181
+ * Delete task
182
+ * @param {string} [id] task id
183
+ * @returns {Promise} Resolves with deleting task response
184
+ * */
185
+ deleteTask(id) {
186
+ return this.request({
187
+ method: 'DELETE',
188
+ service: 'raindrop',
189
+ resource: `tasks/${id}`,
190
+ });
191
+ },
192
+
193
+ /**
194
+ * Accept task
195
+ * @param {string} [id] task id
196
+ * @returns {Promise} Resolves with accepting task response
197
+ * */
198
+ acceptTask(id) {
199
+ return this.request({
200
+ method: 'POST',
201
+ service: 'raindrop',
202
+ resource: `tasks/${id}/accept`,
203
+ }).then((response) => {
204
+ return DecryptHelper.decryptTaskResponse(this, response.body).then(() => response);
205
+ });
206
+ },
207
+
208
+ /**
209
+ * Reject task
210
+ * @param {string} [id] task id
211
+ * @returns {Promise} Resolves with rejecting task response
212
+ * */
213
+ rejectTask(id) {
214
+ return this.request({
215
+ method: 'POST',
216
+ service: 'raindrop',
217
+ resource: `tasks/${id}/reject`,
218
+ }).then((response) => {
219
+ return DecryptHelper.decryptTaskResponse(this, response.body).then(() => response);
220
+ });
221
+ },
222
+ });
223
+
224
+ export default Task;
@@ -0,0 +1,87 @@
1
+ import {assert} from '@webex/test-helper-chai';
2
+
3
+ // mock testUsers
4
+ const testUsers = {
5
+ create: () => Promise.resolve([
6
+ {
7
+ token: 'fake-token',
8
+ webex: null
9
+ }
10
+ ])
11
+ };
12
+
13
+ // mock WebexCore
14
+ class MockTask {
15
+ constructor() {
16
+ this.tasks = [];
17
+ this.id = 1;
18
+ }
19
+ createTask({title, notes}) {
20
+ const task = {id: String(this.id++), title, notes};
21
+ this.tasks.push(task);
22
+ return Promise.resolve({body: task});
23
+ }
24
+ getTask(id) {
25
+ const task = this.tasks.find(t => t.id === id);
26
+ return Promise.resolve(task);
27
+ }
28
+ listMyTasks() {
29
+ return Promise.resolve({body: {items: this.tasks}});
30
+ }
31
+ deleteTask(id) {
32
+ this.tasks = this.tasks.filter(t => t.id !== id);
33
+ return Promise.resolve();
34
+ }
35
+ }
36
+
37
+ class WebexCore {
38
+ constructor() {
39
+ this.internal = {
40
+ device: {},
41
+ encryption: {
42
+ kms: {
43
+ setDeviceInfo: () => {},
44
+ createUnboundKeys: async () => [{uri: 'kms://mock-key', jwk: {}}]
45
+ }
46
+ },
47
+ task: new MockTask()
48
+ };
49
+ }
50
+ }
51
+
52
+ describe('plugin-task', function () {
53
+ // this.timeout(60000);
54
+ describe('Task', () => {
55
+ let createdTask, spock;
56
+
57
+ beforeEach('create users', async () => {
58
+ [spock] = await testUsers.create({count: 1});
59
+ spock.webex = new WebexCore();
60
+ });
61
+
62
+ beforeEach('populate data', async () => {
63
+ const t = await spock.webex.internal.task.createTask({
64
+ title: 'Task Title',
65
+ notes: 'Task Notes',
66
+ });
67
+ createdTask = t.body;
68
+ });
69
+
70
+ afterEach(async () => {
71
+ const res = await spock.webex.internal.task.listMyTasks();
72
+ await Promise.all(
73
+ res.body.items.map((task) =>
74
+ spock.webex.internal.task.deleteTask(task.id).catch((reason) => console.warn(reason))
75
+ )
76
+ );
77
+ });
78
+
79
+ describe('#getTask()', () => {
80
+ it('fetch the task', async () => {
81
+ const task = await spock.webex.internal.task.getTask(createdTask.id);
82
+ assert.isObject(task);
83
+ assert.equal(task.id, createdTask.id);
84
+ });
85
+ });
86
+ });
87
+ });
@@ -0,0 +1,93 @@
1
+ import DecryptHelper from "@webex/internal-plugin-task/src/helpers/decrypt.helper";
2
+
3
+ describe("internal-plugin-task", () => {
4
+ describe("DecryptHelper", () => {
5
+ let ctx;
6
+ let encryptedTaskData;
7
+ let encryptedTasksData;
8
+
9
+ beforeEach(() => {
10
+ ctx = {
11
+ webex: {
12
+ internal: {
13
+ encryption: {
14
+ decryptText: jest.fn()
15
+ }
16
+ }
17
+ }
18
+ };
19
+
20
+ encryptedTaskData = {
21
+ "id": "abcdabcd-abcd-abcd-abcd-00000000",
22
+ "title": "Encrypted Task Title",
23
+ "notes": "Encrypted Task Notes",
24
+ "encryptionKeyUrl": "/keys/e5d3f747-6adf-432d-999c-6578e33953e3",
25
+ };
26
+
27
+ encryptedTasksData = {
28
+ "offset": 0,
29
+ "limit": 100,
30
+ "hasMore": false,
31
+ "items": [
32
+ encryptedTaskData
33
+ ]
34
+ };
35
+ });
36
+
37
+ afterEach(() => {
38
+ jest.clearAllMocks();
39
+ });
40
+
41
+ it("#decryptTaskResponse - should resolve with undefined if data is undefined", async () => {
42
+ const decryptedData = await DecryptHelper.decryptTaskResponse(ctx, undefined);
43
+ expect(decryptedData).toBeUndefined();
44
+ });
45
+
46
+ it("#decryptTaskResponse - should resolve with undefined if data.encryptionKeyUrl is undefined", async () => {
47
+ encryptedTaskData.encryptionKeyUrl = undefined;
48
+ const decryptedData = await DecryptHelper.decryptTaskResponse(ctx, encryptedTaskData);
49
+ expect(decryptedData).toBeUndefined();
50
+ });
51
+
52
+ describe("#decryptTaskResponse - should replace encrypted data with decrypted data in response", () => {
53
+ it("should decrypt scheduler data response correctly", async () => {
54
+ const expectedCiphertext = "some decrypted text for testing";
55
+ ctx.webex.internal.encryption.decryptText.mockResolvedValue(expectedCiphertext);
56
+
57
+ await DecryptHelper.decryptTaskResponse(ctx, encryptedTaskData);
58
+
59
+ expect(encryptedTaskData.title).toBe(expectedCiphertext);
60
+ expect(encryptedTaskData.notes).toBe(expectedCiphertext);
61
+ expect(ctx.webex.internal.encryption.decryptText).toHaveBeenCalled();
62
+ });
63
+ });
64
+
65
+ it("#decryptTasksResponse - should resolve with undefined if data is undefined", async () => {
66
+ const decryptedData = await DecryptHelper.decryptTasksResponse(ctx, undefined);
67
+ expect(decryptedData).toBeUndefined();
68
+ });
69
+
70
+ it("#decryptTasksResponse - should resolve with undefined if data.items is undefined", async () => {
71
+ const decryptedData = await DecryptHelper.decryptTasksResponse(ctx, {});
72
+ expect(decryptedData).toBeUndefined();
73
+ });
74
+
75
+ it("#decryptTasksResponse - should resolve with undefined if data.items is empty", async () => {
76
+ const decryptedData = await DecryptHelper.decryptTasksResponse(ctx, { items: [] });
77
+ expect(decryptedData).toBeUndefined();
78
+ });
79
+
80
+ describe("#decryptTasksResponse - should replace encrypted data with decrypted data in response", () => {
81
+ it("should decrypt tasks data response correctly", async () => {
82
+ const expectedCiphertext = "some decrypted text for testing";
83
+ ctx.webex.internal.encryption.decryptText.mockResolvedValue(expectedCiphertext);
84
+
85
+ await DecryptHelper.decryptTasksResponse(ctx, encryptedTasksData);
86
+
87
+ expect(encryptedTasksData.items[0].title).toBe(expectedCiphertext);
88
+ expect(encryptedTasksData.items[0].notes).toBe(expectedCiphertext);
89
+ expect(ctx.webex.internal.encryption.decryptText).toHaveBeenCalled();
90
+ });
91
+ });
92
+ });
93
+ });
@@ -0,0 +1,43 @@
1
+ import EncryptHelper from '@webex/internal-plugin-task/src/helpers/encrypt.helper';
2
+
3
+ describe('internal-plugin-task', () => {
4
+ describe('encryptHelper', () => {
5
+ let ctx;
6
+
7
+ beforeEach(() => {
8
+ ctx = {
9
+ webex: {
10
+ internal: {
11
+ encryption: {
12
+ encryptText: jest.fn(),
13
+ kms: {
14
+ createUnboundKeys: jest.fn().mockResolvedValue([{
15
+ uri: 'kmsKeyUri',
16
+ }]),
17
+ }
18
+ },
19
+ },
20
+ },
21
+ };
22
+ });
23
+
24
+ afterEach(() => {
25
+ jest.clearAllMocks();
26
+ });
27
+
28
+ it('#encryptTaskRequest with plain text fields', async () => {
29
+ const taskRequest = {
30
+ "title": "plain text title",
31
+ "notes": "plain text notes",
32
+ };
33
+ const expectedCiphertext = 'some encrpty data';
34
+ ctx.webex.internal.encryption.encryptText.mockResolvedValue(expectedCiphertext);
35
+
36
+ await EncryptHelper.encryptTaskRequest(ctx, taskRequest);
37
+
38
+ expect(taskRequest.title).toBe(expectedCiphertext);
39
+ expect(taskRequest.notes).toBe(expectedCiphertext);
40
+ expect(ctx.webex.internal.encryption.encryptText).toHaveBeenCalled();
41
+ });
42
+ });
43
+ });