Repository: MaZderMind/cwa-qr (language: python)

Issue:
Change API to Python-Style
I do too much java… therefore the API does not use `snake_case` for methods and variables.
This should be changed before a tagged & published release.

Also the Module-Name should be changed to "cwa_qr" to match the Github-Name.


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index 546ca3a..cd52cb4 100644
--- a/README.md
+++ b/README.md
@@ -15,29 +15,30 @@ Usage
 The Library is not yet packaged as pip and so cwa-directory should be copied over to your project manually.
 
 Use as follows:
+
 ```py
 #!/usr/bin/env python3
 
 import io
 from datetime import datetime, time, timedelta, timezone
 
-import cwa
+import cwa_qr
 import qrcode.image.svg
 
 # Construct Event-Descriptor
-eventDescription = cwa.CwaEventDescription()
-eventDescription.locationDescription = 'Zuhause'
-eventDescription.locationAddress = 'Gau-Odernheim'
-eventDescription.startDateTime = datetime.now(timezone.utc)
-eventDescription.endDateTime = datetime.now(timezone.utc) + timedelta(days=2)
-eventDescription.locationType = cwa.lowlevel.LOCATION_TYPE_PERMANENT_WORKPLACE
-eventDescription.defaultCheckInLengthInMinutes = 4 * 60
+event_description = cwa_qr.CwaEventDescription()
+event_description.location_description = 'Zuhause'
+event_description.location_address = 'Gau-Odernheim'
+event_description.start_date_time = datetime.now(timezone.utc)
+event_description.end_date_time = datetime.now(timezone.utc) + timedelta(days=2)
+event_description.location_type = cwa_qr.lowlevel.LOCATION_TYPE_PERMANENT_WORKPLACE
+event_description.default_check_in_length_in_minutes = 4 * 60
 
 # Renew QR-Code every night at 4:00
-eventDescription.seed = cwa.rolloverDate(datetime.now(), time(4, 0))
+event_description.seed = cwa_qr.rollover_date(datetime.now(), time(4, 0))
 
 # Generate QR-Code
-qr = cwa.generateQrCode(eventDescription)
+qr = cwa_qr.generate_qr_code(event_description)
 
 # Render QR-Code to PNG-File
 img = qr.make_image(fill_color="black", back_color="white")
@@ -49,7 +50,6 @@ img_bytes = io.BytesIO()
 img.save(img_bytes)
 print(len(img_bytes.getvalue()), " bytes of png")
 
-
 # Render QR-Code to SVG-File
 svg = qr.make_image(image_factory=qrcode.image.svg.SvgPathFillImage)
 svg.save('example.svg')
@@ -62,11 +62,11 @@ print(len(svg_bytes.getvalue()), " bytes of svg")
 
 CwaEventDescription
 -------------------
-- `locationDescription`: Description of the Location, Optional, String, max 100 Characters
-- `locationAddress`: Address of the Location, Optional, String, max 100 Characters
-- `startDateTime`: Start of the Event, Optional, datetime in UTC
-- `endDateTime`: End of the Event, Optional, datetime in UTC
-- `locationType`: Type of the Location, Optional, one of
+- `location_description`: Description of the Location, Optional, String, max 100 Characters
+- `location_address`: Address of the Location, Optional, String, max 100 Characters
+- `start_date_time`: Start of the Event, Optional, datetime in UTC
+- `end_date_time`: End of the Event, Optional, datetime in UTC
+- `location_type`: Type of the Location, Optional, one of
 	- `cwa.lowlevel.LOCATION_TYPE_UNSPECIFIED` = 0
 	- `cwa.lowlevel.LOCATION_TYPE_PERMANENT_OTHER` = 1
 	- `cwa.lowlevel.LOCATION_TYPE_TEMPORARY_OTHER` = 2
@@ -80,7 +80,7 @@ CwaEventDescription
 	- `cwa.lowlevel.LOCATION_TYPE_TEMPORARY_CLUB_ACTIVITY `= 10
 	- `cwa.lowlevel.LOCATION_TYPE_TEMPORARY_PRIVATE_EVENT `= 11
 	- `cwa.lowlevel.LOCATION_TYPE_TEMPORARY_WORSHIP_SERVICE `= 12
-- `defaultCheckInLengthInMinutes`: Default Check-out time in minutes, Optional
+- `default_check_in_length_in_minutes`: Default Check-out time in minutes, Optional
 - `seed`: Seed to rotate the QR-Code, Optional, `[str, bytes, int, float, date, datetime]` or `None` (Default).
   **Use with caution & read below!** If unsure, leave blank.
 
@@ -118,17 +118,17 @@ The Secret *must stay constant* over time, or the resulting QR-Codes will not co
 import io
 from datetime import datetime, time
 
-import cwa
+import cwa_qr
 
 # Construct Event-Descriptor
-eventDescription = cwa.CwaEventDescription()
+event_description = cwa_qr.CwaEventDescription()
 # …
-seedDate = cwa.rolloverDate(datetime.now(), time(4, 0))
-eventDescription.seed = "Some Secret" + str(seedDate)
+seed_date = cwa_qr.rollover_date(datetime.now(), time(4, 0))
+event_description.seed = "Some Secret" + str(seed_date)
 ```
 
 this will keep the date-based seed until 4:00 am on the next day and only then roll over to the next day.
