From d600145da4833e14cee0bb2693e5be9ceaa6dd79 Mon Sep 17 00:00:00 2001
From: Benjamin Bertrand <benjamin.bertrand@esss.se>
Date: Wed, 18 Dec 2019 13:03:51 +0100
Subject: [PATCH] Coerce stack_member to int in the form

stack_member was incorrectly coerced to a string

JIRA INFRA-1648 #action In Progress
---
 app/inventory/forms.py       |  2 +-
 app/models.py                |  4 ++--
 app/utils.py                 | 10 ++++++++-
 tests/functional/test_web.py | 39 ++++++++++++++++++++++++++++++++++++
 4 files changed, 51 insertions(+), 4 deletions(-)

diff --git a/app/inventory/forms.py b/app/inventory/forms.py
index be25524..f3d7bde 100644
--- a/app/inventory/forms.py
+++ b/app/inventory/forms.py
@@ -51,7 +51,7 @@ class ItemForm(CSEntryForm):
     parent_id = SelectField("Parent", coerce=utils.coerce_to_str_or_none)
     host_id = SelectField("Host", coerce=utils.coerce_to_str_or_none)
     stack_member = NoValidateSelectField(
-        "Stack member", coerce=utils.coerce_to_str_or_none, choices=[]
+        "Stack member", coerce=utils.coerce_to_int_or_none, choices=[]
     )
     mac_addresses = StringField(
         "MAC addresses",
diff --git a/app/models.py b/app/models.py
index dd0413e..2cd7965 100644
--- a/app/models.py
+++ b/app/models.py
@@ -689,7 +689,7 @@ class Item(CreatedMixin, SearchableMixin, db.Model):
         "children": {"type": "text", "fields": {"keyword": {"type": "keyword"}}},
         "macs": {"type": "text", "fields": {"keyword": {"type": "keyword"}}},
         "host": {"type": "text", "fields": {"keyword": {"type": "keyword"}}},
-        "stack_member": {"type": "text", "fields": {"keyword": {"type": "keyword"}}},
+        "stack_member": {"type": "byte"},
         "history": {"enabled": False},
         "comments": {"type": "text"},
     }
@@ -1293,7 +1293,7 @@ class Host(CreatedMixin, SearchableMixin, db.Model):
         # This function replaces None by Inf so it is set at the end of the list
         # items are sorted by stack_member and then ics_id
         def none_to_inf(nb):
-            return float("inf") if nb is None else nb
+            return float("inf") if nb is None else int(nb)
 
         d = super().to_dict()
         d.update(
diff --git a/app/utils.py b/app/utils.py
index b053226..cf52ea1 100644
--- a/app/utils.py
+++ b/app/utils.py
@@ -192,7 +192,7 @@ def lowercase_field(value):
         return value
 
 
-# coerce function to use with SelectField that can accept a None value
+# coerce functions to use with SelectField that can accept a None value
 # wtforms always coerce to string by default
 # Values returned from the form are usually strings but if a field is disabled
 # None is returned
@@ -204,6 +204,14 @@ def coerce_to_str_or_none(value):
     return str(value)
 
 
+def coerce_to_int_or_none(value):
+    """Return None if the value is not an integer"""
+    try:
+        return int(value)
+    except ValueError:
+        return None
+
+
 def parse_to_utc(string):
     """Convert a string to a datetime object with no timezone"""
     d = dateutil.parser.parse(string)
diff --git a/tests/functional/test_web.py b/tests/functional/test_web.py
index 8827f2d..8a7fd56 100644
--- a/tests/functional/test_web.py
+++ b/tests/functional/test_web.py
@@ -709,3 +709,42 @@ def test_create_item_invalid_ics_id(logged_rw_client):
     assert response.status_code == 200
     assert b"Register new item" in response.data
     assert b"The ICS id shall be composed of 3 letters and 3 digits" in response.data
+
+
+def test_create_item_with_stack_member(
+    host_factory, device_type_factory, item_factory, logged_rw_client
+):
+    # Test for JIRA INFRA-1648
+    network_type = device_type_factory(name="NETWORK")
+    host = host_factory(device_type=network_type)
+    item1 = item_factory(ics_id="AAA001", host=host, stack_member=0)
+    ics_id = "AAA042"
+    form = {
+        "ics_id": ics_id,
+        "serial_number": "12345",
+        "host_id": host.id,
+        "stack_member": 1,
+    }
+    response = logged_rw_client.post(f"/inventory/items/create", data=form)
+    assert response.status_code == 302
+    item2 = models.Item.query.filter_by(ics_id=ics_id).first()
+    assert host.stack_members() == [item1, item2]
+
+
+def test_create_item_with_host_and_no_stack_member(
+    host_factory, device_type_factory, item_factory, logged_rw_client
+):
+    network_type = device_type_factory(name="NETWORK")
+    host = host_factory(device_type=network_type)
+    ics_id = "AAA042"
+    form = {
+        "ics_id": ics_id,
+        "serial_number": "12345",
+        "host_id": host.id,
+        "stack_member": "",
+    }
+    response = logged_rw_client.post(f"/inventory/items/create", data=form)
+    assert response.status_code == 302
+    item = models.Item.query.filter_by(ics_id=ics_id).first()
+    assert item.host == host
+    assert item.stack_member is None
-- 
GitLab