Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(internal-device-plugin): added deleteDevices #3998

Open
wants to merge 3 commits into
base: next
Choose a base branch
from

Conversation

shnaaz
Copy link
Contributor

@shnaaz shnaaz commented Nov 21, 2024

COMPLETES

https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-584363

Added a method deleteDevices which is called when register fails.
This is done to tackle failures where stale device registrations don't get deleted. This is possible when user just closes the browser instead of leaving a meeting.
Added logic where register is tried once more after failure.

Summary by CodeRabbit

  • New Features

    • Introduced a new method for automatic deletion of devices when the number exceeds a specified limit.
    • Enhanced error handling during device registration to manage device lifecycle more effectively.
  • Bug Fixes

    • Improved registration process to handle errors and manage device cleanup when registration fails.
  • Tests

    • Added test cases to validate the new device deletion functionality and error handling in the registration process.

@shnaaz shnaaz requested review from a team as code owners November 21, 2024 10:27
Copy link

coderabbitai bot commented Nov 21, 2024

Walkthrough

The changes introduce a new method deleteDevices in the Device class, which deletes the oldest third of devices of type 'WEB' if more than two exist. The register method now includes error handling that calls deleteDevices upon registration failure. The original register method is replaced by a new internal method _registerInternal. Additionally, the _getBody method is introduced, and the refresh method's error handling is updated to attempt re-registration on a 404 error. These modifications enhance device lifecycle management and error handling.

Changes

File Path Change Summary
packages/@webex/internal-plugin-device/src/device.js - Added method: deleteDevices()
- Modified register() to call deleteDevices() on error
- Renamed register to _registerInternal and updated its logic
- Introduced _getBody() method
- Enhanced error handling in refresh() method
packages/@webex/internal-plugin-device/test/unit/spec/device.js - Added test cases for deleteDevices()
- Modified tests for register() to include error handling logic

Poem

In the meadow where devices play,
A rabbit hops and shouts hooray!
With deleteDevices, old ones go,
Leaving room for new friends to grow.
Registration's smoother, errors fade,
In this lively tech parade! 🐇✨

Warning

There were issues while running some tools. Please review the errors and either fix the tool’s configuration or disable the tool if it’s a critical failure.

🔧 eslint (1.23.1)

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

warning eslint@8.57.1: This version is no longer supported. Please see https://eslint.org/version-support for other options.
warning eslint > @humanwhocodes/config-array@0.13.0: Use @eslint/config-array instead
warning eslint > @humanwhocodes/config-array > @humanwhocodes/object-schema@2.0.3: Use @eslint/object-schema instead
warning eslint > file-entry-cache > flat-cache > rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported
warning eslint > file-entry-cache > flat-cache > rimraf > glob@7.2.3: Glob versions prior to v9 are no longer supported
warning eslint > file-entry-cache > flat-cache > rimraf > glob > inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
warning eslint-import-resolver-typescript > glob@7.2.3: Glob versions prior to v9 are no longer supported
warning glob@7.2.3: Glob versions prior to v9 are no longer supported
warning intern > glob@7.1.7: Glob versions prior to v9 are no longer supported
warning intern > glob > inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
warning jasmine > glob@7.2.3: Glob versions prior to v9 are no longer supported
warning jest > jest-cli > jest-config > glob@7.2.3: Glob versions prior to v9 are no longer supported
warning jest > @jest/core > jest-runtime > glob@7.2.3: Glob versions prior to v9 are no longer supported
warning jest > @jest/core > @jest/reporters > glob@7.2.3: Glob versions prior to v9 are no longer supported
warning jest > @jest/core > @jest/transform > babel-plugin-istanbul > test-exclude > glob@7.2.3: Glob versions prior to v9 are no longer supported
warning mocha > glob@7.2.0: Glob versions prior to v9 are no longer supported
warning mocha > glob > inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
warning sinon@9.2.4: 16.1.1
warning wd > request@2.88.0: request has been deprecated, see request/request#3142
warning wd > q@1.5.1: You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.