-See [test_rollover.py](cwa/test_rollover.py) for an in-depth look at the rollover code.
+See [test_rollover.py](cwa_qr/test_rollover.py) for an in-depth look at the rollover code.
 
 Python 2/3
 ----------
diff --git a/cwa/__init__.py b/cwa/__init__.py
deleted file mode 100644
index 5094a3b..0000000
--- a/cwa/__init__.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from .cwa import CwaEventDescription, generateQrCode, generateUrl, generatePayload, lowlevel
-from .rollover import rolloverDate
-
-__all__ = [
-    "CwaEventDescription",
-    "generateQrCode",
-    "generateUrl",
-    "generatePayload",
-    "rolloverDate",
-    "lowlevel",
-]
diff --git a/cwa/cwa.py b/cwa/cwa.py
deleted file mode 100644
index fc2eb88..0000000
--- a/cwa/cwa.py
+++ /dev/null
@@ -1,100 +0,0 @@
-import base64
-from typing import Optional, Union
-
-import qrcode
-from datetime import datetime, date
-
-from . import cwa_pb2 as lowlevel
-from . import seed
-
-PUBLIC_KEY_STR = 'gwLMzE153tQwAOf2MZoUXXfzWTdlSpfS99iZffmcmxOG9njSK4RTimFOFwDh6t0Tyw8XR01ugDYjtuKwj' \
-                 'juK49Oh83FWct6XpefPi9Skjxvvz53i9gaMmUEc96pbtoaA'
-
-PUBLIC_KEY = base64.standard_b64decode(PUBLIC_KEY_STR.encode('ascii'))
-
-
-class CwaEventDescription(object):
-    def __init__(self):
-        """Description of the Location, Required, String, max 100 Characters"""
-        self.locationDescription: Optional[str] = None
-
-        """Address of the Location, Required, String, max 100 Characters"""
-        self.locationAddress: Optional[str] = None
-
-        """Start of the Event, Optional, datetime in UTC"""
-        self.startDateTime: Optional[datetime] = None
-
-        """End of the Event, Optional, datetime in UTC"""
-        self.endDateTime: Optional[datetime] = None
-
-        """Type of the Location, Optional
-
-        one of
-        - cwa.lowlevel.LOCATION_TYPE_UNSPECIFIED = 0
-        - cwa.lowlevel.LOCATION_TYPE_PERMANENT_OTHER = 1
-        - cwa.lowlevel.LOCATION_TYPE_TEMPORARY_OTHER = 2
-        - cwa.lowlevel.LOCATION_TYPE_PERMANENT_RETAIL = 3
-        - cwa.lowlevel.LOCATION_TYPE_PERMANENT_FOOD_SERVICE = 4
-        - cwa.lowlevel.LOCATION_TYPE_PERMANENT_CRAFT = 5
-        - cwa.lowlevel.LOCATION_TYPE_PERMANENT_WORKPLACE = 6
-        - cwa.lowlevel.LOCATION_TYPE_PERMANENT_EDUCATIONAL_INSTITUTION = 7
-        - cwa.lowlevel.LOCATION_TYPE_PERMANENT_PUBLIC_BUILDING = 8
-        - cwa.lowlevel.LOCATION_TYPE_TEMPORARY_CULTURAL_EVENT = 9
-        - cwa.lowlevel.LOCATION_TYPE_TEMPORARY_CLUB_ACTIVITY = 10
-        - cwa.lowlevel.LOCATION_TYPE_TEMPORARY_PRIVATE_EVENT = 11
-        - cwa.lowlevel.LOCATION_TYPE_TEMPORARY_WORSHIP_SERVICE = 12
-        """
-        self.locationType: Optional[int] = None
-
-        """Default Checkout-Time im Minutes, Optional"""
-        self.defaultCheckInLengthInMinutes: Optional[int] = None
-
-        """Seed to rotate the QR-Code, Optional, [str, bytes, int, float, date, datetime] or None (Default)
-
-        Use with caution & read the Section about *Rotating QR-Codes* in the README first! If unsure, leave blank.
-        """
-        self.seed: Union[str, bytes, int, float, date, datetime, None] = None
-
-
-def generatePayload(eventDescription: CwaEventDescription) -> lowlevel.QRCodePayload:
-    payload = lowlevel.QRCodePayload()
-    payload.version = 1
-
-    payload.locationData.version = 1
-    payload.locationData.description = eventDescription.locationDescription
-    payload.locationData.address = eventDescription.locationAddress
-    payload.locationData.startTimestamp = int(eventDescription.startDateTime.timestamp()) if \
-        eventDescription.startDateTime else 0
-    payload.locationData.endTimestamp = int(eventDescription.endDateTime.timestamp()) if \
-        eventDescription.endDateTime else 0
-
-    payload.crowdNotifierData.version = 1
-    payload.crowdNotifierData.publicKey = PUBLIC_KEY
-    payload.crowdNotifierData.cryptographicSeed = seed.constructSeed(eventDescription.seed)
-
-    cwaLocationData = lowlevel.CWALocationData()
-    cwaLocationData.version = 1
-    cwaLocationData.type = eventDescription.locationType if \
-        eventDescription.locationType is not None else lowlevel.LOCATION_TYPE_UNSPECIFIED
-    cwaLocationData.defaultCheckInLengthInMinutes = eventDescription.defaultCheckInLengthInMinutes if \
-        eventDescription.defaultCheckInLengthInMinutes is not None else 0
-
-    payload.countryData = cwaLocationData.SerializeToString()
-    return payload
-
-
-def generateUrl(eventDescription: CwaEventDescription) -> str:
-    payload = generatePayload(eventDescription)
-    serialized = payload.SerializeToString()
-    encoded = base64.urlsafe_b64encode(serialized)
-    url = 'https://e.coronawarn.app?v=1#' + encoded.decode('ascii')
-
-    return url
-
-
-def generateQrCode(eventDescription: CwaEventDescription) -> qrcode.QRCode:
-    qr = qrcode.QRCode()
-    qr.add_data(generateUrl(eventDescription))
-    qr.make(fit=True)
-
-    return qr
diff --git a/cwa_qr/__init__.py b/cwa_qr/__init__.py
new file mode 100644
index 0000000..27e60b5
--- /dev/null
+++ b/cwa_qr/__init__.py
@@ -0,0 +1,11 @@
+from .cwa import CwaEventDescription, generate_qr_code, generate_url, generate_payload, lowlevel
+from .rollover import rollover_date
+
+__all__ = [
+    "CwaEventDescription",
+    "generate_qr_code",
+    "generate_url",
+    "generate_payload",
+    "rollover_date",
+    "lowlevel",
+]
diff --git a/cwa/cwa.proto b/cwa_qr/cwa.proto
similarity index 100%
rename from cwa/cwa.proto
rename to cwa_qr/cwa.proto
diff --git a/cwa_qr/cwa.py b/cwa_qr/cwa.py
new file mode 100644
index 0000000..7f5101d
--- /dev/null
+++ b/cwa_qr/cwa.py
@@ -0,0 +1,100 @@
+import base64
+from typing import Optional, Union
+
+import qrcode
+from datetime import datetime, date
+
+from . import cwa_pb2 as lowlevel
+from . import seed
+
+PUBLIC_KEY_STR = 'gwLMzE153tQwAOf2MZoUXXfzWTdlSpfS99iZffmcmxOG9njSK4RTimFOFwDh6t0Tyw8XR01ugDYjtuKwj' \
+                 'juK49Oh83FWct6XpefPi9Skjxvvz53i9gaMmUEc96pbtoaA'
+
+PUBLIC_KEY = base64.standard_b64decode(PUBLIC_KEY_STR.encode('ascii'))
+
+
+class CwaEventDescription(object):
+    def __init__(self):
+        """Description of the Location, Required, String, max 100 Characters"""
+        self.location_description: Optional[str] = None
+
+        """Address of the Location, Required, String, max 100 Characters"""
+        self.location_address: Optional[str] = None
+
+        """Start of the Event, Optional, datetime in UTC"""
+        self.start_date_time: Optional[datetime] = None
+
+        """End of the Event, Optional, datetime in UTC"""
+        self.end_date_time: Optional[datetime] = None
+
+        """Type of the Location, Optional
+
+        one of
+        - cwa_qr.lowlevel.LOCATION_TYPE_UNSPECIFIED = 0
+        - cwa_qr.lowlevel.LOCATION_TYPE_PERMANENT_OTHER = 1
+        - cwa_qr.lowlevel.LOCATION_TYPE_TEMPORARY_OTHER = 2
+        - cwa_qr.lowlevel.LOCATION_TYPE_PERMANENT_RETAIL = 3
+        - cwa_qr.lowlevel.LOCATION_TYPE_PERMANENT_FOOD_SERVICE = 4
+        - cwa_qr.lowlevel.LOCATION_TYPE_PERMANENT_CRAFT = 5
+        - cwa_qr.lowlevel.LOCATION_TYPE_PERMANENT_WORKPLACE = 6
+        - cwa_qr.lowlevel.LOCATION_TYPE_PERMANENT_EDUCATIONAL_INSTITUTION = 7
+        - cwa_qr.lowlevel.LOCATION_TYPE_PERMANENT_PUBLIC_BUILDING = 8
+        - cwa_qr.lowlevel.LOCATION_TYPE_TEMPORARY_CULTURAL_EVENT = 9
+        - cwa_qr.lowlevel.LOCATION_TYPE_TEMPORARY_CLUB_ACTIVITY = 10
+        - cwa_qr.lowlevel.LOCATION_TYPE_TEMPORARY_PRIVATE_EVENT = 11
+        - cwa_qr.lowlevel.LOCATION_TYPE_TEMPORARY_WORSHIP_SERVICE = 12
+        """
+        self.location_type: Optional[int] = None
+
+        """Default Checkout-Time im Minutes, Optional"""
+        self.default_check_in_length_in_minutes: Optional[int] = None
+
+        """Seed to rotate the QR-Code, Optional, [str, bytes, int, float, date, datetime] or None (Default)
+
+        Use with caution & read the Section about *Rotating QR-Codes* in the README first! If unsure, leave blank.
+        """
+        self.seed: Union[str, bytes, int, float, date, datetime, None] = None
+
+
+def generate_payload(event_description: CwaEventDescription) -> lowlevel.QRCodePayload:
+    payload = lowlevel.QRCodePayload()
+    payload.version = 1
+
+    payload.locationData.version = 1
+    payload.locationData.description = event_description.location_description
+    payload.locationData.address = event_description.location_address
+    payload.locationData.startTimestamp = int(event_description.start_date_time.timestamp()) if \
+        event_description.start_date_time else 0
+    payload.locationData.endTimestamp = int(event_description.end_date_time.timestamp()) if \
+        event_description.end_date_time else 0
+
+    payload.crowdNotifierData.version = 1
+    payload.crowdNotifierData.publicKey = PUBLIC_KEY
+    payload.crowdNotifierData.cryptographicSeed = seed.construct_seed(event_description.seed)
+
+    cwa_location_data = lowlevel.CWALocationData()
+    cwa_location_data.version = 1
+    cwa_location_data.type = event_description.location_type if \
+        event_description.location_type is not None else lowlevel.LOCATION_TYPE_UNSPECIFIED
+    cwa_location_data.defaultCheckInLengthInMinutes = event_description.default_check_in_length_in_minutes if \
+        event_description.default_check_in_length_in_minutes is not None else 0
+
+    payload.countryData = cwa_location_data.SerializeToString()
+    return payload
+
+
+def generate_url(event_description: CwaEventDescription) -> str:
+    payload = generate_payload(event_description)
+    serialized = payload.SerializeToString()
+    encoded = base64.urlsafe_b64encode(serialized)
+    url = 'https://e.coronawarn.app?v=1#' + encoded.decode('ascii')
+
+    return url
+
+
+def generate_qr_code(event_description: CwaEventDescription) -> qrcode.QRCode:
+    qr = qrcode.QRCode()
+    qr.add_data(generate_url(event_description))
+    qr.make(fit=True)
+
+    return qr
diff --git a/cwa/cwa_pb2.py b/cwa_qr/cwa_pb2.py
similarity index 100%
rename from cwa/cwa_pb2.py
rename to cwa_qr/cwa_pb2.py
diff --git a/cwa/generate-cwa_pb2.sh b/cwa_qr/generate-cwa_pb2.sh
similarity index 100%
rename from cwa/generate-cwa_pb2.sh
rename to cwa_qr/generate-cwa_pb2.sh
diff --git a/cwa/rollover.py b/cwa_qr/rollover.py
similarity index 77%
rename from cwa/rollover.py
rename to cwa_qr/rollover.py
index 3992699..cad324a 100644
--- a/cwa/rollover.py
+++ b/cwa_qr/rollover.py
@@ -1,7 +1,7 @@
 from datetime import datetime, date, time, timedelta
 
 
