Skip to content
Merged
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
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ jobs:
test -x plugins/playbooks/board_support/roles/mender/files/update-modules/docker-compose
test -x plugins/playbooks/os_setup/roles/radar_data_bootstrap/files/retina-env-guard
test -x configuration/mender/inventory/mender-inventory-retina-stack
test -x configuration/mender/inventory/mender-inventory-retina-contact
- name: Tests
run: pytest tests/
24 changes: 24 additions & 0 deletions configuration/mender/inventory/mender-inventory-retina-contact
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/sh

# Reports the support email this device's owner entered, if they entered one.
#
# This is the contact address from the setup wizard's contact step and
# Configuration > How we reach you: whom to get in touch with about this node.
# It is not the claim address in telemetry-claim.json, which decides who owns
# the node. retina-gui keeps the two apart on purpose, and so does this.
#
# retina-gui removes the file once every contact box is empty, so a node whose
# owner gave nothing reports nothing rather than an empty value.
#
# retina-gui checks only the length of this field, not its shape, and a newline
# in it would let the owner's text add inventory attributes of its own. Anything
# that is not a single local@domain token is dropped rather than reported.

CONTACT_FILE="${RETINA_CONTACT_FILE:-/data/retina-gui/telemetry-contact.json}"

[ -f "${CONTACT_FILE}" ] || exit 0

jq -r '.email | strings
| select(test("^[^@\\s[:cntrl:]]+@[^@\\s[:cntrl:]]+$"))
| "contact_email=" + .' "${CONTACT_FILE}" 2>/dev/null
exit 0
77 changes: 77 additions & 0 deletions tests/contact-email/test_contact_email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""
Tests for the mender-inventory-retina-contact inventory script.

It runs as root on every node every 600 s and puts text the owner typed into
Mender inventory, so each test runs the real script under /bin/sh against a
contact file written the way retina-gui writes it, or deliberately not.
"""

import json
import os
import shutil
import subprocess
import tempfile
import unittest

REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
INVENTORY = os.path.join(REPO, 'configuration', 'mender', 'inventory', 'mender-inventory-retina-contact')


class TestContactInventory(unittest.TestCase):

def setUp(self):
self.dir = tempfile.mkdtemp()
self.contact_file = os.path.join(self.dir, 'telemetry-contact.json')

def tearDown(self):
shutil.rmtree(self.dir)

def write(self, text):
with open(self.contact_file, 'w') as f:
f.write(text)

def run_script(self):
env = dict(os.environ, RETINA_CONTACT_FILE=self.contact_file)
result = subprocess.run(['/bin/sh', INVENTORY], env=env, capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stderr, '')
return result.stdout

def test_an_entered_email_is_reported(self):
self.write(json.dumps({'email': 'ann@example.com', 'first_name': 'Ann'}))
self.assertEqual(self.run_script(), 'contact_email=ann@example.com\n')

def test_a_non_ascii_email_is_reported(self):
self.write(json.dumps({'email': 'josé@exämple.de'}, ensure_ascii=False))
self.assertEqual(self.run_script(), 'contact_email=josé@exämple.de\n')

def test_no_file_reports_nothing(self):
# retina-gui removes the file once every contact box is empty.
self.assertEqual(self.run_script(), '')

def test_contact_details_without_an_email_report_nothing(self):
self.write(json.dumps({'first_name': 'Ann', 'phone': '+44 20 7946 0000'}))
self.assertEqual(self.run_script(), '')

def test_a_newline_cannot_add_attributes(self):
# retina-gui checks only the length, so this can be saved.
self.write(json.dumps({'email': 'ann@example.com\nremote_access=true'}))
self.assertEqual(self.run_script(), '')

def test_values_that_are_not_one_address_are_dropped(self):
for email in ('ann smith@example.com', 'a@b@example.com', 'ann@example.com\u0007',
'example.com', '@example.com', 'ann@', 42, None):
with self.subTest(email=email):
self.write(json.dumps({'email': email}))
self.assertEqual(self.run_script(), '')

def test_an_unreadable_file_reports_nothing(self):
for text in ('{"email": "ann@exa', '["ann@example.com"]', ''):
with self.subTest(text=text):
self.write(text)
self.assertEqual(self.run_script(), '')


if __name__ == '__main__':
unittest.main()
Loading