(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)
warning wd > archiver > glob@7.2.3: Glob versions prior to v9 are no longer supported
warning wd > request > uuid@3.4.0: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.
warning wd > request > har-validator@5.1.5: this library is no longer supported
warning wd > archiver > archiver-utils > glob@7.2.3: Glob versions prior to v9 are no longer supported
warning @babel/cli > glob@7.2.3: Glob versions prior to v9 are no longer supported
warning @babel/plugin-proposal-async-generator-functions@7.20.7: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead.
warning @babel/plugin-proposal-class-properties@7.18.6: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.
warning @babel/plugin-proposal-export-namespace-from@7.18.9: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead.
warning @babel/plugin-proposal-nullish-coalescing-operator@7.18.6: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.
warning @babel/plugin-proposal-object-rest-spread@7.20.7: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead.
warning @babel/plugin-proposal-optional-chaining@7.21.0: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.
warning @babel/polyfill@7.12.1: 🚨 This package has been deprecated in favor of separate inclusion of a polyfill and regenerator-runtime (when needed). See the @babel/polyfill docs (https://babeljs.io/docs/en/babel-polyfill) for more information.
warning @babel/polyfill > core-js@2.6.12: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.
warning @babel/runtime-corejs2 > core-js@2.6.12: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.
warning babel-plugin-lodash > glob@7.2.3: Glob versions prior to v9 are no longer supported
warning workspace-aggregator-69f6899d-a0a1-4a30-8bc0-0045104318af > eslint@8.57.1: This version is no longer supported. Please see https://eslint.org/version-support for other options.
warning workspace-aggregator-69f6899d-a0a1-4a30-8bc0-0045104318af > glob@7.2.3: Glob versions prior to v9 are no longer supported
warning workspace-aggregator-69f6899d-a0a1-4a30-8bc0-0045104318af > sinon@9.2.4: 16.1.1
warning workspace-aggregator-69f6899d-a0a1-4a30-8bc0-0045104318af > @babel/plugin-proposal-async-generator-functions@7.20.7: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead.
warning workspace-aggregator-69f6899d-a0a1-4a30-8bc0-0045104318af > @babel/plugin-proposal-class-properties@7.18.6: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.
warning workspace-aggregator-69f6899d-a0a1-4a30-8bc0-0045104318af > @babel/plugin-proposal-export-namespace-from@7.18.9: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead.
warning workspace-aggregator-69f6899d-a0a1-4a30-8bc0-0045104318af > @babel/plugin-proposal-nullish-coalescing-operator@7.18.6: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.
warning workspace-aggregator-69f6899d-a0a1-4a30-8bc0-0045104318af > @babel/plugin-proposal-object-rest-spread@7.20.7: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead.
warning workspace-aggregator-69f6899d-a0a1-4a30-8bc0-0045104318af > @babel/plugin-proposal-optional-chaining@7.21.0: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.
warning workspace-aggregator-69f6899d-a0a1-4a30-8bc0-0045104318af > @babel/polyfill@7.12.1: 🚨 This package has been deprecated in favor of separate inclusion of a polyfill and regenerator-runtime (when needed). See the @babel/polyfill docs (https://babeljs.io/docs/en/babel-polyfill) for more information.
error eslint-plugin-jsdoc@40.3.0: The engine "node" is incompatible with this module. Expected version "^14 || ^16 || ^17 || ^18 || ^19". Got "22.9.0"
error Found incompatible module.


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@shnaaz shnaaz added the validated If the pull request is validated for automation. label Nov 21, 2024
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Outside diff range and nitpick comments (2)
packages/@webex/internal-plugin-device/src/device.js (2)

Line range hint 510-585: Remove unnecessary block statements

There are redundant block statements in the code between lines 510-585:

  • Unnecessary blocks: Standalone block statements without declarations clutter the code.

Consider removing these blocks to improve readability:

-  {
     // Method implementation
-  }

This enhances code clarity without altering functionality.

🧰 Tools
🪛 Biome

[error] 508-509: Decorators are not valid here.

Decorators are only valid on class declarations, class expressions, and class methods.

(parse)


[error] 510-510: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)


[error] 510-510: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


489-498: Assignment within an expression can be confusing

Using assignments within expressions might lead to unintended side effects:

  • Confusing assignments: Assignments in expressions can reduce code readability.

Refactor the code to eliminate the assignment within the expression:

-          if (!deviceRegistrationOptions.deleteFlag) {
+          const {deleteFlag = false} = deviceRegistrationOptions;
+          if (!deleteFlag) {

This makes the code clearer and more maintainable.

🧰 Tools
🪛 Biome

[error] 489-489: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)


[error] 490-497: Illegal return statement outside of a function

(parse)


[error] 498-498: Expected a statement but instead found ','.

Expected a statement here.

(parse)


[error] 489-489: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


[error] 489-498: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.

(lint/complexity/noUselessLoneBlockStatements)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 8d6d1bc and 1b9b12a.

📒 Files selected for processing (2)
  • packages/@webex/internal-plugin-device/src/device.js (3 hunks)
  • packages/@webex/internal-plugin-device/test/unit/spec/device.js (2 hunks)
🧰 Additional context used
🪛 Biome
packages/@webex/internal-plugin-device/src/device.js

[error] 449-481: Illegal return statement outside of a function

(parse)


[error] 482-482: Expected a statement but instead found ','.

Expected a statement here.

(parse)


[error] 487-488: Decorators are not valid here.

Decorators are only valid on class declarations, class expressions, and class methods.

(parse)


[error] 489-489: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)


[error] 490-497: Illegal return statement outside of a function

(parse)


[error] 498-498: Expected a statement but instead found ','.

Expected a statement here.

(parse)


[error] 489-489: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


[error] 447-482: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.

(lint/complexity/noUselessLoneBlockStatements)


[error] 489-498: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.

(lint/complexity/noUselessLoneBlockStatements)


[error] 510-510: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)


[error] 510-510: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


[error] 510-585: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.

(lint/complexity/noUselessLoneBlockStatements)

🔇 Additional comments (8)
packages/@webex/internal-plugin-device/src/device.js (4)

5-5: Importing 'orderBy' from 'lodash'

The orderBy function is correctly imported from 'lodash' and used later in the code.


487-498: ⚠️ Potential issue

Incorrect use of decorators and syntax errors in 'register' method

The register method has syntax errors due to incorrect use of decorators and misplaced block statements:

  • Decorators are not valid here: Decorators should be applied directly to methods within classes.
  • Illegal return statement outside of a function: Return statements are incorrectly placed.
  • Assignment within an expression: Assignments should not be used within expressions.

Please adjust the method as follows to fix the syntax errors:

+  /**
+   * Registers the device and deletes old devices upon failure
+   *
+   * @param {Object} deviceRegistrationOptions - Options for device registration
+   * @returns {Promise<void>}
+   */
+  @oneFlight
+  @waitForValue('@')
+  register(deviceRegistrationOptions = {}) {
+    return this._registerInternal(deviceRegistrationOptions).catch((error) => {
+      if (!deviceRegistrationOptions.deleteFlag) {
+        return this.deleteDevices().then(() =>
+          this._registerInternal({...deviceRegistrationOptions, deleteFlag: true})
+        );
+      }
+      throw error;
+    });
+  },

This correction:

  • Places decorators directly above the method definition.
  • Fixes the placement of return statements.
  • Avoids assignment within expressions by using object spread {...deviceRegistrationOptions, deleteFlag: true}.

Likely invalid or redundant comment.

🧰 Tools
🪛 Biome

[error] 487-488: Decorators are not valid here.

Decorators are only valid on class declarations, class expressions, and class methods.

(parse)


[error] 489-489: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)


[error] 490-497: Illegal return statement outside of a function

(parse)


[error] 498-498: Expected a statement but instead found ','.

Expected a statement here.

(parse)


[error] 489-489: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


[error] 489-498: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.

(lint/complexity/noUselessLoneBlockStatements)


510-510: ⚠️ Potential issue

Syntax error and decorator misuse in '_registerInternal' method

There's a syntax error due to the incorrect placement of decorators on the _registerInternal method:

  • Decorators are not valid here: Decorators must be applied directly to class methods.

Please apply the decorators correctly as shown below:

+  /**
+   * Internal method for device registration
+   *
+   * @param {Object} deviceRegistrationOptions - Options for device registration
+   * @returns {Promise<void>}
+   */
+  @oneFlight
+  @waitForValue('@')
+  _registerInternal(deviceRegistrationOptions = {}) {
     this.logger.info('device: registering');
     // Rest of the method...
  },

This ensures that:

  • Decorators are correctly applied.
  • Syntax errors are resolved.

Likely invalid or redundant comment.

🧰 Tools
🪛 Biome