-def rolloverDate(dt: datetime, rollover: time) -> date:
+def rollover_date(dt: datetime, rollover: time) -> date:
     given_time = dt.time()
     if given_time < rollover:
         return dt.date() - timedelta(days=1)
diff --git a/cwa/seed.py b/cwa_qr/seed.py
similarity index 87%
rename from cwa/seed.py
rename to cwa_qr/seed.py
index 2e9cc84..4d5ee96 100644
--- a/cwa/seed.py
+++ b/cwa_qr/seed.py
@@ -1,7 +1,7 @@
 import random
 
 
-def constructSeed(seed) -> bytes:
+def construct_seed(seed) -> bytes:
     if seed is None:
         seed = b''
 
diff --git a/example.py b/example.py
index 7812245..c402bfb 100755
--- a/example.py
+++ b/example.py
@@ -2,28 +2,28 @@
 
 from datetime import datetime, time, timedelta, timezone
 
-import cwa
+import cwa_qr
 
 # Construct Event-Descriptor
-eventDescription = cwa.CwaEventDescription()
-eventDescription.locationDescription = 'Zuhause'
-eventDescription.locationAddress = 'Gau-Odernheim'
-eventDescription.startDateTime = datetime.now(timezone.utc)
-eventDescription.endDateTime = datetime.now(timezone.utc) + timedelta(days=2)
-eventDescription.locationType = cwa.lowlevel.LOCATION_TYPE_PERMANENT_WORKPLACE
-eventDescription.defaultCheckInLengthInMinutes = 4 * 60
+event_description = cwa_qr.CwaEventDescription()
+event_description.location_description = 'Zuhause'
+event_description.location_address = 'Gau-Odernheim'
+event_description.start_date_time = datetime.now(timezone.utc)
+event_description.end_date_time = datetime.now(timezone.utc) + timedelta(days=2)
+event_description.location_type = cwa_qr.lowlevel.LOCATION_TYPE_PERMANENT_WORKPLACE
+event_description.default_check_in_length_in_minutes = 4 * 60
 
 # Renew QR-Code every night at 4:00
