diff --git a/changelogs/fragments/2830-npm-version-update.yml b/changelogs/fragments/2830-npm-version-update.yml new file mode 100644 index 0000000000..ab05258e2c --- /dev/null +++ b/changelogs/fragments/2830-npm-version-update.yml @@ -0,0 +1,4 @@ +bugfixes: + - "npm - when the ``version`` option is used the comparison of installed vs missing will + use name@version instead of just name, allowing version specific updates + (https://github.com/ansible-collections/community.general/issues/2021)." diff --git a/plugins/modules/packaging/language/npm.py b/plugins/modules/packaging/language/npm.py index 62121297d7..5a48468970 100644 --- a/plugins/modules/packaging/language/npm.py +++ b/plugins/modules/packaging/language/npm.py @@ -181,7 +181,7 @@ class Npm(object): cmd.append('--ignore-scripts') if self.unsafe_perm: cmd.append('--unsafe-perm') - if self.name and add_package_name: + if self.name_version and add_package_name: cmd.append(self.name_version) if self.registry: cmd.append('--registry') @@ -215,14 +215,17 @@ class Npm(object): except (getattr(json, 'JSONDecodeError', ValueError)) as e: self.module.fail_json(msg="Failed to parse NPM output with error %s" % to_native(e)) if 'dependencies' in data: - for dep in data['dependencies']: - if 'missing' in data['dependencies'][dep] and data['dependencies'][dep]['missing']: + for dep, props in data['dependencies'].items(): + dep_version = dep + '@' + str(props['version']) + + if 'missing' in props and props['missing']: missing.append(dep) - elif 'invalid' in data['dependencies'][dep] and data['dependencies'][dep]['invalid']: + elif 'invalid' in props and props['invalid']: missing.append(dep) else: installed.append(dep) - if self.name and self.name not in installed: + installed.append(dep_version) + if self.name_version and self.name_version not in installed: missing.append(self.name) # Named dependency not installed else: diff --git a/tests/unit/plugins/modules/packaging/language/test_npm.py b/tests/unit/plugins/modules/packaging/language/test_npm.py index 849bfac1a6..abdacc6aef 100644 --- a/tests/unit/plugins/modules/packaging/language/test_npm.py +++ b/tests/unit/plugins/modules/packaging/language/test_npm.py @@ -47,6 +47,66 @@ class NPMModuleTestCase(ModuleTestCase): result = self.module_main(AnsibleExitJson) self.assertTrue(result['changed']) + self.module_main_command.assert_has_calls([ + call(['/testbin/npm', 'list', '--json', '--long', '--global'], check_rc=False, cwd=None), + call(['/testbin/npm', 'install', '--global', 'coffee-script'], check_rc=True, cwd=None), + ]) + + def test_present_version(self): + set_module_args({ + 'name': 'coffee-script', + 'global': 'true', + 'state': 'present', + 'version': '2.5.1' + }) + self.module_main_command.side_effect = [ + (0, '{}', ''), + (0, '{}', ''), + ] + + result = self.module_main(AnsibleExitJson) + + self.assertTrue(result['changed']) + self.module_main_command.assert_has_calls([ + call(['/testbin/npm', 'list', '--json', '--long', '--global'], check_rc=False, cwd=None), + call(['/testbin/npm', 'install', '--global', 'coffee-script@2.5.1'], check_rc=True, cwd=None), + ]) + + def test_present_version_update(self): + set_module_args({ + 'name': 'coffee-script', + 'global': 'true', + 'state': 'present', + 'version': '2.5.1' + }) + self.module_main_command.side_effect = [ + (0, '{"dependencies": {"coffee-script": {"version" : "2.5.0"}}}', ''), + (0, '{}', ''), + ] + + result = self.module_main(AnsibleExitJson) + + self.assertTrue(result['changed']) + self.module_main_command.assert_has_calls([ + call(['/testbin/npm', 'list', '--json', '--long', '--global'], check_rc=False, cwd=None), + call(['/testbin/npm', 'install', '--global', 'coffee-script@2.5.1'], check_rc=True, cwd=None), + ]) + + def test_present_version_exists(self): + set_module_args({ + 'name': 'coffee-script', + 'global': 'true', + 'state': 'present', + 'version': '2.5.1' + }) + self.module_main_command.side_effect = [ + (0, '{"dependencies": {"coffee-script": {"version" : "2.5.1"}}}', ''), + (0, '{}', ''), + ] + + result = self.module_main(AnsibleExitJson) + + self.assertFalse(result['changed']) self.module_main_command.assert_has_calls([ call(['/testbin/npm', 'list', '--json', '--long', '--global'], check_rc=False, cwd=None), ]) @@ -58,7 +118,7 @@ class NPMModuleTestCase(ModuleTestCase): 'state': 'absent' }) self.module_main_command.side_effect = [ - (0, '{"dependencies": {"coffee-script": {}}}', ''), + (0, '{"dependencies": {"coffee-script": {"version" : "2.5.1"}}}', ''), (0, '{}', ''), ] @@ -66,5 +126,46 @@ class NPMModuleTestCase(ModuleTestCase): self.assertTrue(result['changed']) self.module_main_command.assert_has_calls([ + call(['/testbin/npm', 'list', '--json', '--long', '--global'], check_rc=False, cwd=None), + call(['/testbin/npm', 'uninstall', '--global', 'coffee-script'], check_rc=True, cwd=None), + ]) + + def test_absent_version(self): + set_module_args({ + 'name': 'coffee-script', + 'global': 'true', + 'state': 'absent', + 'version': '2.5.1' + }) + self.module_main_command.side_effect = [ + (0, '{"dependencies": {"coffee-script": {"version" : "2.5.1"}}}', ''), + (0, '{}', ''), + ] + + result = self.module_main(AnsibleExitJson) + + self.assertTrue(result['changed']) + self.module_main_command.assert_has_calls([ + call(['/testbin/npm', 'list', '--json', '--long', '--global'], check_rc=False, cwd=None), + call(['/testbin/npm', 'uninstall', '--global', 'coffee-script'], check_rc=True, cwd=None), + ]) + + def test_absent_version_different(self): + set_module_args({ + 'name': 'coffee-script', + 'global': 'true', + 'state': 'absent', + 'version': '2.5.1' + }) + self.module_main_command.side_effect = [ + (0, '{"dependencies": {"coffee-script": {"version" : "2.5.0"}}}', ''), + (0, '{}', ''), + ] + + result = self.module_main(AnsibleExitJson) + + self.assertTrue(result['changed']) + self.module_main_command.assert_has_calls([ + call(['/testbin/npm', 'list', '--json', '--long', '--global'], check_rc=False, cwd=None), call(['/testbin/npm', 'uninstall', '--global', 'coffee-script'], check_rc=True, cwd=None), ])