Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class Messages():
# as format argument.

WRONG_PROPERTY_FORMAT = \
"Property Name '{}' is not in the expected format i.e. SetHeader[Component][HeaderName]. "
"Property Name '{}' is not in the expected format i.e. SetHeader[Identifier][HeaderName]. "

# Element not found in flowData. This takes the element datakey
# as format argument.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@
from .setheaderelement import SetHeaderElement
import importlib

# The data keys of the JSONBundler and the JavaScriptBuilder.
JSON_BUILDER_KEY = "jsonbundler"
JAVASCRIPT_BUILDER_KEY = "javascriptbuilder"


def _data_key(element):

# Flow data keeps each element's results under its data key in lower
# case, so two keys that differ only in letters are the same key.
return str(element.datakey).lower()


class PipelineBuilder(object):
"""
Expand All @@ -40,6 +51,9 @@ class PipelineBuilder(object):
def __init__(self, settings= {}):
"""
Pipeline Builder constructor.
The SequenceElement, JSONBundler, JavaScriptBuilder and
SetHeaderElement switches below each add their element only when the
caller has not added one with the same data key.
@type settings: dictionary
@param settings : settings for the pipeline builder including:
`add_javascript_builder (Bool)` - Whether to add the JavaScriptBuilder, with the
Expand Down Expand Up @@ -188,14 +202,52 @@ def build(self):

"""

flow_elements = (
self.get_sequence_elements() +
self.flow_elements +
self.get_javascript_elements() +
self.get_setheader_elements())
flow_elements = self._add_web_elements(self.flow_elements)

return Pipeline(flow_elements, logger=self.logger, suppress_process_exceptions=self.suppress_process_exceptions, data_file_update_service=self.data_file_update_service, auto_close_elements=self.auto_close_elements)

def _add_web_elements(self, flow_elements):

"""!
The caller's elements with each web integration element the settings
ask for, unless the caller added one with the same data key. The
SequenceElement goes first and the others after every other element,
except that a JSONBundler goes immediately before a JavaScriptBuilder
the caller added, because the JavaScriptBuilder reads the
JSONBundler's results.

@type flow_elements: list
@param flow_elements: The FlowElements the caller added
@rtype: list
@return: Returns a new list of FlowElements
"""

present = set(_data_key(element) for element in flow_elements)

def absent(element):
return _data_key(element) not in present

elements = [element for element in self.get_sequence_elements()
if absent(element)]
elements.extend(flow_elements)

for element in self.get_javascript_elements():
if not absent(element):
continue
builder = next(
(index for index, added in enumerate(elements)
if _data_key(added) == JAVASCRIPT_BUILDER_KEY),
None)
if _data_key(element) == JSON_BUILDER_KEY and builder is not None:
elements.insert(builder, element)
else:
elements.append(element)

elements.extend(element for element in self.get_setheader_elements()
if absent(element))

return elements

def add_logger(self, logger):
"""
Add an instance of the logger class to the pipeline.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@
from .flowelement import FlowElement
from .elementdata_dictionary import ElementDataDictionary
from .messages import Messages
import re

# The start of the name of every property whose value is for a response
# header.
SET_HEADER_PREFIX = "SetHeader"

class SetHeaderElement(FlowElement):

Expand Down Expand Up @@ -101,8 +104,9 @@ def get_setheader_properties_pipeline(self, pipeline):
# Loop over each flowElement property
for propertykey, propertymeta in properties.items():

# Check if the property starts with SetHeader
if("setheader" in propertykey):
# Property keys are in lower case, and only a name that
# starts with SetHeader is for a response header.
if propertykey.startswith(SET_HEADER_PREFIX.lower()):
setHeader_element_list.append(propertymeta["name"])

# Add SetHeader element list in dict against flowElement.datakey as key
Expand Down Expand Up @@ -192,42 +196,27 @@ def get_property_value(self, flowData, element_key, property_key):
def get_response_header_name(self, property_key):

"""!
Determines which response header the property value will be appended to by
stripping the 'SetHeader' string and the 'Component Name' from the property name.

@param property_key: Key for SetHeaderAcceptCH property
The response header a SetHeader property's value is for. The
property name is SetHeader[Identifier][HeaderName]. The identifier
may start with either case and has no upper-case letters after its
first character, so the header name starts at the first upper-case
letter after that character.

@param property_key: A property name, such as SetHeaderBrowserAccept-CH
@rtype String
@return Response Header name
@return The response header name, such as Accept-CH

"""