-seedDate = eventDescription.seed = cwa.rolloverDate(datetime.now(), time(4, 0))
+seedDate = event_description.seed = cwa_qr.rollover_date(datetime.now(), time(4, 0))
 print("seedDate", seedDate)
-eventDescription.seed = "Some Secret" + str(seedDate)
+event_description.seed = "Some Secret" + str(seedDate)
 
 # Generate URL for Debugging purpose
-url = cwa.generateUrl(eventDescription)
+url = cwa_qr.generate_url(event_description)
 print('url', url)
 
 # Generate QR-Code
-qr = cwa.generateQrCode(eventDescription)
+qr = cwa_qr.generate_qr_code(event_description)
 
 img = qr.make_image(fill_color="black", back_color="white")
 img.save('example.png')
diff --git a/lint.sh b/lint.sh
index 31805cc..56e2dd4 100755
--- a/lint.sh
+++ b/lint.sh
@@ -2,7 +2,7 @@
 set -ex
 
 # stop the build if there are Python syntax errors or undefined names
-flake8 *.py cwa --count --select=E9,F63,F7,F82 --show-source --statistics
+flake8 *.py cwa_qr --count --select=E9,F63,F7,F82 --show-source --statistics
 
 # The GitHub editor is 127 chars wide
-flake8 *.py cwa --count --max-complexity=10 --max-line-length=127 --statistics --exclude cwa/cwa_pb2.py
+flake8 *.py cwa_qr --count --max-complexity=10 --max-line-length=127 --statistics --exclude cwa_qr/cwa_pb2.py
diff --git a/setup.py b/setup.py
index 405f189..0ad7da2 100755
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@
 from setuptools import find_packages, setup
 
 setup(
-    name='cwa',
+    name='cwa_qr',
     version='1.0',
     description='Python Implementation of the CoronaWarnApp (CWA) Event Registration',
     author='Greg Ward',


Test patch:
diff --git a/cwa/test_cwa.py b/cwa/test_cwa.py
deleted file mode 100644
index d557adb..0000000
--- a/cwa/test_cwa.py
+++ /dev/null
@@ -1,90 +0,0 @@
-import io
-from datetime import datetime, timedelta, timezone
-
-import qrcode
-
-from . import cwa
-
-fullEventDescription = cwa.CwaEventDescription()
-fullEventDescription.locationDescription = 'Zuhause'
-fullEventDescription.locationAddress = 'Gau-Odernheim'
-fullEventDescription.startDateTime = datetime.now(timezone.utc)
-fullEventDescription.endDateTime = datetime.now(timezone.utc) + timedelta(days=2)
-fullEventDescription.locationType = cwa.lowlevel.LOCATION_TYPE_PERMANENT_WORKPLACE
-fullEventDescription.defaultCheckInLengthInMinutes = 4 * 60
-
-
-def test_generate_with_minimal_parameters():
-    minimalEventDescription = cwa.CwaEventDescription()
-    minimalEventDescription.locationDescription = 'Zuhause'
-    minimalEventDescription.locationAddress = 'Gau-Odernheim'
-    url = cwa.generateUrl(minimalEventDescription)
-    assert url != ''
-
-
-def test_generate_with_all_parameters():
-    url = cwa.generateUrl(fullEventDescription)
-    assert url != ''
-
-
-def test_generate_without_seed_creates_same_results():
-    urlA = cwa.generateUrl(fullEventDescription)
-    urlB = cwa.generateUrl(fullEventDescription)
-    assert urlA == urlB
-
-
-def test_generate_with_same_seed_creates_same_result():
-    fullEventDescription.seed = 'a'
-    urlA = cwa.generateUrl(fullEventDescription)
-    urlB = cwa.generateUrl(fullEventDescription)
-    assert urlA == urlB
-
-
-def test_generate_with_different_seeds_creates_different_results():
-    fullEventDescription.seed = 'a'
-    urlA = cwa.generateUrl(fullEventDescription)
-
-    fullEventDescription.seed = 'b'
-    urlB = cwa.generateUrl(fullEventDescription)
-
-    assert urlA != urlB
-
-
-def test_generate_url():
-    url = cwa.generateUrl(fullEventDescription)
-    assert url is not None
-    assert isinstance(url, str)
-
-
-def test_generate_payload_object():
-    payload = cwa.generatePayload(fullEventDescription)
-    assert payload is not None
-    assert isinstance(payload, cwa.lowlevel.QRCodePayload)
-
-
-def test_generate_qr_code():
-    qr = cwa.generateQrCode(fullEventDescription)
-    assert qr is not None
-    assert isinstance(qr, qrcode.QRCode)
-
-
-def test_generate_qr_code_png():
-    qr = cwa.generateQrCode(fullEventDescription)
-    img = qr.make_image(fill_color="black", back_color="white")
-
-    img_bytes = io.BytesIO()
-    img.save(img_bytes)
-
-    assert img_bytes.getvalue().startswith(b'\x89PNG\r\n\x1a\n')
-
-
-def test_generate_qr_code_svg():
-    import qrcode.image.svg
-
-    qr = cwa.generateQrCode(fullEventDescription)
-    svg = qr.make_image(image_factory=qrcode.image.svg.SvgPathFillImage)
-
-    svg_bytes = io.BytesIO()
-    svg.save(svg_bytes)
-
-    assert svg_bytes.getvalue().startswith(b'<?xml')
diff --git a/cwa/test_rollover.py b/cwa/test_rollover.py
deleted file mode 100644
index a128b48..0000000
--- a/cwa/test_rollover.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from datetime import datetime, time, date
-
-from . import rollover
-
-
-def test_rollover():
-    rollover_time = time(4, 0)
-
-    # between 4:00 and 23:59 it's the given day
-    assert rollover.rolloverDate(datetime(2021, 1, 1, 6, 0), rollover_time) == date(2021, 1, 1)
-    assert rollover.rolloverDate(datetime(2021, 1, 1, 12, 0), rollover_time) == date(2021, 1, 1)
-    assert rollover.rolloverDate(datetime(2021, 1, 1, 20, 0), rollover_time) == date(2021, 1, 1)
-    assert rollover.rolloverDate(datetime(2021, 1, 1, 22, 0), rollover_time) == date(2021, 1, 1)
-    assert rollover.rolloverDate(datetime(2021, 1, 1, 23, 59), rollover_time) == date(2021, 1, 1)
-
-    # between 0:00 and 4:00 on the following day, it's still the day before
-    assert rollover.rolloverDate(datetime(2021, 1, 2, 0, 0), rollover_time) == date(2021, 1, 1)
-    assert rollover.rolloverDate(datetime(2021, 1, 2, 2, 0), rollover_time) == date(2021, 1, 1)
-    assert rollover.rolloverDate(datetime(2021, 1, 2, 3, 0), rollover_time) == date(2021, 1, 1)
-    assert rollover.rolloverDate(datetime(2021, 1, 2, 3, 59), rollover_time) == date(2021, 1, 1)
-
-    # after 4:00 it's the next day
-    assert rollover.rolloverDate(datetime(2021, 1, 2, 4, 0), rollover_time) == date(2021, 1, 2)
-    assert rollover.rolloverDate(datetime(2021, 1, 2, 12, 0), rollover_time) == date(2021, 1, 2)
-    assert rollover.rolloverDate(datetime(2021, 1, 2, 20, 0), rollover_time) == date(2021, 1, 2)
-    assert rollover.rolloverDate(datetime(2021, 1, 2, 23, 0), rollover_time) == date(2021, 1, 2)
-    assert rollover.rolloverDate(datetime(2021, 1, 2, 23, 59), rollover_time) == date(2021, 1, 2)
-
-    # and it continues to be the next day until 4:00
-    assert rollover.rolloverDate(datetime(2021, 1, 3, 0, 0), rollover_time) == date(2021, 1, 2)
-    assert rollover.rolloverDate(datetime(2021, 1, 3, 2, 0), rollover_time) == date(2021, 1, 2)
-    assert rollover.rolloverDate(datetime(2021, 1, 3, 3, 59), rollover_time) == date(2021, 1, 2)
-
-    # then it's the day after
-    assert rollover.rolloverDate(datetime(2021, 1, 3, 4, 0), rollover_time) == date(2021, 1, 3)
diff --git a/cwa/test_seed.py b/cwa/test_seed.py
deleted file mode 100644
index bd8d2ca..0000000
--- a/cwa/test_seed.py
+++ /dev/null
@@ -1,34 +0,0 @@
-from datetime import datetime
-
-from . import seed
-
-
-def assert_accepts_seed(value):
-    seedA = seed.constructSeed(value)
-    seedB = seed.constructSeed(value)
-    assert len(seedA) == 16
-    assert seedA == seedB
-
-
-def test_accepts_int_seed():
-    assert_accepts_seed(42)
-
-
-def test_accepts_float_seed():
-    assert_accepts_seed(3.1415)
-
-
-def test_accepts_str_seed():
-    assert_accepts_seed('foo')
-
-
-def test_accepts_datetime_seed():
-    assert_accepts_seed(datetime(2021, 4, 21, 10, 0))
-
-
-def test_accepts_bytes_seed():
-    assert_accepts_seed(b'\xDE\xAD\xBE\xEF')
-
-
-def test_accepts_none_seed():
-    assert_accepts_seed(None)
diff --git a/cwa_qr/test_cwa.py b/cwa_qr/test_cwa.py
new file mode 100644
index 0000000..a54c69f
--- /dev/null
+++ b/cwa_qr/test_cwa.py
@@ -0,0 +1,90 @@
+import io
+from datetime import datetime, timedelta, timezone
+
+import qrcode
+
+from . import cwa
+
+full_event_description = cwa.CwaEventDescription()
+full_event_description.location_description = 'Zuhause'
+full_event_description.location_address = 'Gau-Odernheim'
+full_event_description.start_date_time = datetime.now(timezone.utc)
+full_event_description.end_date_time = datetime.now(timezone.utc) + timedelta(days=2)
+full_event_description.location_type = cwa.lowlevel.LOCATION_TYPE_PERMANENT_WORKPLACE
+full_event_description.default_check_in_length_in_minutes = 4 * 60
+
+
+def test_generate_with_minimal_parameters():
+    minimal_event_description = cwa.CwaEventDescription()
+    minimal_event_description.location_description = 'Zuhause'
+    minimal_event_description.location_address = 'Gau-Odernheim'
+    url = cwa.generate_url(minimal_event_description)
+    assert url != ''
+
+
+def test_generate_with_all_parameters():
+    url = cwa.generate_url(full_event_description)
+    assert url != ''
+
+
+def test_generate_without_seed_creates_same_results():
+    url_a = cwa.generate_url(full_event_description)
+    url_b = cwa.generate_url(full_event_description)
+    assert url_a == url_b
+
+
+def test_generate_with_same_seed_creates_same_result():
+    full_event_description.seed = 'a'
+    url_a = cwa.generate_url(full_event_description)
+    url_b = cwa.generate_url(full_event_description)
+    assert url_a == url_b
+
+
+def test_generate_with_different_seeds_creates_different_results():
+    full_event_description.seed = 'a'
+    url_a = cwa.generate_url(full_event_description)
+
+    full_event_description.seed = 'b'
+    url_b = cwa.generate_url(full_event_description)
+
+    assert url_a != url_b
+
+
+def test_generate_url():
+    url = cwa.generate_url(full_event_description)
+    assert url is not None
+    assert isinstance(url, str)
+
+
+def test_generate_payload_object():
+    payload = cwa.generate_payload(full_event_description)
+    assert payload is not None
+    assert isinstance(payload, cwa.lowlevel.QRCodePayload)
+
+
+def test_generate_qr_code():
+    qr = cwa.generate_qr_code(full_event_description)
+    assert qr is not None
+    assert isinstance(qr, qrcode.QRCode)
+
+
+def test_generate_qr_code_png():
+    qr = cwa.generate_qr_code(full_event_description)
+    img = qr.make_image(fill_color="black", back_color="white")
+
+    img_bytes = io.BytesIO()
+    img.save(img_bytes)
+
+    assert img_bytes.getvalue().startswith(b'\x89PNG\r\n\x1a\n')
+
+
+def test_generate_qr_code_svg():
+    import qrcode.image.svg
+
+    qr = cwa.generate_qr_code(full_event_description)
+    svg = qr.make_image(image_factory=qrcode.image.svg.SvgPathFillImage)
+
+    svg_bytes = io.BytesIO()
+    svg.save(svg_bytes)
+
+    assert svg_bytes.getvalue().startswith(b'<?xml')
diff --git a/cwa_qr/test_rollover.py b/cwa_qr/test_rollover.py
new file mode 100644
index 0000000..8f9a12e
--- /dev/null
+++ b/cwa_qr/test_rollover.py
@@ -0,0 +1,35 @@
+from datetime import datetime, time, date
+
+from . import rollover
+
+
+def test_rollover():
+    rollover_time = time(4, 0)
+
+    # between 4:00 and 23:59 it's the given day
+    assert rollover.rollover_date(datetime(2021, 1, 1, 6, 0), rollover_time) == date(2021, 1, 1)
+    assert rollover.rollover_date(datetime(2021, 1, 1, 12, 0), rollover_time) == date(2021, 1, 1)
+    assert rollover.rollover_date(datetime(2021, 1, 1, 20, 0), rollover_time) == date(2021, 1, 1)
+    assert rollover.rollover_date(datetime(2021, 1, 1, 22, 0), rollover_time) == date(2021, 1, 1)
+    assert rollover.rollover_date(datetime(2021, 1, 1, 23, 59), rollover_time) == date(2021, 1, 1)
+
+    # between 0:00 and 4:00 on the following day, it's still the day before
+    assert rollover.rollover_date(datetime(2021, 1, 2, 0, 0), rollover_time) == date(2021, 1, 1)
+    assert rollover.rollover_date(datetime(2021, 1, 2, 2, 0), rollover_time) == date(2021, 1, 1)
+    assert rollover.rollover_date(datetime(2021, 1, 2, 3, 0), rollover_time) == date(2021, 1, 1)
+    assert rollover.rollover_date(datetime(2021, 1, 2, 3, 59), rollover_time) == date(2021, 1, 1)
+
+    # after 4:00 it's the next day
+    assert rollover.rollover_date(datetime(2021, 1, 2, 4, 0), rollover_time) == date(2021, 1, 2)
+    assert rollover.rollover_date(datetime(2021, 1, 2, 12, 0), rollover_time) == date(2021, 1, 2)
+    assert rollover.rollover_date(datetime(2021, 1, 2, 20, 0), rollover_time) == date(2021, 1, 2)
+    assert rollover.rollover_date(datetime(2021, 1, 2, 23, 0), rollover_time) == date(2021, 1, 2)
+    assert rollover.rollover_date(datetime(2021, 1, 2, 23, 59), rollover_time) == date(2021, 1, 2)
+
+    # and it continues to be the next day until 4:00
+    assert rollover.rollover_date(datetime(2021, 1, 3, 0, 0), rollover_time) == date(2021, 1, 2)
+    assert rollover.rollover_date(datetime(2021, 1, 3, 2, 0), rollover_time) == date(2021, 1, 2)
+    assert rollover.rollover_date(datetime(2021, 1, 3, 3, 59), rollover_time) == date(2021, 1, 2)
+
+    # then it's the day after
+    assert rollover.rollover_date(datetime(2021, 1, 3, 4, 0), rollover_time) == date(2021, 1, 3)
diff --git a/cwa_qr/test_seed.py b/cwa_qr/test_seed.py
new file mode 100644
index 0000000..35b8a96
--- /dev/null
+++ b/cwa_qr/test_seed.py
@@ -0,0 +1,34 @@
+from datetime import datetime
+
+from . import seed
+
+
+def assert_seed(input_value):
+    seed_a = seed.construct_seed(input_value)
+    seed_b = seed.construct_seed(input_value)
+    assert len(seed_a) == 16
+    assert seed_a == seed_b
+
+
+def test_accepts_int_seed():
+    assert_seed(42)
+
+
+def test_accepts_float_seed():
+    assert_seed(3.1415)
+
+
+def test_accepts_str_seed():
+    assert_seed('foo')
+
+
+def test_accepts_datetime_seed():
+    assert_seed(datetime(2021, 4, 21, 10, 0))
+
+
+def test_accepts_bytes_seed():
+    assert_seed(b'\xDE\xAD\xBE\xEF')
+
+
+def test_accepts_none_seed():
+    assert_seed(None)