Tower: upload cetmix_tower_yaml 16.0.2.0.3 (via marketplace)
This commit is contained in:
375
addons/cetmix_tower_yaml/tests/test_yaml_export_wizard.py
Normal file
375
addons/cetmix_tower_yaml/tests/test_yaml_export_wizard.py
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
# Copyright (C) 2024 Cetmix OÜ
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
import base64
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from odoo.exceptions import AccessError, ValidationError
|
||||||
|
|
||||||
|
from odoo.addons.base.tests.common import BaseCommon
|
||||||
|
|
||||||
|
|
||||||
|
class TestYamlExportWizard(BaseCommon):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls, *args, **kwargs):
|
||||||
|
super().setUpClass(*args, **kwargs)
|
||||||
|
|
||||||
|
# Used to ensure that the file header
|
||||||
|
# is present in the YAML code
|
||||||
|
cls.file_header = """
|
||||||
|
# This file is generated with Cetmix Tower.
|
||||||
|
# Details and documentation: https://cetmix.com/tower
|
||||||
|
"""
|
||||||
|
# Create a command
|
||||||
|
cls.TowerCommand = cls.env["cx.tower.command"]
|
||||||
|
cls.command_test_wizard = cls.TowerCommand.create(
|
||||||
|
{
|
||||||
|
"reference": "test_command_from_yaml",
|
||||||
|
"name": "Test Command From Yaml",
|
||||||
|
"code": "echo 'Test Command From Yaml'",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
cls.command_test_wizard_2 = cls.TowerCommand.create(
|
||||||
|
{
|
||||||
|
"reference": "test_command_from_yaml_2",
|
||||||
|
"name": "Test Command From Yaml 2",
|
||||||
|
"code": "echo 'Test Command From Yaml 2'",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a flight plan
|
||||||
|
cls.FlightPlan = cls.env["cx.tower.plan"]
|
||||||
|
cls.flight_plan_test_wizard = cls.FlightPlan.create(
|
||||||
|
{
|
||||||
|
"name": "Test Flight Plan From Yaml",
|
||||||
|
"line_ids": [
|
||||||
|
(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
"command_id": cls.command_test_wizard.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a server template
|
||||||
|
cls.ServerTemplate = cls.env["cx.tower.server.template"]
|
||||||
|
cls.server_template_test_wizard = cls.ServerTemplate.create(
|
||||||
|
{
|
||||||
|
"name": "Test Server Template From Yaml",
|
||||||
|
"flight_plan_id": cls.flight_plan_test_wizard.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a wizard and trigger onchange
|
||||||
|
cls.YamlExportWizard = cls.env["cx.tower.yaml.export.wiz"]
|
||||||
|
cls.test_wizard = cls.YamlExportWizard.with_context(
|
||||||
|
active_model="cx.tower.server.template",
|
||||||
|
active_ids=[cls.server_template_test_wizard.id],
|
||||||
|
).create({})
|
||||||
|
cls.test_wizard.onchange_explode_child_records()
|
||||||
|
|
||||||
|
def test_user_without_export_group_cannot_export(self):
|
||||||
|
"""Test if user without export group cannot export"""
|
||||||
|
|
||||||
|
# Tower manager user without export group
|
||||||
|
self.user_yaml_export = self.env["res.users"].create(
|
||||||
|
{
|
||||||
|
"name": "No Yaml Export User",
|
||||||
|
"login": "no_yaml_export_user",
|
||||||
|
"groups_id": [
|
||||||
|
(4, self.env.ref("cetmix_tower_server.group_manager").id)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
with self.assertRaises(AccessError):
|
||||||
|
self.test_wizard.with_user(self.user_yaml_export).read([])
|
||||||
|
|
||||||
|
def test_yaml_export_wizard_yaml_generation(self):
|
||||||
|
"""Test code generation of YAML export wizard."""
|
||||||
|
|
||||||
|
wizard_yaml = """
|
||||||
|
# This file is generated with Cetmix Tower.
|
||||||
|
# Details and documentation: https://cetmix.com/tower
|
||||||
|
cetmix_tower_yaml_version: 1
|
||||||
|
records:
|
||||||
|
- cetmix_tower_model: command
|
||||||
|
access_level: manager
|
||||||
|
reference: test_command_from_yaml
|
||||||
|
name: Test Command From Yaml
|
||||||
|
action: ssh_command
|
||||||
|
allow_parallel_run: false
|
||||||
|
note: false
|
||||||
|
path: false
|
||||||
|
code: echo 'Test Command From Yaml'
|
||||||
|
server_status: false
|
||||||
|
no_split_for_sudo: false
|
||||||
|
if_file_exists: skip
|
||||||
|
disconnect_file: false
|
||||||
|
- cetmix_tower_model: command
|
||||||
|
access_level: manager
|
||||||
|
reference: test_command_from_yaml_2
|
||||||
|
name: Test Command From Yaml 2
|
||||||
|
action: ssh_command
|
||||||
|
allow_parallel_run: false
|
||||||
|
note: false
|
||||||
|
path: false
|
||||||
|
code: echo 'Test Command From Yaml 2'
|
||||||
|
server_status: false
|
||||||
|
no_split_for_sudo: false
|
||||||
|
if_file_exists: skip
|
||||||
|
disconnect_file: false
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -- 1 --
|
||||||
|
# Test with two commands
|
||||||
|
context = {
|
||||||
|
"default_explode_child_records": True,
|
||||||
|
"default_remove_empty_values": True,
|
||||||
|
"active_model": "cx.tower.command",
|
||||||
|
"active_ids": [self.command_test_wizard.id, self.command_test_wizard_2.id],
|
||||||
|
}
|
||||||
|
wizard = self.YamlExportWizard.with_context(context).create({}) # pylint: disable=context-overridden # new need a new clean context
|
||||||
|
wizard.onchange_explode_child_records()
|
||||||
|
self.assertEqual(wizard.yaml_code, wizard_yaml)
|
||||||
|
|
||||||
|
def test_yaml_export_wizard(self):
|
||||||
|
"""Test the YAML export wizard."""
|
||||||
|
|
||||||
|
# -- 1 --
|
||||||
|
# Test wizard action
|
||||||
|
result = self.test_wizard.action_generate_yaml_file()
|
||||||
|
self.assertEqual(
|
||||||
|
result["type"], "ir.actions.act_window", "Action should be a window"
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
result["res_model"],
|
||||||
|
"cx.tower.yaml.export.wiz.download",
|
||||||
|
"Result model should be the download wizard",
|
||||||
|
)
|
||||||
|
self.assertTrue(result["res_id"], "Wizard should have been created")
|
||||||
|
|
||||||
|
# -- 2 --
|
||||||
|
# Ensure download wizard file name is generated
|
||||||
|
# based on the record reference
|
||||||
|
download_wizard = self.env["cx.tower.yaml.export.wiz.download"].browse(
|
||||||
|
result["res_id"]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
download_wizard.yaml_file_name,
|
||||||
|
f"server_template_{self.server_template_test_wizard.reference}.yaml",
|
||||||
|
"YAML file name should be generated based on record reference",
|
||||||
|
)
|
||||||
|
|
||||||
|
# -- 3 --
|
||||||
|
# Decode YAML file and check if it's valid
|
||||||
|
yaml_file_content = base64.decodebytes(download_wizard.yaml_file).decode(
|
||||||
|
"utf-8"
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
yaml_file_content,
|
||||||
|
self.test_wizard.yaml_code,
|
||||||
|
"YAML file content should be the same as the original YAML code",
|
||||||
|
)
|
||||||
|
|
||||||
|
# -- 4 --
|
||||||
|
# Test if empty YAML code is handled correctly
|
||||||
|
self.test_wizard.yaml_code = ""
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
self.test_wizard.action_generate_yaml_file()
|
||||||
|
|
||||||
|
def test_reference_object_uniqueness(self):
|
||||||
|
"""
|
||||||
|
Ensure each reference is exported as a full object only once
|
||||||
|
(other times only as ref).
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Prepare YAML export for flight_plan with two same commands
|
||||||
|
self.flight_plan_test_wizard.line_ids = [
|
||||||
|
(0, 0, {"command_id": self.command_test_wizard.id}),
|
||||||
|
(0, 0, {"command_id": self.command_test_wizard.id}),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Prepare YAML code
|
||||||
|
self.test_wizard.onchange_explode_child_records()
|
||||||
|
yaml_data = yaml.safe_load(self.test_wizard.yaml_code)
|
||||||
|
|
||||||
|
# reference counters
|
||||||
|
ref_full = set()
|
||||||
|
ref_refs = set()
|
||||||
|
|
||||||
|
# Recursively walk through the YAML data and count references
|
||||||
|
def walk(obj):
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
ref = obj.get("reference")
|
||||||
|
# dict only with "reference" = ref, otherwise — full object
|
||||||
|
if ref:
|
||||||
|
if list(obj.keys()) == ["reference"]:
|
||||||
|
ref_refs.add(ref)
|
||||||
|
else:
|
||||||
|
ref_full.add(ref)
|
||||||
|
for v in obj.values():
|
||||||
|
walk(v)
|
||||||
|
elif isinstance(obj, list):
|
||||||
|
for v in obj:
|
||||||
|
walk(v)
|
||||||
|
|
||||||
|
# Walk through the YAML data
|
||||||
|
walk(yaml_data["records"])
|
||||||
|
|
||||||
|
# Each reference as a full object — only once
|
||||||
|
for ref in ref_full:
|
||||||
|
self.assertEqual(
|
||||||
|
list(ref_full).count(ref),
|
||||||
|
1,
|
||||||
|
f"Reference '{ref}' appears as a full object more than once",
|
||||||
|
)
|
||||||
|
# Check that no full objects appear more than once
|
||||||
|
self.assertEqual(
|
||||||
|
len(ref_full),
|
||||||
|
len(set(ref_full)),
|
||||||
|
"Some full objects appear more than once",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that for each ref there is no only reference, but no full object
|
||||||
|
for ref in ref_refs:
|
||||||
|
self.assertIn(
|
||||||
|
ref,
|
||||||
|
ref_full,
|
||||||
|
f"Reference '{ref}' is used only as a reference, "
|
||||||
|
"but no full object present",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_export_required_model_name_in_yaml(self):
|
||||||
|
"""
|
||||||
|
Test that the model name is required in the YAML file for each record
|
||||||
|
"""
|
||||||
|
# create a command to run flight plan
|
||||||
|
command_run_flight_plan = self.TowerCommand.create(
|
||||||
|
{
|
||||||
|
"name": "Run Flight Plan",
|
||||||
|
"action": "plan",
|
||||||
|
"flight_plan_id": self.flight_plan_test_wizard.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# export 2 commands: command_run_flight_plan and command_test_wizard
|
||||||
|
wizard = self.YamlExportWizard.with_context(
|
||||||
|
active_model="cx.tower.command",
|
||||||
|
active_ids=[command_run_flight_plan.id, self.command_test_wizard.id],
|
||||||
|
).create({})
|
||||||
|
|
||||||
|
wizard.onchange_explode_child_records()
|
||||||
|
|
||||||
|
yaml_data = yaml.safe_load(wizard.yaml_code)
|
||||||
|
|
||||||
|
# check that the model name is present in the YAML file for each record
|
||||||
|
for record in yaml_data["records"]:
|
||||||
|
self.assertIn("cetmix_tower_model", record)
|
||||||
|
|
||||||
|
def test_default_yaml_file_name_is_used(self):
|
||||||
|
"""
|
||||||
|
Wizard should pre-fill `yaml_file_name` with the auto-generated
|
||||||
|
value that ends with '.yaml' and contains the model prefix.
|
||||||
|
"""
|
||||||
|
wiz = self.YamlExportWizard.with_context(
|
||||||
|
active_model="cx.tower.command",
|
||||||
|
active_ids=[self.command_test_wizard.id],
|
||||||
|
).create({})
|
||||||
|
|
||||||
|
default_name = wiz.yaml_file_name
|
||||||
|
|
||||||
|
self.assertFalse(
|
||||||
|
default_name.endswith(".yaml"),
|
||||||
|
"Default file name must NO have .yaml suffix",
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
"command_",
|
||||||
|
default_name,
|
||||||
|
"Default file name should include model prefix",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_yaml_file_name_is_auto_fixed(self):
|
||||||
|
"""
|
||||||
|
When the user assigns an invalid name, wizard should auto-sanitise
|
||||||
|
it to a safe *basename* (lowercase, underscores, no extension).
|
||||||
|
"""
|
||||||
|
wiz = self.YamlExportWizard.with_context(
|
||||||
|
active_model="cx.tower.command",
|
||||||
|
active_ids=[self.command_test_wizard.id],
|
||||||
|
).create({})
|
||||||
|
|
||||||
|
# user enters a 'dirty' name with spaces, capitals, symbols
|
||||||
|
wiz.write({"yaml_file_name": "My File!@# .YAML"})
|
||||||
|
|
||||||
|
# write() override strips to a basename WITHOUT '.yaml'
|
||||||
|
self.assertEqual(
|
||||||
|
wiz.yaml_file_name,
|
||||||
|
"my_file",
|
||||||
|
"Wizard field must hold only the cleaned basename, without extension",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_action_generate_appends_extension(self):
|
||||||
|
"""
|
||||||
|
When generating the download record, the system must append
|
||||||
|
the `.yaml` extension to the sanitized basename.
|
||||||
|
"""
|
||||||
|
wiz = self.YamlExportWizard.with_context(
|
||||||
|
active_model="cx.tower.command",
|
||||||
|
active_ids=[self.command_test_wizard.id],
|
||||||
|
).create({})
|
||||||
|
wiz.onchange_explode_child_records()
|
||||||
|
act = wiz.action_generate_yaml_file()
|
||||||
|
download = self.env["cx.tower.yaml.export.wiz.download"].browse(act["res_id"])
|
||||||
|
self.assertTrue(download.yaml_file_name.endswith(".yaml"))
|
||||||
|
|
||||||
|
def test_custom_requires_text(self):
|
||||||
|
"""Creating a template with license 'custom' but no text must fail"""
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
self.env["cx.tower.yaml.manifest.tmpl"].create(
|
||||||
|
{
|
||||||
|
"name": "Bad Manifest",
|
||||||
|
"license": "custom",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
tmpl_ok = self.env["cx.tower.yaml.manifest.tmpl"].create(
|
||||||
|
{
|
||||||
|
"name": "Good Manifest",
|
||||||
|
"license": "custom",
|
||||||
|
"license_text": "Custom license terms",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertEqual(tmpl_ok.license, "custom")
|
||||||
|
self.assertEqual(tmpl_ok.license_text, "Custom license terms")
|
||||||
|
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
self.env["cx.tower.yaml.manifest.tmpl"].create(
|
||||||
|
{
|
||||||
|
"name": "Bad Manifest 2",
|
||||||
|
"license": "custom",
|
||||||
|
"license_text": " ",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_wizard_resets_price_on_license_change(self):
|
||||||
|
"""Wizard must reset price/currency when license changes away from 'custom'"""
|
||||||
|
wiz = self.YamlExportWizard.new(
|
||||||
|
{
|
||||||
|
"manifest_license": "custom",
|
||||||
|
"manifest_price": 42.0,
|
||||||
|
"manifest_currency": "EUR",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
wiz.manifest_license = "agpl-3"
|
||||||
|
wiz._onchange_manifest_license()
|
||||||
|
self.assertEqual(wiz.manifest_price, 0.0)
|
||||||
|
self.assertFalse(wiz.manifest_currency)
|
||||||
|
|
||||||
|
wiz.manifest_price = 7.5
|
||||||
|
wiz.manifest_currency = "USD"
|
||||||
|
wiz.manifest_license = "custom"
|
||||||
|
wiz._onchange_manifest_license()
|
||||||
|
self.assertEqual(wiz.manifest_price, 7.5)
|
||||||
|
self.assertEqual(wiz.manifest_currency, "USD")
|
||||||
Reference in New Issue
Block a user