actual_property_name = property_key
response_header = ""

# Check if property name starts with SetHeader.
# If Yes, Discard SetHeader from property name.
if ("SetHeader" not in property_key[0:9]):
raise Exception(Messages.PROPERTY_NOT_SET_HEADER.format(actual_property_name))
else:
# Discard SetHeader from property_key
property_key = property_key.replace("SetHeader", "", 1)
if not property_key.startswith(SET_HEADER_PREFIX):
raise Exception(
Messages.PROPERTY_NOT_SET_HEADER.format(property_key))

# Check if the first letter of Component name is in Uppercase
# If Yes, Split the propertyKey based on Uppercase letters
if(property_key[0].isupper() is False):
raise Exception(Messages.WRONG_PROPERTY_FORMAT.format(actual_property_name))
else:
# Get the Component name string to be removed from the key
parts = re.findall(r'([A-Z][^A-Z]*)', property_key)
discard_letter = parts[0]

# Check if property name contains the header name that starts with upper case
# If Yes, Remove the previously found Component Name to get the Header Name
if len(parts)<=1 :
raise Exception(Messages.WRONG_PROPERTY_FORMAT.format(actual_property_name))
name = property_key[len(SET_HEADER_PREFIX):]

else:
# Replace the first letter with the empty string.
response_header = property_key.replace(discard_letter, "")
for index in range(1, len(name)):
if "A" <= name[index] <= "Z":
return name[index:]

return response_header
# There is no identifier, or no header name after it.
raise Exception(Messages.WRONG_PROPERTY_FORMAT.format(property_key))
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# *********************************************************************
# This Original Work is copyright of 51 Degrees Mobile Experts Limited.
# Copyright 2026 51 Degrees Mobile Experts Limited, Davidson House,
# Forbury Square, Reading, Berkshire, United Kingdom RG1 3EU.
#
# This Original Work is licensed under the European Union Public Licence
# (EUPL) v.1.2 and is subject to its terms as set out below.
#
# If a copy of the EUPL was not distributed with this file, You can obtain
# one at https://opensource.org/licenses/EUPL-1.2.
#
# The 'Compatible Licences' set out in the Appendix to the EUPL (as may be
# amended by the European Commission) shall be deemed incompatible for
# the purposes of the Work and the provisions of the compatibility
# clause in Article 5 of the EUPL shall not apply.
#
# If using the Work as, or as part of, a network application, by
# including the attribution notice(s) required under Article 5 of the EUPL
# in the end user terms of the application under an appropriate heading,
# such notice(s) shall fulfill the requirements of that article.
# *********************************************************************

"""The basic list filter accepts the keys it was given, in any letters, and
nothing else. Its list attribute is read by other code, so it keeps holding
the keys in lower case, and the filter matches what the list holds when the
list is set after the filter is made."""

import unittest

from fiftyone_pipeline_core.basiclist_evidence_keyfilter import \
BasicListEvidenceKeyFilter


class SetsItsOwnList(BasicListEvidenceKeyFilter):
"""A filter that sets its list without calling the base constructor."""

def __init__(self):
self.list = ["header.user-agent"]


class BasicListEvidenceKeyFilterTests(unittest.TestCase):

def test_a_listed_key_is_accepted_in_any_letters(self):
key_filter = BasicListEvidenceKeyFilter(
["Header.User-Agent", "query.sequence"])
for key in ("header.user-agent", "HEADER.USER-AGENT",
"Header.User-Agent", "query.sequence", "Query.Sequence"):
self.assertTrue(key_filter.filter_evidence_key(key), key)

def test_a_key_that_is_not_listed_is_refused(self):
key_filter = BasicListEvidenceKeyFilter(["header.user-agent"])
for key in ("header.user", "header.user-agent2", "user-agent",
"cookie.user-agent", ""):
self.assertFalse(key_filter.filter_evidence_key(key), key)

def test_an_empty_list_accepts_nothing(self):
key_filter = BasicListEvidenceKeyFilter([])
self.assertFalse(key_filter.filter_evidence_key("header.user-agent"))
self.assertFalse(key_filter.filter_evidence_key(""))

def test_filtering_keeps_listed_keys_as_they_were_given(self):
key_filter = BasicListEvidenceKeyFilter(["header.user-agent"])
self.assertEqual(
{"Header.User-Agent": "a browser"},
key_filter.filter_evidence({
"Header.User-Agent": "a browser",
"header.accept": "text/html"}))

def test_the_list_holds_the_keys_in_lower_case_in_order(self):
key_filter = BasicListEvidenceKeyFilter(
["Query.Sequence", "header.user-agent", "Header.Host"])
self.assertEqual(
["query.sequence", "header.user-agent", "header.host"],
key_filter.list)
self.assertIsInstance(key_filter.list, list)

def test_a_list_set_after_the_filter_is_made_is_matched(self):
key_filter = BasicListEvidenceKeyFilter(["header.user-agent"])
key_filter.list = ["header.accept"]
self.assertTrue(key_filter.filter_evidence_key("Header.Accept"))
self.assertFalse(key_filter.filter_evidence_key("header.user-agent"))

def test_a_subclass_that_sets_its_own_list_is_matched(self):
key_filter = SetsItsOwnList()
self.assertTrue(key_filter.filter_evidence_key("Header.User-Agent"))
self.assertFalse(key_filter.filter_evidence_key("header.accept"))
self.assertEqual(["header.user-agent"], key_filter.list)


if __name__ == "__main__":
unittest.main()
50 changes: 40 additions & 10 deletions pipeline/fiftyone_pipeline_core/tests/test_setheader.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,30 +83,60 @@ def test_set_response_header(self, data, expected_value):
response = set_response_header(flowdata, response)
self.assertEqual(expected_value, Constants.ACCEPTCH_HEADER in response.headers)

# Test get response header function for valid formats.
# Test get response header function for valid formats. The name is
# SetHeader[Identifier][HeaderName], and the identifier has no upper-case
# characters after its first character, which may be either case.
@parameterized.expand([
("SetHeaderBrowserAccept-CH", "Accept-CH"),
("SetHeaderHardwareAccept-CH", "Accept-CH"),
("SetHeaderPlatformAccept-CH", "Accept-CH"),
("SetHeaderBrowserCritical-CH", "Critical-CH"),
("SetHeaderUnknownAccept-CH", "Accept-CH")
("SetHeaderUnknownAccept-CH", "Accept-CH"),
("SetHeaderbrowserAccept-CH", "Accept-CH"),
# The header name holds the identifier, which must be kept.
("SetHeaderAcceptAccept-CH", "Accept-CH")
])
def test_get_response_header_name(self, property_key, expected_value):
setHeaderElement = SetHeaderElement()
actual_value = setHeaderElement.get_response_header_name(property_key)
self.assertEqual(expected_value, actual_value)

# Test get response header function raises exception
# Test get response header function raises exception
# for invalid property formats.
@parameterized.expand([
("TestBrowserAccept-CH", Messages.PROPERTY_NOT_SET_HEADER),
("SetHeaderbrowserAccept-CH", Messages.WRONG_PROPERTY_FORMAT),
("SetHeaderBrowseraccept-ch", Messages.WRONG_PROPERTY_FORMAT)
("SetHeaderBrowseraccept-ch", Messages.WRONG_PROPERTY_FORMAT),
("SetHeaderbrowser", Messages.WRONG_PROPERTY_FORMAT),
("SetHeaderBrowser", Messages.WRONG_PROPERTY_FORMAT),
("SetHeader", Messages.WRONG_PROPERTY_FORMAT)
])
def test_get_response_header_name(self, property_value, expected_message):
def test_get_response_header_name_error(
self, property_value, expected_message):

setHeaderElement = SetHeaderElement()
try:
with self.assertRaises(Exception) as caught:
setHeaderElement.get_response_header_name(property_value)
except Exception as e:
message = str(e)

self.assertEqual(expected_message.format(property_value), message)
self.assertEqual(
expected_message.format(property_value), str(caught.exception))

# Only a property whose name starts with SetHeader is for a response
# header, so a name that holds SetHeader later on is left alone.
def test_get_setheader_properties_pipeline_needs_the_prefix(self):

class Element(object):
datakey = "device"

def get_properties(self):
return {
"setheaderbrowseraccept-ch": {
"name": "SetHeaderBrowserAccept-CH"},
"notsetheaderbrowseraccept-ch": {
"name": "NotSetHeaderBrowserAccept-CH"}}

class Pipeline(object):
flow_elements = [Element()]

self.assertEqual(
{"device": ["SetHeaderBrowserAccept-CH"]},
SetHeaderElement().get_setheader_properties_pipeline(Pipeline()))
Loading
Loading