[error] 510-510: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)


[error] 510-510: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


[error] 510-585: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.

(lint/complexity/noUselessLoneBlockStatements)


447-482: Static analysis reports multiple syntax issues

Static analysis tools have identified several syntax errors in this code segment:

  • Illegal return statements
  • Invalid decorator usage
  • Unnecessary block statements

Please run the following command to identify and fix syntax errors:

This will help in pinpointing the exact locations of syntax issues for correction.

🧰 Tools
🪛 Biome

[error] 449-481: Illegal return statement outside of a function

(parse)


[error] 482-482: Expected a statement but instead found ','.

Expected a statement here.

(parse)


[error] 447-482: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.

(lint/complexity/noUselessLoneBlockStatements)

packages/@webex/internal-plugin-device/test/unit/spec/device.js (4)

360-394: New Test Cases for deleteDevices() Functionality

The test cases added for deleteDevices() effectively cover the scenarios where devices should be deleted based on the specified criteria.


427-449: Test Case for No Deletion When Only Two Devices Exist

The test correctly verifies that no devices are deleted when there are only two devices, aligning with the expected behavior.


480-495: Test for Invoking deleteDevices() When Registration Fails and deleteFlag is False

The test appropriately verifies that deleteDevices() is called upon registration failure when deleteFlag is false, and that register is retried as expected.


497-512: Test for Not Invoking deleteDevices() When deleteFlag is True

The test correctly ensures that deleteDevices() is not called when registration fails and deleteFlag is true, adhering to the intended logic.

Comment on lines 470 to 476
this.request({
uri: url,
method: 'DELETE',
}).then(() => {
this.clear();
});
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Potential issue with 'this.clear()' inside deletion loop

Calling this.clear() inside the loop may lead to unintended side effects:

  • Clearing the device multiple times: this.clear() is called for each device deletion.
  • Asynchronous issues: The loop does not wait for deletion requests to complete before clearing.

Move this.clear() outside the loop and ensure all deletions are complete before clearing:

              // Send DELETE requests for each URL
-             urlsToDelete.forEach((url) => {
-               this.request({
-                 uri: url,
-                 method: 'DELETE',
-               }).then(() => {
-                 this.clear();
-               });
-             });
+             await Promise.all(
+               urlsToDelete.map((url) =>
+                 this.request({
+                   uri: url,
+                   method: 'DELETE',
+                 })
+               )
+             );
+             // Clear after all deletions are complete
+             this.clear();

