Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions common/vcon.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,14 @@ def add_tag(self, tag_name, tag_value):
# back as spec-correct ``encoding=json`` + stringified list, per
# draft-ietf-vcon-vcon-core-02 §2.3.2 (body is always a String).
tags = self.decoded_body(tags_attachment) or []
if isinstance(tags, dict):
# A producer wrote purpose="tags" with a dict body (e.g. the SIPREC
# adapter's provenance metadata). Flatten to the tag convention's
# "name:value" list so get_tag reads it and append never raises.
# (CON-737)
tags = [f"{k}:{v}" for k, v in tags.items()]
elif not isinstance(tags, list):
tags = [tags]
tags.append(f"{tag_name}:{tag_value}")
tags_attachment["body"] = json.dumps(tags)
tags_attachment["encoding"] = "json"
Expand Down
17 changes: 15 additions & 2 deletions conserver/links/tag/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,21 @@ def run(

vcon_redis = VconRedis()
vCon = vcon_redis.get_vcon(vcon_uuid)
for tag in opts.get("tags", []):
vCon.add_tag(tag_name=tag, tag_value=tag)
tags = opts.get("tags", [])
if isinstance(tags, dict):
# dict-form options: {name: value} (CON-737)
pairs = list(tags.items())
else:
# list-form options: "name:value" strings, or bare names.
pairs = []
for tag in tags:
if isinstance(tag, str) and ":" in tag:
name, value = tag.split(":", 1)
else:
name = value = tag
pairs.append((name, value))
for name, value in pairs:
vCon.add_tag(tag_name=name, tag_value=value)
vcon_redis.store_vcon(vCon)

# Return the vcon_uuid down the chain.
Expand Down
46 changes: 46 additions & 0 deletions conserver/links/tag/test_tag.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
from unittest.mock import patch

from vcon import Vcon
Expand Down Expand Up @@ -44,3 +45,48 @@ def test_run_handles_empty_tag_list(mock_vcon_redis):
assert result == "test-uuid"
assert vcon.get_tag("iron") is None
mock_instance.store_vcon.assert_called_once_with(vcon)


@patch("links.tag.VconRedis")
def test_run_applies_name_value_string_tags(mock_vcon_redis):
# "name:value" options must set name -> value, not "tag:tag". (CON-737)
vcon = Vcon.build_new()
mock_instance = mock_vcon_redis.return_value
mock_instance.get_vcon.return_value = vcon

run("test-uuid", "tag", opts={"tags": ["source:siprec-adapter", "pipeline:interop"]})

assert vcon.get_tag("source") == "siprec-adapter"
assert vcon.get_tag("pipeline") == "interop"


@patch("links.tag.VconRedis")
def test_run_applies_dict_form_tags(mock_vcon_redis):
# dict-form options: {name: value}. (CON-737)
vcon = Vcon.build_new()
mock_instance = mock_vcon_redis.return_value
mock_instance.get_vcon.return_value = vcon

run("test-uuid", "tag", opts={"tags": {"env": "prod", "team": "interop"}})

assert vcon.get_tag("env") == "prod"
assert vcon.get_tag("team") == "interop"


@patch("links.tag.VconRedis")
def test_run_tolerates_dict_bodied_tags_attachment(mock_vcon_redis):
# A producer (e.g. the SIPREC adapter) wrote purpose="tags" with a dict
# body. add_tag must flatten it instead of raising on `.append`. (CON-737)
vcon = Vcon.build_new()
vcon.vcon_dict["attachments"].append(
{"purpose": "tags", "encoding": "json", "body": json.dumps({"source": "siprec"})}
)
mock_instance = mock_vcon_redis.return_value
mock_instance.get_vcon.return_value = vcon

result = run("test-uuid", "tag", opts={"tags": ["pipeline:interop"]})

assert result == "test-uuid"
assert vcon.get_tag("source") == "siprec" # pre-existing dict entry preserved
assert vcon.get_tag("pipeline") == "interop" # new tag appended
mock_instance.store_vcon.assert_called_once_with(vcon)