This adjustment:

  • Uses Promise.all with await to handle asynchronous deletion requests.
  • Ensures this.clear() is called once after all deletions.

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 447 to 482
deleteDevices() {
// Fetch devices with a GET request
return this.request({
method: 'GET',
service: 'wdm',
resource: 'devices',
})
.then((response) => {
const {devices} = response.body;

// Filter devices of type 'WEB'
const webDevices = devices.filter((item) => item.deviceType === 'WEB');

const sortedDevices = orderBy(webDevices, [(item) => new Date(item.modificationTime)]);

// If there are more than two devices, delete the last third
if (sortedDevices.length > 2) {
const totalItems = sortedDevices.length;
const countToDelete = Math.ceil(totalItems / 3);
const urlsToDelete = sortedDevices.slice(0, countToDelete).map((item) => item.url);

// Send DELETE requests for each URL
urlsToDelete.forEach((url) => {
this.request({
uri: url,
method: 'DELETE',
}).then(() => {
this.clear();
});
});
}
})
.catch((error) => {
this.logger.error('Failed to retrieve devices:', error);
});
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Critical syntax errors in 'deleteDevices' method

There are several syntax errors in the deleteDevices method that prevent the code from compiling:

  • Illegal return statement outside of a function: The return statements may be misplaced.
  • Decorators are not valid here: Decorators like @oneFlight and @waitForValue('@') are incorrectly applied.
  • Unnecessary block statements: There are block statements that serve no purpose.
  • Promise handling issues: Improper use of .then() and .catch() chaining.

To fix these issues, consider refactoring the method as follows:

+  /**
+   * Fetches the web devices and deletes the third of them which are not recent devices in use
+   * @returns {Promise<void>}
+   */
+  async deleteDevices() {
+    try {
+      // Fetch devices with a GET request
+      const response = await this.request({
+        method: 'GET',
+        service: 'wdm',
+        resource: 'devices',
+      });
+
+      const {devices} = response.body;
+
+      // Filter devices of type 'WEB'
+      const webDevices = devices.filter((item) => item.deviceType === 'WEB');
+
+      const sortedDevices = orderBy(webDevices, [(item) => new Date(item.modificationTime)]);
+
+      // If there are more than two devices, delete the oldest third
+      if (sortedDevices.length > 2) {
+        const totalItems = sortedDevices.length;
+        const countToDelete = Math.ceil(totalItems / 3);
+        const urlsToDelete = sortedDevices.slice(0, countToDelete).map((item) => item.url);
+
+        // Send DELETE requests for each URL
+        await Promise.all(
+          urlsToDelete.map((url) =>
+            this.request({
+              uri: url,
+              method: 'DELETE',
+            })
+          )
+        );
+        // Clear after deletions are complete
+        this.clear();
+      }
+    } catch (error) {
+      this.logger.error('Failed to retrieve devices:', error);
+    }
+  },

This refactoring:

  • Uses async/await for better readability and error handling.
  • Ensures that this.clear() is called only once after all deletions are complete.
  • Fixes syntax errors and removes unnecessary block statements.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
deleteDevices() {
// Fetch devices with a GET request
return this.request({
method: 'GET',
service: 'wdm',
resource: 'devices',
})
.then((response) => {
const {devices} = response.body;
// Filter devices of type 'WEB'
const webDevices = devices.filter((item) => item.deviceType === 'WEB');
const sortedDevices = orderBy(webDevices, [(item) => new Date(item.modificationTime)]);
// If there are more than two devices, delete the last third
if (sortedDevices.length > 2) {
const totalItems = sortedDevices.length;
const countToDelete = Math.ceil(totalItems / 3);
const urlsToDelete = sortedDevices.slice(0, countToDelete).map((item) => item.url);
// Send DELETE requests for each URL
urlsToDelete.forEach((url) => {
this.request({
uri: url,
method: 'DELETE',
}).then(() => {
this.clear();
});
});
}
})
.catch((error) => {
this.logger.error('Failed to retrieve devices:', error);
});
},
/**
* Fetches the web devices and deletes the third of them which are not recent devices in use
* @returns {Promise<void>}
*/
async deleteDevices() {
try {
// Fetch devices with a GET request
const response = await this.request({
method: 'GET',
service: 'wdm',
resource: 'devices',
});
const {devices} = response.body;
// Filter devices of type 'WEB'
const webDevices = devices.filter((item) => item.deviceType === 'WEB');
const sortedDevices = orderBy(webDevices, [(item) => new Date(item.modificationTime)]);
// If there are more than two devices, delete the oldest third
if (sortedDevices.length > 2) {
const totalItems = sortedDevices.length;
const countToDelete = Math.ceil(totalItems / 3);
const urlsToDelete = sortedDevices.slice(0, countToDelete).map((item) => item.url);
// Send DELETE requests for each URL
await Promise.all(
urlsToDelete.map((url) =>
this.request({
uri: url,
method: 'DELETE',
})
)
);
// Clear after deletions are complete
this.clear();
}
} catch (error) {
this.logger.error('Failed to retrieve devices:', error);
}
},
🧰 Tools
🪛 Biome

[error] 449-481: Illegal return statement outside of a function

(parse)


[error] 482-482: Expected a statement but instead found ','.

Expected a statement here.

(parse)


[error] 447-482: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.

(lint/complexity/noUselessLoneBlockStatements)

Comment on lines 395 to 426
it('should delete correct number of devices', async () => {
const response = {
body: {
devices: [
{url: 'url3', modificationTime: '2023-10-03T10:00:00Z', deviceType: 'WEB'},
{url: 'url4', modificationTime: '2023-10-04T10:00:00Z', deviceType: 'notweb'},
{url: 'url1', modificationTime: '2023-10-01T10:00:00Z', deviceType: 'WEB'},
{url: 'url2', modificationTime: '2023-10-02T10:00:00Z', deviceType: 'WEB'},
]
}
};
const requestStub = sinon.stub(device, 'request');
requestStub.withArgs(sinon.match({method: 'GET'})).resolves(response);
requestStub.withArgs(sinon.match({method: 'DELETE'})).resolves();

const clearSpy = sinon.spy(device, 'clear');
await device.deleteDevices();

const expectedDeletions = ['url1'];

expectedDeletions.forEach(url => {
assert(requestStub.calledWith(sinon.match({uri: url, method: 'DELETE'})));
});

const notDeletedUrls = ['url2', 'url3', 'url4'];
notDeletedUrls.forEach(url => {
assert(requestStub.neverCalledWith(sinon.match({uri: url, method: 'DELETE'})));
});

assert.equal(clearSpy.callCount, expectedDeletions.length);
});

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Duplicate Test Case with Identical Description and Implementation

The test case starting at line 395 appears to be a duplicate of the test case starting at line 361. Both tests have the same description 'should delete correct number of devices' and contain identical code. This duplication may lead to confusion and unnecessary maintenance.

Consider removing the duplicate test or modifying it to cover a different scenario with a unique description.

Copy link

This pull request is automatically being deployed by Amplify Hosting (learn more).

Access this pull request here: https://pr-3998.d3m3l2kee0btzx.amplifyapp.com

return this._registerInternal(deviceRegistrationOptions).catch((error) => {
if (!deviceRegistrationOptions.deleteFlag) {
return this.deleteDevices().then(() => {
return this._registerInternal({deleteFlag: true});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: we don't need the delete flag anymore

return this._registerInternal(deviceRegistrationOptions).catch((error) => {
if (!deviceRegistrationOptions.deleteFlag) {
return this.deleteDevices().then(() => {
return this._registerInternal({deleteFlag: true});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: we should be passing in the original options

@waitForValue('@')
register(deviceRegistrationOptions = {}) {
return this._registerInternal(deviceRegistrationOptions).catch((error) => {
if (!deviceRegistrationOptions.deleteFlag) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: instead of checking the delete flag, we should be checking for the error message of excessive device registrations

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Outside diff range and nitpick comments (3)
packages/@webex/internal-plugin-device/test/unit/spec/device.js (2)

361-392: Enhance test coverage with additional test cases.

While the test verifies basic functionality, consider adding the following test cases for better coverage:

  • Empty device list
  • List with only non-WEB devices
  • Single device
  • Edge case where number of devices to delete is not a whole number (e.g., 5 devices where 1.67 should be deleted)
 it('should delete correct number of devices', async () => {
+  // Given 7 WEB devices, we expect to delete the oldest 2 devices (rounded down from 2.33)
   const response = {
     body: {
       devices: [

445-460: Verify parameter consistency in retry attempt.

The test verifies that registration is retried after device deletion, but doesn't confirm that the same parameters are passed to both attempts. Consider enhancing the test to verify parameter consistency.

 it('calls delete devices when errors with User has excessive device registrations', async () => {
   setup();
   const deleteDeviceSpy = sinon.stub(device, 'deleteDevices').callsFake(() => Promise.resolve());
   const registerStub = sinon.stub(device, '_registerInternal');
+  const testParams = {includeDetails: 'test-param'};
   
   registerStub.onFirstCall().rejects({body: {message: 'User has excessive device registrations'}});
   registerStub.onSecondCall().callsFake(() => Promise.resolve({exampleKey: 'example response value',}));

-  const result = await device.register();
+  const result = await device.register(testParams);

   assert.calledOnce(deleteDeviceSpy);
   assert.equal(registerStub.callCount, 2);
+  assert.calledWith(registerStub.firstCall, testParams);
+  assert.calledWith(registerStub.secondCall, testParams);
   
   assert.deepEqual(result, {exampleKey: 'example response value'});
 });

+it('propagates error when both registration attempts fail', async () => {
+  setup();
+  const deleteDeviceSpy = sinon.stub(device, 'deleteDevices').callsFake(() => Promise.resolve());
+  const registerStub = sinon.stub(device, '_registerInternal');
+  
+  const error = {body: {message: 'User has excessive device registrations'}};
+  registerStub.rejects(error);
+
+  await assert.isRejected(device.register(), error);
+  assert.calledOnce(deleteDeviceSpy);
+  assert.calledTwice(registerStub);
+});
packages/@webex/internal-plugin-device/src/device.js (1)

482-496: Enhance error handling and logging in register method

The retry logic is good, but could be improved with better error handling and logging.

Consider these improvements:

+ const EXCESSIVE_DEVICE_ERROR = 'User has excessive device registrations';

  register(deviceRegistrationOptions = {}) {
    return this._registerInternal(deviceRegistrationOptions).catch((error) => {
-     if (error?.body?.message === 'User has excessive device registrations') {
+     if (error?.body?.message === EXCESSIVE_DEVICE_ERROR) {
+       this.logger.info('device: excessive registrations detected, attempting cleanup');
        return this.deleteDevices().then(() => {
+         this.logger.info('device: retrying registration after cleanup');
          return this._registerInternal(deviceRegistrationOptions);
        });
      }
+     this.logger.error('device: registration failed', error);
      throw error;
    });
  },
🧰 Tools
🪛 Biome

[error] 485-486: Decorators are not valid here.

Decorators are only valid on class declarations, class expressions, and class methods.

(parse)


[error] 487-487: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)


[error] 488-495: Illegal return statement outside of a function

(parse)


[error] 496-496: Expected a statement but instead found ','.

Expected a statement here.

(parse)


[error] 487-487: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


[error] 487-496: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.

(lint/complexity/noUselessLoneBlockStatements)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 1b9b12a and a7b2468.

📒 Files selected for processing (2)
  • packages/@webex/internal-plugin-device/src/device.js (3 hunks)
  • packages/@webex/internal-plugin-device/test/unit/spec/device.js (2 hunks)
🧰 Additional context used
🪛 Biome
packages/@webex/internal-plugin-device/src/device.js

[error] 449-479: Illegal return statement outside of a function

(parse)


[error] 480-480: Expected a statement but instead found ','.

Expected a statement here.

(parse)


[error] 485-486: Decorators are not valid here.

Decorators are only valid on class declarations, class expressions, and class methods.

(parse)


[error] 487-487: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)


[error] 488-495: Illegal return statement outside of a function

(parse)


[error] 496-496: Expected a statement but instead found ','.

Expected a statement here.

(parse)


[error] 487-487: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


[error] 447-480: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.

(lint/complexity/noUselessLoneBlockStatements)


[error] 487-496: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.

(lint/complexity/noUselessLoneBlockStatements)


[error] 508-508: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)


[error] 508-508: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


[error] 508-583: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.

(lint/complexity/noUselessLoneBlockStatements)

🔇 Additional comments (2)
packages/@webex/internal-plugin-device/test/unit/spec/device.js (1)

462-477: LGTM! Good negative test case.

The test properly verifies that device deletion is not triggered for other types of errors.

packages/@webex/internal-plugin-device/src/device.js (1)

508-508: LGTM: Good method renaming

The renaming of register to _registerInternal follows best practices for internal methods while maintaining the same functionality.

🧰 Tools
🪛 Biome

[error] 508-508: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)


[error] 508-508: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


[error] 508-583: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.

(lint/complexity/noUselessLoneBlockStatements)

Comment on lines 447 to 480
deleteDevices() {
// Fetch devices with a GET request
return this.request({
method: 'GET',
service: 'wdm',
resource: 'devices',
})
.then((response) => {
const {devices} = response.body;

// Filter devices of type 'WEB'
const webDevices = devices.filter((item) => item.deviceType === 'WEB');

const sortedDevices = orderBy(webDevices, [(item) => new Date(item.modificationTime)]);

// If there are more than two devices, delete the last third
if (sortedDevices.length > 2) {
const totalItems = sortedDevices.length;
const countToDelete = Math.ceil(totalItems / 3);
const urlsToDelete = sortedDevices.slice(0, countToDelete).map((item) => item.url);

// Send DELETE requests for each URL
urlsToDelete.forEach((url) => {
this.request({
uri: url,
method: 'DELETE',
});
});
}
})
.catch((error) => {
this.logger.error('Failed to retrieve devices:', error);
});
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Critical: Fix async operations and error handling in deleteDevices

The current implementation has several critical issues:

  1. DELETE requests are not awaited, potentially leading to race conditions
  2. this.clear() is called multiple times unnecessarily
  3. Errors are swallowed in the catch block, making debugging difficult

Apply this fix to properly handle async operations and error handling:

  deleteDevices() {
    // Fetch devices with a GET request
    return this.request({
      method: 'GET',
      service: 'wdm',
      resource: 'devices',
    })
      .then((response) => {
        const {devices} = response.body;

        // Filter devices of type 'WEB'
        const webDevices = devices.filter((item) => item.deviceType === 'WEB');

        const sortedDevices = orderBy(webDevices, [(item) => new Date(item.modificationTime)]);

        // If there are more than two devices, delete the last third
        if (sortedDevices.length > 2) {
          const totalItems = sortedDevices.length;
          const countToDelete = Math.ceil(totalItems / 3);
          const urlsToDelete = sortedDevices.slice(0, countToDelete).map((item) => item.url);

-         // Send DELETE requests for each URL
-         urlsToDelete.forEach((url) => {
-           this.request({
-             uri: url,
-             method: 'DELETE',
-           });
-         });
+         // Wait for all DELETE requests to complete
+         return Promise.all(
+           urlsToDelete.map((url) =>
+             this.request({
+               uri: url,
+               method: 'DELETE',
+             })
+           )
+         ).then(() => {
+           // Clear only once after all deletions are complete
+           this.clear();
+         });
        }
+       return Promise.resolve();
      })
      .catch((error) => {
-       this.logger.error('Failed to retrieve devices:', error);
+       this.logger.error('Failed to retrieve or delete devices:', error);
+       return Promise.reject(error);
      });
  },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
deleteDevices() {
// Fetch devices with a GET request
return this.request({
method: 'GET',
service: 'wdm',
resource: 'devices',
})
.then((response) => {
const {devices} = response.body;
// Filter devices of type 'WEB'
const webDevices = devices.filter((item) => item.deviceType === 'WEB');
const sortedDevices = orderBy(webDevices, [(item) => new Date(item.modificationTime)]);
// If there are more than two devices, delete the last third
if (sortedDevices.length > 2) {
const totalItems = sortedDevices.length;
const countToDelete = Math.ceil(totalItems / 3);
const urlsToDelete = sortedDevices.slice(0, countToDelete).map((item) => item.url);
// Send DELETE requests for each URL
urlsToDelete.forEach((url) => {
this.request({
uri: url,
method: 'DELETE',
});
});
}
})
.catch((error) => {
this.logger.error('Failed to retrieve devices:', error);
});
},
deleteDevices() {
// Fetch devices with a GET request
return this.request({
method: 'GET',
service: 'wdm',
resource: 'devices',
})
.then((response) => {
const {devices} = response.body;
// Filter devices of type 'WEB'
const webDevices = devices.filter((item) => item.deviceType === 'WEB');
const sortedDevices = orderBy(webDevices, [(item) => new Date(item.modificationTime)]);
// If there are more than two devices, delete the last third
if (sortedDevices.length > 2) {
const totalItems = sortedDevices.length;
const countToDelete = Math.ceil(totalItems / 3);
const urlsToDelete = sortedDevices.slice(0, countToDelete).map((item) => item.url);
// Wait for all DELETE requests to complete
return Promise.all(
urlsToDelete.map((url) =>
this.request({
uri: url,
method: 'DELETE',
})
)
).then(() => {
// Clear only once after all deletions are complete
this.clear();
});
}
return Promise.resolve();
})
.catch((error) => {
this.logger.error('Failed to retrieve or delete devices:', error);
return Promise.reject(error);
});
},
🧰 Tools
🪛 Biome

[error] 449-479: Illegal return statement outside of a function

(parse)


[error] 480-480: Expected a statement but instead found ','.

Expected a statement here.

(parse)


[error] 447-480: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.

(lint/complexity/noUselessLoneBlockStatements)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Outside diff range and nitpick comments (5)
packages/@webex/internal-plugin-device/src/device.js (2)

484-498: Enhance error logging for registration failures

The error handling logic is good, but would benefit from additional logging to aid debugging.

  register(deviceRegistrationOptions = {}) {
    return this._registerInternal(deviceRegistrationOptions).catch((error) => {
+     this.logger.warn('Device registration failed:', error);
      if (error?.body?.message === 'User has excessive device registrations') {
+       this.logger.info('Attempting to clean up old devices and retry registration');
        return this.deleteDevices().then(() => {
          return this._registerInternal(deviceRegistrationOptions);
        });
      }
+     this.logger.error('Registration failed with unhandled error');
      throw error;
    });
  }
🧰 Tools
🪛 Biome (1.9.4)

[error] 487-488: Decorators are not valid here.

Decorators are only valid on class declarations, class expressions, and class methods.

(parse)


[error] 489-489: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)


[error] 490-497: Illegal return statement outside of a function

(parse)


[error] 489-489: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


[error] 489-498: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.

(lint/complexity/noUselessLoneBlockStatements)


500-505: Add null checks to _getBody method

The method should handle cases where config or its properties might be undefined.

  _getBody() {
    return {
-     ...(this.config.defaults.body ? this.config.defaults.body : {}),
-     ...(this.config.body ? this.config.body : {}),
+     ...(this.config?.defaults?.body || {}),
+     ...(this.config?.body || {}),
    };
  }
🧰 Tools
🪛 Biome (1.9.4)

[error] 501-504: Illegal return statement outside of a function

(parse)


[error] 505-505: Expected a statement but instead found ','.

Expected a statement here.

(parse)


[error] 500-505: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.

(lint/complexity/noUselessLoneBlockStatements)

packages/@webex/internal-plugin-device/test/unit/spec/device.js (3)

432-452: Add more edge cases to test suite

The test suite covers the basic scenarios but misses important edge cases.

Consider adding these test cases:

it('handles empty device list', async () => {
  setup('WEB');
  const response = { body: { devices: [] } };
  // ... implementation
});

it('handles all devices of different type', async () => {
  setup('WEB');
  const response = {
    body: {
      devices: [
        {url: 'url1', modificationTime: '2023-10-01T10:00:00Z', deviceType: 'MOBILE'},
        {url: 'url2', modificationTime: '2023-10-02T10:00:00Z', deviceType: 'MOBILE'}
      ]
    }
  };
  // ... implementation
});

it('handles API errors gracefully', async () => {
  setup('WEB');
  const requestStub = sinon.stub(device, 'request');
  requestStub.withArgs(sinon.match({method: 'GET'})).rejects(new Error('API Error'));
  // ... implementation
});

484-499: Enhance error handling test coverage

The test verifies basic error handling but could be more comprehensive.

Consider adding these test cases:

it('retries only once after deleting devices', async () => {
  setup();
  const deleteDeviceSpy = sinon.stub(device, 'deleteDevices').resolves();
  const registerStub = sinon.stub(device, '_registerInternal');
  
  registerStub.onFirstCall().rejects({body: {message: 'User has excessive device registrations'}});
  registerStub.onSecondCall().rejects({body: {message: 'User has excessive device registrations'}});
  
  await assert.isRejected(device.register(), /User has excessive device registrations/);
  
  assert.calledOnce(deleteDeviceSpy);
  assert.calledTwice(registerStub);
});

it('verifies the exact error message for device deletion', async () => {
  setup();
  const deleteDeviceSpy = sinon.stub(device, 'deleteDevices').resolves();
  const registerStub = sinon.stub(device, '_registerInternal');
  
  registerStub.rejects({body: {message: 'Similar but different error'}});
  
  await assert.isRejected(device.register());
  
  assert.notCalled(deleteDeviceSpy);
});

501-516: Improve test readability and error handling

The test could be more explicit about its expectations and error handling.

-      try {
-        await device.register({deleteFlag: true});
-      } catch (error) {
-        assert.notCalled(deleteDeviceSpy);
-
-        assert.equal(registerStub.callCount, 1);
-
-        assert.match(error.message, /some error/, 'Expected error message not matched');
-      }
+      await assert.isRejected(
+        device.register({deleteFlag: true}),
+        /some error/,
+        'Should reject with the original error'
+      );
+      
+      assert.notCalled(deleteDeviceSpy, 'Should not delete devices for unrelated errors');
+      assert.calledOnce(registerStub, 'Should not retry registration');
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between a7b2468 and 3a9fe57.

📒 Files selected for processing (2)
  • packages/@webex/internal-plugin-device/src/device.js (4 hunks)
  • packages/@webex/internal-plugin-device/test/unit/spec/device.js (2 hunks)
🧰 Additional context used
🪛 Biome (1.9.4)
packages/@webex/internal-plugin-device/src/device.js

[error] 449-481: Illegal return statement outside of a function

(parse)


[error] 482-482: Expected a statement but instead found ','.

Expected a statement here.

(parse)


[error] 487-488: Decorators are not valid here.

Decorators are only valid on class declarations, class expressions, and class methods.

(parse)


[error] 489-489: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)


[error] 490-497: Illegal return statement outside of a function

(parse)


[error] 498-500: Expected a statement but instead found ',

_getBody()'.

Expected a statement here.

(parse)


[error] 501-504: Illegal return statement outside of a function

(parse)


[error] 505-505: Expected a statement but instead found ','.

Expected a statement here.

(parse)


[error] 489-489: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


[error] 447-482: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.

(lint/complexity/noUselessLoneBlockStatements)


[error] 489-498: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.

(lint/complexity/noUselessLoneBlockStatements)


[error] 500-505: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.

(lint/complexity/noUselessLoneBlockStatements)


[error] 518-518: Expected a semicolon or an implicit semicolon after a statement, but found none

An explicit or implicit semicolon is expected here...

...Which is required to end this statement

(parse)


[error] 518-518: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


[error] 518-590: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.

(lint/complexity/noUselessLoneBlockStatements)

🔇 Additional comments (1)
packages/@webex/internal-plugin-device/src/device.js (1)

Line range hint 518-590: Verify impact on existing device registrations

The refactoring of the registration flow could impact existing device registrations. Please ensure this is tested thoroughly.

✅ Verification successful

No impact on existing device registrations - changes are well tested

Based on the test file analysis, the changes in the registration flow are thoroughly tested, particularly:

  • Existing device registration handling is verified through tests that check the registered state
  • Error handling for excessive device registrations is tested
  • Device refresh and re-registration scenarios are covered
  • Registration success processing and feature updates are verified

The refactoring maintains backward compatibility and proper error handling, with no adverse effects on existing device registrations.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for all device registration related tests
rg -l "register.*device" --type test

# Search for existing device registration error handling
rg "excessive.*device.*registration" --type test

Length of output: 147


Script:

#!/bin/bash
# Search for device registration tests
rg -l "register.*device" "test|spec"

# Search for device registration implementation and its usages
ast-grep --pattern 'function $_registerInternal' 
rg "_registerInternal" -A 2

# Search for device registration tests in test files
fd -e test.js -e spec.js -x rg -l "register.*device" {}

Length of output: 1797


Script:

#!/bin/bash
# Check the test file content for device registration tests
cat packages/@webex/internal-plugin-device/test/unit/spec/device.js

# Look for any other test files related to device registration
fd -e test.js -e spec.js -x rg "describe.*device.*registration|test.*device.*registration" {}

Length of output: 28176

Comment on lines +443 to +482
/**
* Fetches the web devices and deletes the third of them which are not recent devices in use
* @returns {Promise<void, Error>}
*/
deleteDevices() {
// Fetch devices with a GET request
return this.request({
method: 'GET',
service: 'wdm',
resource: 'devices',
})
.then((response) => {
const {devices} = response.body;

const {deviceType} = this._getBody();

// Filter devices of type deviceType
const webDevices = devices.filter((item) => item.deviceType === deviceType);

const sortedDevices = orderBy(webDevices, [(item) => new Date(item.modificationTime)]);

// If there are more than two devices, delete the last third
if (sortedDevices.length > 2) {
const totalItems = sortedDevices.length;
const countToDelete = Math.ceil(totalItems / 3);
const urlsToDelete = sortedDevices.slice(0, countToDelete).map((item) => item.url);

// Send DELETE requests for each URL
urlsToDelete.forEach((url) => {
this.request({
uri: url,
method: 'DELETE',
});
});
}
})
.catch((error) => {
this.logger.error('Failed to retrieve devices:', error);
});
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Critical: Fix async operations and error handling in deleteDevices

The current implementation has several critical issues:

  1. Race condition: DELETE requests are not awaited, potentially leading to inconsistent state
  2. Error handling: Errors are swallowed in catch block
  3. Memory management: Device state not cleared after deletions

Apply this fix to properly handle async operations and error handling:

  deleteDevices() {
    // Fetch devices with a GET request
    return this.request({
      method: 'GET',
      service: 'wdm',
      resource: 'devices',
    })
      .then((response) => {
        const {devices} = response.body;
        const {deviceType} = this._getBody();

        // Filter devices of type deviceType
        const webDevices = devices.filter((item) => item.deviceType === deviceType);
        const sortedDevices = orderBy(webDevices, [(item) => new Date(item.modificationTime)]);

        // If there are more than two devices, delete the oldest third
        if (sortedDevices.length > 2) {
          const totalItems = sortedDevices.length;
          const countToDelete = Math.ceil(totalItems / 3);
          const urlsToDelete = sortedDevices.slice(0, countToDelete).map((item) => item.url);

-         // Send DELETE requests for each URL
-         urlsToDelete.forEach((url) => {
-           this.request({
-             uri: url,
-             method: 'DELETE',
-           });
-         });
+         // Wait for all DELETE requests to complete
+         return Promise.all(
+           urlsToDelete.map((url) =>
+             this.request({
+               uri: url,
+               method: 'DELETE',
+             })
+           )
+         ).then(() => {
+           // Clear device state after all deletions complete
+           this.clear();
+         });
        }
+       return Promise.resolve();
      })
      .catch((error) => {
-       this.logger.error('Failed to retrieve devices:', error);
+       this.logger.error('Failed to retrieve or delete devices:', error);
+       return Promise.reject(error);
      });
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Fetches the web devices and deletes the third of them which are not recent devices in use
* @returns {Promise<void, Error>}
*/
deleteDevices() {
// Fetch devices with a GET request
return this.request({
method: 'GET',
service: 'wdm',
resource: 'devices',
})
.then((response) => {
const {devices} = response.body;
const {deviceType} = this._getBody();
// Filter devices of type deviceType
const webDevices = devices.filter((item) => item.deviceType === deviceType);
const sortedDevices = orderBy(webDevices, [(item) => new Date(item.modificationTime)]);
// If there are more than two devices, delete the last third
if (sortedDevices.length > 2) {
const totalItems = sortedDevices.length;
const countToDelete = Math.ceil(totalItems / 3);
const urlsToDelete = sortedDevices.slice(0, countToDelete).map((item) => item.url);
// Send DELETE requests for each URL
urlsToDelete.forEach((url) => {
this.request({
uri: url,
method: 'DELETE',
});
});
}
})
.catch((error) => {
this.logger.error('Failed to retrieve devices:', error);
});
},
/**
* Fetches the web devices and deletes the third of them which are not recent devices in use
* @returns {Promise<void, Error>}
*/
deleteDevices() {
// Fetch devices with a GET request
return this.request({
method: 'GET',
service: 'wdm',
resource: 'devices',
})
.then((response) => {
const {devices} = response.body;
const {deviceType} = this._getBody();
// Filter devices of type deviceType
const webDevices = devices.filter((item) => item.deviceType === deviceType);
const sortedDevices = orderBy(webDevices, [(item) => new Date(item.modificationTime)]);
// If there are more than two devices, delete the oldest third
if (sortedDevices.length > 2) {
const totalItems = sortedDevices.length;
const countToDelete = Math.ceil(totalItems / 3);
const urlsToDelete = sortedDevices.slice(0, countToDelete).map((item) => item.url);
// Wait for all DELETE requests to complete
return Promise.all(
urlsToDelete.map((url) =>
this.request({
uri: url,
method: 'DELETE',
})
)
).then(() => {
// Clear device state after all deletions complete
this.clear();
});
}
return Promise.resolve();
})
.catch((error) => {
this.logger.error('Failed to retrieve or delete devices:', error);
return Promise.reject(error);
});
},
🧰 Tools
🪛 Biome (1.9.4)

[error] 449-481: Illegal return statement outside of a function

(parse)


[error] 482-482: Expected a statement but instead found ','.

Expected a statement here.

(parse)


[error] 447-482: This block statement doesn't serve any purpose and can be safely removed.

Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.

(lint/complexity/noUselessLoneBlockStatements)

Comment on lines +364 to +396
it('should delete correct number of devices', async () => {
setup('WEB');
const response = {
body: {
devices: [
{url: 'url3', modificationTime: '2023-10-03T10:00:00Z', deviceType: 'WEB'},
{url: 'url4', modificationTime: '2023-10-04T10:00:00Z', deviceType: 'notweb'},
{url: 'url1', modificationTime: '2023-10-01T10:00:00Z', deviceType: 'WEB'},
{url: 'url2', modificationTime: '2023-10-02T10:00:00Z', deviceType: 'WEB'},
{url: 'url5', modificationTime: '2023-10-00T10:00:00Z', deviceType: 'WEB'},
{url: 'url6', modificationTime: '2023-09-50T10:00:00Z', deviceType: 'WEB'},
{url: 'url7', modificationTime: '2023-09-30T10:00:00Z', deviceType: 'WEB'},
{url: 'url8', modificationTime: '2023-08-30T10:00:00Z', deviceType: 'WEB'},
]
}
};
const requestStub = sinon.stub(device, 'request');
requestStub.withArgs(sinon.match({method: 'GET'})).resolves(response);
requestStub.withArgs(sinon.match({method: 'DELETE'})).resolves();

await device.deleteDevices();

const expectedDeletions = ['url8', 'url7', 'url1'];

expectedDeletions.forEach(url => {
assert(requestStub.calledWith(sinon.match({uri: url, method: 'DELETE'})));
});

const notDeletedUrls = ['url2', 'url3', 'url5', 'url6', 'url4'];
notDeletedUrls.forEach(url => {
assert(requestStub.neverCalledWith(sinon.match({uri: url, method: 'DELETE'})));
});
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consolidate duplicate test cases using parameterized tests

The test cases for 'WEB' and 'WEBCLIENT' device types are nearly identical in structure and assertions. Consider using parameterized tests to reduce duplication and improve maintainability.

-    it('should delete correct number of devices', async () => {
-      setup('WEB');
-        const response = {
-          body: {
-              devices: [
-                {url: 'url3', modificationTime: '2023-10-03T10:00:00Z', deviceType: 'WEB'},
-                // ... more devices
-              ]
-          }
-        };
-        // ... test implementation
-    });
-
-    it('should delete correct number of devices when deviceType in config WEBCLIENT', async () => {
-      setup('WEBCLIENT');
-        const response = {
-          body: {
-              devices: [
-                {url: 'url3', modificationTime: '2023-10-03T10:00:00Z', deviceType: 'WEBCLIENT'},
-                // ... more devices
-              ]
-          }
-        };
-        // ... same test implementation
-    });
+    ['WEB', 'WEBCLIENT'].forEach(deviceType => {
+      it(`should delete correct number of devices for ${deviceType}`, async () => {
+        setup(deviceType);
+        const response = {
+          body: {
+            devices: [
+              {url: 'url3', modificationTime: '2023-10-03T10:00:00Z', deviceType},
+              // ... more devices with dynamic deviceType
+            ]
+          }
+        };
+        // ... test implementation
+      });
+    });

Also applies to: 398-430

Comment on lines +386 to +390
const expectedDeletions = ['url8', 'url7', 'url1'];

expectedDeletions.forEach(url => {
assert(requestStub.calledWith(sinon.match({uri: url, method: 'DELETE'})));
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance test coverage for multiple device deletions

The test data includes 8 devices but only verifies deletion of a single device. Expand the test to verify that multiple devices (oldest third) are actually deleted.

-      const expectedDeletions = ['url8', 'url7', 'url1'];
-
-      expectedDeletions.forEach(url => {
-          assert(requestStub.calledWith(sinon.match({uri: url, method: 'DELETE'})));
-      });
+      const expectedDeletions = ['url8', 'url7', 'url6'];  // Oldest third (3 devices)
+      
+      // Verify all expected deletions occurred
+      assert.equal(requestStub.callCount, expectedDeletions.length, 'Should delete exactly 3 devices');
+      
+      expectedDeletions.forEach(url => {
+          assert(requestStub.calledWith(sinon.match({uri: url, method: 'DELETE'})));
+      });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const expectedDeletions = ['url8', 'url7', 'url1'];
expectedDeletions.forEach(url => {
assert(requestStub.calledWith(sinon.match({uri: url, method: 'DELETE'})));
});
const expectedDeletions = ['url8', 'url7', 'url6']; // Oldest third (3 devices)
// Verify all expected deletions occurred
assert.equal(requestStub.callCount, expectedDeletions.length, 'Should delete exactly 3 devices');
expectedDeletions.forEach(url => {
assert(requestStub.calledWith(sinon.match({uri: url, method: 'DELETE'})));
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
validated If the pull request is validated for automation.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants