From ca271b42e5caee46efa4f605a1d3136caaade4fa Mon Sep 17 00:00:00 2001
From: Jesper Zedlitz <jesper@zedlitz.de>
Date: Fri, 27 Dec 2024 08:52:13 +0100
Subject: [PATCH] new tests for ODS, RDF, and WMTS

---
 formats/gml_format.py                 |   8 -
 formats/ods_format.py                 |  27 +++
 formats/pdf_format.py                 |   4 +-
 formats/png_format.py                 |   5 +-
 formats/rdf_format.py                 |  19 ++
 formats/shp_format.py                 |  12 +-
 formats/wfs_srvc_format.py            |  22 ++-
 formats/wms_srvc_format.py            |  22 ++-
 formats/wmts_srvc_format.py           |  53 +++++
 formats/zip_format.py                 |  11 ++
 tests/data/WMTSCapabilities.xml       |  96 +++++++++
 tests/data/rdf.json                   | 273 ++++++++++++++++++++++++++
 tests/data/rdf.xml                    |  80 ++++++++
 tests/data/valid.docx                 | Bin 0 -> 4997 bytes
 tests/data/valid.ods                  | Bin 0 -> 9575 bytes
 tests/data/valid.odt                  | Bin 0 -> 9939 bytes
 tests/data/valid.xlsx                 | Bin 0 -> 5515 bytes
 tests/test_all_formats.py             |  26 +++
 tests/test_format_fidelity_checker.py |  58 +++---
 tests/test_gml_format.py              |   2 +
 tests/test_ods_format.py              |  36 ++++
 tests/test_rdf_format.py              |  32 +++
 tests/test_wmts_format.py             |  26 +++
 tests/test_xml_format.py              |  20 ++
 tests/test_zip_format.py              |  21 ++
 25 files changed, 791 insertions(+), 62 deletions(-)
 create mode 100644 formats/ods_format.py
 create mode 100644 formats/rdf_format.py
 create mode 100644 formats/wmts_srvc_format.py
 create mode 100644 formats/zip_format.py
 create mode 100644 tests/data/WMTSCapabilities.xml
 create mode 100644 tests/data/rdf.json
 create mode 100644 tests/data/rdf.xml
 create mode 100644 tests/data/valid.docx
 create mode 100644 tests/data/valid.ods
 create mode 100644 tests/data/valid.odt
 create mode 100644 tests/data/valid.xlsx
 create mode 100644 tests/test_all_formats.py
 create mode 100644 tests/test_ods_format.py
 create mode 100644 tests/test_rdf_format.py
 create mode 100644 tests/test_wmts_format.py
 create mode 100644 tests/test_xml_format.py
 create mode 100644 tests/test_zip_format.py

diff --git a/formats/gml_format.py b/formats/gml_format.py
index c74e401..b0dc4f9 100644
--- a/formats/gml_format.py
+++ b/formats/gml_format.py
@@ -1,6 +1,4 @@
 import geopandas
-from pyogrio.errors import DataSourceError
-from shapely.errors import GEOSException
 
 
 def is_valid(resource, file):
@@ -10,12 +8,6 @@ def is_valid(resource, file):
         try:
             geopandas.read_file(f)
             return True
-        except DataSourceError as e:
-            resource["error"] = str(e)
-            return False
-        except GEOSException as e:
-            resource["error"] = str(e)
-            return False
         except Exception as e:
             resource["error"] = str(e)
             return False
diff --git a/formats/ods_format.py b/formats/ods_format.py
new file mode 100644
index 0000000..0ff033b
--- /dev/null
+++ b/formats/ods_format.py
@@ -0,0 +1,27 @@
+import zipfile
+
+
+def is_valid(resource, file):
+    """Check if the content is a ODS file."""
+
+    if not zipfile.is_zipfile(file.name):
+        resource["error"] = "Not a ZIP file."
+        return False
+
+    with zipfile.ZipFile(file.name, "r") as zip_ref:
+        zip_contents = zip_ref.namelist()
+
+        required_files = ["mimetype", "content.xml", "meta.xml", "styles.xml"]
+
+        if not all(file in zip_contents for file in required_files):
+            resource["error"] = "That does not look like an ODS file."
+            return False
+
+        with zip_ref.open("mimetype") as mimetype_file:
+            mimetype_content = mimetype_file.read().decode("utf-8").strip()
+
+        if mimetype_content != "application/vnd.oasis.opendocument.spreadsheet":
+            resource["error"] = f"Incorrect MIME type: {mimetype_content}"
+            return False
+
+        return True
diff --git a/formats/pdf_format.py b/formats/pdf_format.py
index 4a7ee69..2c7e933 100644
--- a/formats/pdf_format.py
+++ b/formats/pdf_format.py
@@ -1,5 +1,4 @@
 from pypdf import PdfReader
-from pypdf.errors import PyPdfError
 
 
 def is_valid(resource, file):
@@ -9,5 +8,6 @@ def is_valid(resource, file):
         try:
             PdfReader(f)
             return True
-        except PyPdfError:
+        except Exception as e:
+            resource["error"] = str(e)
             return False
diff --git a/formats/png_format.py b/formats/png_format.py
index ec3a734..c7a9efb 100644
--- a/formats/png_format.py
+++ b/formats/png_format.py
@@ -1,4 +1,4 @@
-from PIL import Image, UnidentifiedImageError
+from PIL import Image
 
 
 def is_valid(resource, file):
@@ -7,5 +7,6 @@ def is_valid(resource, file):
     try:
         with Image.open(file.name, formats=["PNG"]):
             return True
-    except UnidentifiedImageError:
+    except Exception as e:
+        resource["error"] = str(e)
         return False
diff --git a/formats/rdf_format.py b/formats/rdf_format.py
new file mode 100644
index 0000000..27de8ee
--- /dev/null
+++ b/formats/rdf_format.py
@@ -0,0 +1,19 @@
+from rdflib import Graph
+
+
+def is_valid(resource, file):
+    """Check if file is a valid RDF document."""
+
+    try:
+        graph = Graph()
+        graph.parse(file.name)
+
+        # even an empty RDF document contains two statements
+        if len(graph) > 2:
+            return True
+        else:
+            resource["error"] = "RDF document does not contain any statements."
+            return False
+    except Exception as e:
+        resource["error"] = str(e)
+        return False
diff --git a/formats/shp_format.py b/formats/shp_format.py
index de42333..a133299 100644
--- a/formats/shp_format.py
+++ b/formats/shp_format.py
@@ -1,6 +1,4 @@
 import geopandas
-from pyogrio.errors import DataSourceError
-from shapely.errors import GEOSException
 import zipfile
 
 
@@ -24,10 +22,7 @@ def is_valid(resource, file):
         with open(file.name, "rb") as f:
             try:
                 geopandas.read_file(f)
-            except DataSourceError as e:
-                resource["error"] = str(e)
-                return False
-            except GEOSException as e:
+            except Exception as e:
                 resource["error"] = str(e)
                 return False
         return True
@@ -37,10 +32,7 @@ def is_valid(resource, file):
                 with z.open(shp) as f:
                     try:
                         geopandas.read_file(f"zip://{file.name}!{shp}")
-                    except DataSourceError as e:
-                        resource["error"] = str(e)
-                        return False
-                    except GEOSException as e:
+                    except Exception as e:
                         resource["error"] = str(e)
                         return False
         return True
diff --git a/formats/wfs_srvc_format.py b/formats/wfs_srvc_format.py
index bdf788e..9ded4c2 100644
--- a/formats/wfs_srvc_format.py
+++ b/formats/wfs_srvc_format.py
@@ -12,21 +12,26 @@ def _load_into_file(url):
         return temp_file
 
 
-def _is_capabilites_response(file):
+def _is_capabilites_response(resource, file):
     with open(file.name, "rb") as f:
         try:
             xml = ET.parse(f).getroot()
 
-            return (
+            if (
                 xml.tag == "{http://www.opengis.net/wfs/2.0}WFS_Capabilities"
                 or xml.tag == "{http://www.opengis.net/wfs}WFS_Capabilities"
-            )
-        except ET.ParseError:
+            ):
+                return True
+            else:
+                resource["error"] = "Root element is not WFS_Capabilities"
+                return False
+        except Exception as e:
+            resource["error"] = str(e)
             return False
 
 
 def is_valid(resource, file):
-    if _is_capabilites_response(file):
+    if _is_capabilites_response(resource, file):
         return True
 
     # The response is not a capabilites XML files. That is allowed.
@@ -38,7 +43,12 @@ def is_valid(resource, file):
             url = url + "?"
 
         url = url + "service=WFS&request=GetCapabilities"
-        return _is_capabilites_response(_load_into_file(url))
+
+        try:
+            return _is_capabilites_response(resource, _load_into_file(url))
+        except Exception as e:
+            resource["error"] = str(e)
+            return False
     else:
         # The URL already contains a getCapabilites request but the result was not a correct answer.
         return False
diff --git a/formats/wms_srvc_format.py b/formats/wms_srvc_format.py
index 7221d62..6263452 100644
--- a/formats/wms_srvc_format.py
+++ b/formats/wms_srvc_format.py
@@ -12,18 +12,25 @@ def _load_into_file(url):
         return temp_file
 
 
-def _is_capabilites_response(file):
+def _is_capabilites_response(resource, file):
     with open(file.name, "rb") as f:
         try:
             xml = ET.parse(f).getroot()
 
-            return xml.tag == "{http://www.opengis.net/wms}WMS_Capabilities"
-        except ET.ParseError:
+            if xml.tag == "{http://www.opengis.net/wms}WMS_Capabilities":
+                return True
+            else:
+                resource["error"] = (
+                    "Root element is not {http://www.opengis.net/wmts/1.0}WMS_Capabilities"
+                )
+                return False
+        except Exception as e:
+            resource["error"] = str(e)
             return False
 
 
 def is_valid(resource, file):
-    if _is_capabilites_response(file):
+    if _is_capabilites_response(resource, file):
         return True
 
     # The response is not a capabilites XML files. That is allowed.
@@ -35,7 +42,12 @@ def is_valid(resource, file):
             url = url + "?"
 
         url = url + "service=WMS&request=GetCapabilities"
-        return _is_capabilites_response(_load_into_file(url))
+        try:
+            return _is_capabilites_response(resource, _load_into_file(url))
+        except Exception as e:
+            resource["error"] = str(e)
+            return False
+
     else:
         # The URL already contains a getCapabilites request but the result was not a correct answer.
         return False
diff --git a/formats/wmts_srvc_format.py b/formats/wmts_srvc_format.py
new file mode 100644
index 0000000..a27c093
--- /dev/null
+++ b/formats/wmts_srvc_format.py
@@ -0,0 +1,53 @@
+import xml.etree.ElementTree as ET
+import requests
+import tempfile
+
+
+def _load_into_file(url):
+    response = requests.get(url)
+    response.raise_for_status()
+
+    with tempfile.NamedTemporaryFile(delete=False) as temp_file:
+        temp_file.write(response.content)
+        return temp_file
+
+
+def _is_capabilites_response(resource, file):
+    with open(file.name, "rb") as f:
+        try:
+            xml = ET.parse(f).getroot()
+
+            if xml.tag == "{http://www.opengis.net/wmts/1.0}Capabilities":
+                return True
+            else:
+                resource["error"] = (
+                    "Root element is not {http://www.opengis.net/wmts/1.0}WMS_Capabilities"
+                )
+                return False
+        except Exception as e:
+            resource["error"] = str(e)
+            return False
+
+
+def is_valid(resource, file):
+    if _is_capabilites_response(resource, file):
+        return True
+
+    # The response is not a capabilites XML files. That is allowed.
+    # Let's add the request parameters to the URL and try again.
+
+    url = resource["url"]
+    if "request=" not in url.lower():
+        if not url.endswith("?"):
+            url = url + "?"
+
+        url = url + "service=WMTS&request=GetCapabilities"
+        try:
+            return _is_capabilites_response(resource, _load_into_file(url))
+        except Exception as e:
+            resource["error"] = str(e)
+            return False
+
+    else:
+        # The URL already contains a getCapabilites request but the result was not a correct answer.
+        return False
diff --git a/formats/zip_format.py b/formats/zip_format.py
new file mode 100644
index 0000000..8de8b6a
--- /dev/null
+++ b/formats/zip_format.py
@@ -0,0 +1,11 @@
+import zipfile
+
+
+def is_valid(resource, file):
+    """Check if the file is a ZIP file."""
+
+    if not zipfile.is_zipfile(file.name):
+        resource["error"] = "Not a ZIP file."
+        return False
+
+    return True
diff --git a/tests/data/WMTSCapabilities.xml b/tests/data/WMTSCapabilities.xml
new file mode 100644
index 0000000..a9d19df
--- /dev/null
+++ b/tests/data/WMTSCapabilities.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0"?>
+<Capabilities xmlns="http://www.opengis.net/wmts/1.0" xmlns:ows="http://www.opengis.net/ows/1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:gml="http://www.opengis.net/gml" xsi:schemaLocation="http://www.opengis.net/wmts/1.0 http://schemas.opengis.net/wmts/1.0/wmtsGetCapabilities_response.xsd" version="1.0.0">
+  <ows:ServiceIdentification>
+    <ows:Title>WMTS_SH_ALKIS</ows:Title>
+    <ows:Abstract>Flächendeckende Beschreibung der Angaben zu den Layern "Flurstücke", "Gebäude" sowie zu den Gruppierungen "Tatsächliche Nutzung" und "Gesetzliche Festlegungen" gemäß der entsprechenden Objektbereiche im ALKIS-Objektartenkatalog. Die Gruppierung "Weiteres" ist optional und enthält die Objektbereiche "Bauwerke und Einrichtungen" sowie "Relief". Alle ALKIS-Objekte des Grunddatenbestandes (ausser Grenzpunkte und Netzpunkte) sind Pflichtinhalte. Alle weiteren ALKIS-Objekte können optional geführt werden. Die Präsentation der ALKIS-Daten erfolgt grundsätzlich nach dem ALKIS-Signaturenkatalog für AdV-Standardausgaben. Soweit im Signaturenkatalog festgelegt, stehen für alle Layer Darstellungen in Farbe zur Verfügung. Für "Flurstücke" und "Gebäude" werden zusätzlich Darstellungen in Grausstufen (entsprechend Signaturenkatalog) und in Gelb (keine Flächendarstellung, nur Konturen) angeboten.</ows:Abstract>
+    <ows:Keywords>
+        <ows:Keyword>WMS</ows:Keyword>
+        <ows:Keyword>Landesamt für Vermessung ung Geoinformation Schleswig-Holstein</ows:Keyword>
+        <ows:Keyword>LVermGeo SH</ows:Keyword>
+        <ows:Keyword>AdV</ows:Keyword>
+        <ows:Keyword>ALKIS</ows:Keyword>
+        <ows:Keyword>opendata</ows:Keyword>
+    </ows:Keywords>
+    <ows:ServiceType>OGC WMTS</ows:ServiceType>
+    <ows:ServiceTypeVersion>1.0.0</ows:ServiceTypeVersion>
+    <ows:Fees>Für die Nutzung der Daten ist die Creative Commons (CC BY 4.0) – Namensnennung 4.0 International anzuwenden. Die Lizenz ist über  http://creativecommons.org/licenses/by/4.0 abrufbar. Der Quellenvermerk lautet "© GeoBasis-DE/LVermGeo SH/CC BY 4.0" ||{"id":"cc-by/4.0","name":"Creative Commons Namensnennung – 4.0 International (CC BY 4.0)","url":"http://creativecommons.org/licenses/by/4.0/","quelle":"© GeoBasis-DE/LVermGeo SH/CC BY 4.0"}</ows:Fees>
+    <ows:AccessConstraints>NONE</ows:AccessConstraints>
+  </ows:ServiceIdentification>
+  <ows:ServiceProvider>
+    <ows:ProviderName>Landesamt für Vermessung und Geoinformation Schleswig-Holstein (LVermGeo SH)</ows:ProviderName>
+    <ows:ProviderSite xlink:href="http://www.schleswig-holstein.de/DE/Landesregierung/LVERMGEOSH/lvermgeosh_node.html"/>
+    <ows:ServiceContact>
+      <ows:IndividualName>Servicestelle Geoserver</ows:IndividualName>
+      <ows:PositionName></ows:PositionName>
+      <ows:ContactInfo>
+        <ows:Phone>
+          <ows:Voice>+49 (0)431 383-2019</ows:Voice>
+          <ows:Facsimile>+49 (0)431 988624-2019</ows:Facsimile>
+        </ows:Phone>
+        <ows:Address>
+          <ows:DeliveryPoint>Landesamt für Vermessung und Geoinformation Schleswig-Holstein (LVermGeo SH)</ows:DeliveryPoint>
+          <ows:City>Kiel</ows:City>
+          <ows:PostalCode>24106</ows:PostalCode>
+          <ows:Country>Germany</ows:Country>
+          <ows:ElectronicMailAddress>Geoserver@LVermGeo.landsh.de</ows:ElectronicMailAddress>
+        </ows:Address>
+      </ows:ContactInfo>
+    </ows:ServiceContact>
+  </ows:ServiceProvider>
+  <Contents>
+    <Layer>
+      <ows:Title>SH_ALKIS</ows:Title>
+      <ows:Abstract></ows:Abstract>
+      <ows:WGS84BoundingBox>
+        <ows:LowerCorner>0.10594674240568917 45.237542736025574</ows:LowerCorner>
+        <ows:UpperCorner>20.448891294525673 56.84787345153812</ows:UpperCorner>
+      </ows:WGS84BoundingBox>
+      <ows:Identifier>SH_ALKIS</ows:Identifier>
+      <Style>
+        <ows:Identifier>default</ows:Identifier>
+        <LegendURL
+          format="image/png"
+          xlink:href="https://dienste.gdi-sh.de//WMTS_SH_ALKIS_OpenGBD/service?service=WMS&amp;request=GetLegendGraphic&amp;version=1.3.0&amp;format=image%2Fpng&amp;layer=SH_ALKIS"
+        />
+      </Style>
+      <Format>image/png</Format>
+      <TileMatrixSetLink>
+        <TileMatrixSet>DE_EPSG_25832_ADV</TileMatrixSet>
+      </TileMatrixSetLink>
+      <ResourceURL format="image/png" resourceType="tile"
+        template="https://dienste.gdi-sh.de//WMTS_SH_ALKIS_OpenGBD/wmts/SH_ALKIS/{TileMatrixSet}/{TileMatrix}/{TileCol}/{TileRow}.png"/>
+    </Layer>
+    <TileMatrixSet>
+      <ows:Identifier>DE_EPSG_25832_ADV</ows:Identifier>
+      <ows:SupportedCRS>EPSG:25832</ows:SupportedCRS>
+      <TileMatrix>
+        <ows:Identifier>00</ows:Identifier>
+        <ScaleDenominator>4265.4591676995715</ScaleDenominator>
+        <TopLeftCorner>-46133.17 6301219.54</TopLeftCorner>
+        <TileWidth>256</TileWidth>
+        <TileHeight>256</TileHeight>
+        <MatrixWidth>4096</MatrixWidth>
+        <MatrixHeight>4096</MatrixHeight>
+      </TileMatrix>
+      <TileMatrix>
+        <ows:Identifier>01</ows:Identifier>
+        <ScaleDenominator>2132.729583849782</ScaleDenominator>
+        <TopLeftCorner>-46133.17 6301219.54</TopLeftCorner>
+        <TileWidth>256</TileWidth>
+        <TileHeight>256</TileHeight>
+        <MatrixWidth>8192</MatrixWidth>
+        <MatrixHeight>8192</MatrixHeight>
+      </TileMatrix>
+      <TileMatrix>
+        <ows:Identifier>02</ows:Identifier>
+        <ScaleDenominator>1066.3647919248929</ScaleDenominator>
+        <TopLeftCorner>-46133.17 6301219.54</TopLeftCorner>
+        <TileWidth>256</TileWidth>
+        <TileHeight>256</TileHeight>
+        <MatrixWidth>16384</MatrixWidth>
+        <MatrixHeight>16384</MatrixHeight>
+      </TileMatrix>
+    </TileMatrixSet>
+  </Contents>
+  <ServiceMetadataURL xlink:href="https://dienste.gdi-sh.de//WMTS_SH_ALKIS_OpenGBD/wmts/1.0.0/WMTSCapabilities.xml"/>
+</Capabilities>
\ No newline at end of file
diff --git a/tests/data/rdf.json b/tests/data/rdf.json
new file mode 100644
index 0000000..21a9c10
--- /dev/null
+++ b/tests/data/rdf.json
@@ -0,0 +1,273 @@
+[
+  {
+    "@id": "https://example.org/dataset/87e42608-769f-4ca8-8593-7546a027b2b8",
+    "@type": [
+      "http://www.w3.org/ns/dcat#Dataset"
+    ],
+    "http://purl.org/dc/terms/accessRights": [
+      {
+        "@id": "http://publications.europa.eu/resource/authority/access-right/PUBLIC"
+      }
+    ],
+    "http://purl.org/dc/terms/description": [
+      {
+        "@value": "Anzahl täglicher Landungen und Starts unbekannter Flugobjekte (UFOs) in Schleswig-Holstein.  🛸👽\n##Methodik\nGezählt werden nur die Landungen und Starts von UFOs, die gemeldet und zusätzlich offiziell bestätigt wurden. Sichtungen, die zu keinem Bodenkontakt führen, werden nicht gezählt.\n##Attribute\n- `datum` - Datum\n- `ufo_landungen` - Anzahl UFO-Landungen\n- `ufo_starts` - Anzahl UFO-Starts\n"
+      }
+    ],
+    "http://purl.org/dc/terms/identifier": [
+      {
+        "@value": "87e42608-769f-4ca8-8593-7546a027b2b8"
+      }
+    ],
+    "http://purl.org/dc/terms/issued": [
+      {
+        "@type": "http://www.w3.org/2001/XMLSchema#dateTime",
+        "@value": "2024-06-18T07:20:05.693344"
+      }
+    ],
+    "http://purl.org/dc/terms/license": [
+      {
+        "@id": "http://dcat-ap.de/def/licenses/cc-zero"
+      }
+    ],
+    "http://purl.org/dc/terms/modified": [
+      {
+        "@type": "http://www.w3.org/2001/XMLSchema#dateTime",
+        "@value": "2024-06-18T07:20:05.693344"
+      }
+    ],
+    "http://purl.org/dc/terms/publisher": [
+      {
+        "@id": "https://example.org/organization/ufo-kontrolle"
+      }
+    ],
+    "http://purl.org/dc/terms/spatial": [
+      {
+        "@id": "http://dcat-ap.de/def/politicalGeocoding/stateKey/01"
+      }
+    ],
+    "http://purl.org/dc/terms/temporal": [
+      {
+        "@id": "_:n1fa3c2476143497285348e0c39705837b1"
+      }
+    ],
+    "http://purl.org/dc/terms/title": [
+      {
+        "@value": "Bestätigte UFO-Landungen und -Starts"
+      }
+    ],
+    "http://www.w3.org/ns/dcat#distribution": [
+      {
+        "@id": "_:n1fa3c2476143497285348e0c39705837b4"
+      },
+      {
+        "@id": "_:n1fa3c2476143497285348e0c39705837b2"
+      }
+    ],
+    "http://www.w3.org/ns/dcat#keyword": [
+      {
+        "@value": "Weltall"
+      },
+      {
+        "@value": "Start"
+      },
+      {
+        "@value": "Landung"
+      },
+      {
+        "@value": "Raumschiff"
+      },
+      {
+        "@value": "UFO"
+      },
+      {
+        "@value": "Testdaten"
+      }
+    ],
+    "http://www.w3.org/ns/dcat#theme": [
+      {
+        "@id": "http://publications.europa.eu/resource/authority/data-theme/INTL"
+      }
+    ]
+  },
+  {
+    "@id": "_:n1fa3c2476143497285348e0c39705837b4",
+    "@type": [
+      "http://www.w3.org/ns/dcat#Distribution"
+    ],
+    "http://purl.org/dc/terms/format": [
+      {
+        "@id": "http://publications.europa.eu/resource/authority/file-type/JSON"
+      }
+    ],
+    "http://purl.org/dc/terms/issued": [
+      {
+        "@type": "http://www.w3.org/2001/XMLSchema#dateTime",
+        "@value": "2024-06-18T05:20:07.232559"
+      }
+    ],
+    "http://purl.org/dc/terms/license": [
+      {
+        "@id": "http://dcat-ap.de/def/licenses/cc-zero"
+      }
+    ],
+    "http://purl.org/dc/terms/modified": [
+      {
+        "@type": "http://www.w3.org/2001/XMLSchema#dateTime",
+        "@value": "2024-06-18T05:20:07.191976"
+      }
+    ],
+    "http://purl.org/dc/terms/rights": [
+      {
+        "@id": "http://dcat-ap.de/def/licenses/cc-zero"
+      }
+    ],
+    "http://purl.org/dc/terms/title": [
+      {
+        "@value": "Frictionless Data Resource"
+      }
+    ],
+    "http://spdx.org/rdf/terms#checksum": [
+      {
+        "@id": "_:n1fa3c2476143497285348e0c39705837b5"
+      }
+    ],
+    "http://www.w3.org/ns/dcat#accessURL": [
+      {
+        "@id": "http://localhost:8000/ufo-resource.json"
+      }
+    ],
+    "http://www.w3.org/ns/dcat#byteSize": [
+      {
+        "@type": "http://www.w3.org/2001/XMLSchema#integer",
+        "@value": 487
+      }
+    ],
+    "http://www.w3.org/ns/dcat#downloadURL": [
+      {
+        "@id": "http://localhost:8000/ufo-resource.json"
+      }
+    ],
+    "http://www.w3.org/ns/dcat#mediaType": [
+      {
+        "@id": "https://www.iana.org/assignments/media-types/application/csv"
+      }
+    ]
+  },
+  {
+    "@id": "_:n1fa3c2476143497285348e0c39705837b5",
+    "@type": [
+      "http://spdx.org/rdf/terms#Checksum"
+    ],
+    "http://spdx.org/rdf/terms#algorithm": [
+      {
+        "@id": "http://spdx.org/rdf/terms#checksumAlgorithm_md5"
+      }
+    ],
+    "http://spdx.org/rdf/terms#checksumValue": [
+      {
+        "@type": "http://www.w3.org/2001/XMLSchema#hexBinary",
+        "@value": "8dca8b179bbe0d46c5004da5112f6c4c"
+      }
+    ]
+  },
+  {
+    "@id": "_:n1fa3c2476143497285348e0c39705837b2",
+    "@type": [
+      "http://www.w3.org/ns/dcat#Distribution"
+    ],
+    "http://purl.org/dc/terms/format": [
+      {
+        "@id": "http://publications.europa.eu/resource/authority/file-type/CSV"
+      }
+    ],
+    "http://purl.org/dc/terms/issued": [
+      {
+        "@type": "http://www.w3.org/2001/XMLSchema#dateTime",
+        "@value": "2024-06-18T05:20:07.232559"
+      }
+    ],
+    "http://purl.org/dc/terms/license": [
+      {
+        "@id": "http://dcat-ap.de/def/licenses/cc-zero"
+      }
+    ],
+    "http://purl.org/dc/terms/modified": [
+      {
+        "@type": "http://www.w3.org/2001/XMLSchema#dateTime",
+        "@value": "2024-06-18T05:20:07.191976"
+      }
+    ],
+    "http://purl.org/dc/terms/rights": [
+      {
+        "@id": "http://dcat-ap.de/def/licenses/cc-zero"
+      }
+    ],
+    "http://purl.org/dc/terms/title": [
+      {
+        "@value": "ufo.csv"
+      }
+    ],
+    "http://spdx.org/rdf/terms#checksum": [
+      {
+        "@id": "_:n1fa3c2476143497285348e0c39705837b3"
+      }
+    ],
+    "http://www.w3.org/ns/dcat#accessURL": [
+      {
+        "@id": "http://localhost:8000/ufo.csv"
+      }
+    ],
+    "http://www.w3.org/ns/dcat#byteSize": [
+      {
+        "@type": "http://www.w3.org/2001/XMLSchema#integer",
+        "@value": 151
+      }
+    ],
+    "http://www.w3.org/ns/dcat#downloadURL": [
+      {
+        "@id": "http://localhost:8000/ufo.csv"
+      }
+    ],
+    "http://www.w3.org/ns/dcat#mediaType": [
+      {
+        "@id": "https://www.iana.org/assignments/media-types/application/csv"
+      }
+    ]
+  },
+  {
+    "@id": "_:n1fa3c2476143497285348e0c39705837b3",
+    "@type": [
+      "http://spdx.org/rdf/terms#Checksum"
+    ],
+    "http://spdx.org/rdf/terms#algorithm": [
+      {
+        "@id": "http://spdx.org/rdf/terms#checksumAlgorithm_sha1"
+      }
+    ],
+    "http://spdx.org/rdf/terms#checksumValue": [
+      {
+        "@type": "http://www.w3.org/2001/XMLSchema#hexBinary",
+        "@value": "3ffba0a43d3497a7918b376a335c31fbecc9325b"
+      }
+    ]
+  },
+  {
+    "@id": "_:n1fa3c2476143497285348e0c39705837b1",
+    "@type": [
+      "http://purl.org/dc/terms/PeriodOfTime"
+    ],
+    "http://www.w3.org/ns/dcat#endDate": [
+      {
+        "@type": "http://www.w3.org/2001/XMLSchema#date",
+        "@value": "2024-06-17"
+      }
+    ],
+    "http://www.w3.org/ns/dcat#startDate": [
+      {
+        "@type": "http://www.w3.org/2001/XMLSchema#date",
+        "@value": "2024-06-10"
+      }
+    ]
+  }
+]
\ No newline at end of file
diff --git a/tests/data/rdf.xml b/tests/data/rdf.xml
new file mode 100644
index 0000000..3e6690e
--- /dev/null
+++ b/tests/data/rdf.xml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?>
+<rdf:RDF
+   xmlns:dcat="http://www.w3.org/ns/dcat#"
+   xmlns:dcterms="http://purl.org/dc/terms/"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:spdx="http://spdx.org/rdf/terms#"
+>
+  <rdf:Description rdf:about="https://example.org/dataset/87e42608-769f-4ca8-8593-7546a027b2b8">
+    <rdf:type rdf:resource="http://www.w3.org/ns/dcat#Dataset"/>
+    <dcterms:accessRights rdf:resource="http://publications.europa.eu/resource/authority/access-right/PUBLIC"/>
+    <dcterms:description>Anzahl täglicher Landungen und Starts unbekannter Flugobjekte (UFOs) in Schleswig-Holstein.  🛸👽
+##Methodik
+Gezählt werden nur die Landungen und Starts von UFOs, die gemeldet und zusätzlich offiziell bestätigt wurden. Sichtungen, die zu keinem Bodenkontakt führen, werden nicht gezählt.
+##Attribute
+- `datum` - Datum
+- `ufo_landungen` - Anzahl UFO-Landungen
+- `ufo_starts` - Anzahl UFO-Starts
+</dcterms:description>
+    <dcterms:identifier>87e42608-769f-4ca8-8593-7546a027b2b8</dcterms:identifier>
+    <dcterms:issued rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2024-06-18T07:20:05.693344</dcterms:issued>
+    <dcterms:modified rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2024-06-18T07:20:05.693344</dcterms:modified>
+    <dcterms:license rdf:resource="http://dcat-ap.de/def/licenses/cc-zero"/>
+    <dcterms:publisher rdf:resource="https://example.org/organization/ufo-kontrolle"/>
+    <dcterms:spatial rdf:resource="http://dcat-ap.de/def/politicalGeocoding/stateKey/01"/>
+    <dcterms:temporal rdf:nodeID="n6747fd43db2143cca14c39970555b181b1"/>
+    <dcterms:title>Bestätigte UFO-Landungen und -Starts</dcterms:title>
+    <dcat:distribution rdf:nodeID="n6747fd43db2143cca14c39970555b181b2"/>
+    <dcat:distribution rdf:nodeID="n6747fd43db2143cca14c39970555b181b4"/>
+    <dcat:keyword>UFO</dcat:keyword>
+    <dcat:keyword>Landung</dcat:keyword>
+    <dcat:keyword>Start</dcat:keyword>
+    <dcat:keyword>Raumschiff</dcat:keyword>
+    <dcat:keyword>Weltall</dcat:keyword>
+    <dcat:keyword>Testdaten</dcat:keyword>
+    <dcat:theme rdf:resource="http://publications.europa.eu/resource/authority/data-theme/INTL"/>
+  </rdf:Description>
+  <rdf:Description rdf:nodeID="n6747fd43db2143cca14c39970555b181b4">
+    <rdf:type rdf:resource="http://www.w3.org/ns/dcat#Distribution"/>
+    <dcterms:format rdf:resource="http://publications.europa.eu/resource/authority/file-type/JSON"/>
+    <dcterms:issued rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2024-06-18T05:20:07.232559</dcterms:issued>
+    <dcterms:license rdf:resource="http://dcat-ap.de/def/licenses/cc-zero"/>
+    <dcterms:modified rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2024-06-18T05:20:07.191976</dcterms:modified>
+    <dcterms:rights rdf:resource="http://dcat-ap.de/def/licenses/cc-zero"/>
+    <dcterms:title>Frictionless Data Resource</dcterms:title>
+    <spdx:checksum rdf:nodeID="n6747fd43db2143cca14c39970555b181b5"/>
+    <dcat:accessURL rdf:resource="http://localhost:8000/ufo-resource.json"/>
+    <dcat:byteSize rdf:datatype="http://www.w3.org/2001/XMLSchema#integer">487</dcat:byteSize>
+    <dcat:downloadURL rdf:resource="http://localhost:8000/ufo-resource.json"/>
+    <dcat:mediaType rdf:resource="https://www.iana.org/assignments/media-types/application/csv"/>
+  </rdf:Description>
+  <rdf:Description rdf:nodeID="n6747fd43db2143cca14c39970555b181b2">
+    <rdf:type rdf:resource="http://www.w3.org/ns/dcat#Distribution"/>
+    <dcterms:format rdf:resource="http://publications.europa.eu/resource/authority/file-type/CSV"/>
+    <dcterms:issued rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2024-06-18T05:20:07.232559</dcterms:issued>
+    <dcterms:license rdf:resource="http://dcat-ap.de/def/licenses/cc-zero"/>
+    <dcterms:modified rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2024-06-18T05:20:07.191976</dcterms:modified>
+    <dcterms:rights rdf:resource="http://dcat-ap.de/def/licenses/cc-zero"/>
+    <dcterms:title>ufo.csv</dcterms:title>
+    <spdx:checksum rdf:nodeID="n6747fd43db2143cca14c39970555b181b3"/>
+    <dcat:accessURL rdf:resource="http://localhost:8000/ufo.csv"/>
+    <dcat:byteSize rdf:datatype="http://www.w3.org/2001/XMLSchema#integer">151</dcat:byteSize>
+    <dcat:downloadURL rdf:resource="http://localhost:8000/ufo.csv"/>
+    <dcat:mediaType rdf:resource="https://www.iana.org/assignments/media-types/application/csv"/>
+  </rdf:Description>
+  <rdf:Description rdf:nodeID="n6747fd43db2143cca14c39970555b181b5">
+    <rdf:type rdf:resource="http://spdx.org/rdf/terms#Checksum"/>
+    <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_md5"/>
+    <spdx:checksumValue rdf:datatype="http://www.w3.org/2001/XMLSchema#hexBinary">8dca8b179bbe0d46c5004da5112f6c4c</spdx:checksumValue>
+  </rdf:Description>
+  <rdf:Description rdf:nodeID="n6747fd43db2143cca14c39970555b181b3">
+    <rdf:type rdf:resource="http://spdx.org/rdf/terms#Checksum"/>
+    <spdx:algorithm rdf:resource="http://spdx.org/rdf/terms#checksumAlgorithm_sha1"/>
+    <spdx:checksumValue rdf:datatype="http://www.w3.org/2001/XMLSchema#hexBinary">3ffba0a43d3497a7918b376a335c31fbecc9325b</spdx:checksumValue>
+  </rdf:Description>
+  <rdf:Description rdf:nodeID="n6747fd43db2143cca14c39970555b181b1">
+    <rdf:type rdf:resource="http://purl.org/dc/terms/PeriodOfTime"/>
+    <dcat:endDate rdf:datatype="http://www.w3.org/2001/XMLSchema#date">2024-06-17</dcat:endDate>
+    <dcat:startDate rdf:datatype="http://www.w3.org/2001/XMLSchema#date">2024-06-10</dcat:startDate>
+  </rdf:Description>
+</rdf:RDF>
diff --git a/tests/data/valid.docx b/tests/data/valid.docx
new file mode 100644
index 0000000000000000000000000000000000000000..2fc6b99e13b6734528592b091b1fa76f3dbd8b2c
GIT binary patch
literal 4997
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

literal 0
HcmV?d00001

diff --git a/tests/data/valid.ods b/tests/data/valid.ods
new file mode 100644
index 0000000000000000000000000000000000000000..3726fbeac976ef3a6a7399dd2db5682b88c513b4
GIT binary patch
literal 9575
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
Y#dLr-D;r3eAcG)7DhmU{8wC&#0H2vNrvLx|

literal 0
HcmV?d00001

diff --git a/tests/data/valid.odt b/tests/data/valid.odt
new file mode 100644
index 0000000000000000000000000000000000000000..f4802adff3a9191037421a3ccd64151e95d8fe1f
GIT binary patch
literal 9939
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
F@c=a-Bz^z@

literal 0
HcmV?d00001

diff --git a/tests/data/valid.xlsx b/tests/data/valid.xlsx
new file mode 100644
index 0000000000000000000000000000000000000000..5f1a3bcdf0411c4b605d5fa51708daf42dc48ae7
GIT binary patch
literal 5515
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
XAK=Z(2C{;Sfs3JmiGks(5Qql=R2NH5

literal 0
HcmV?d00001

diff --git a/tests/test_all_formats.py b/tests/test_all_formats.py
new file mode 100644
index 0000000..b2b8fd9
--- /dev/null
+++ b/tests/test_all_formats.py
@@ -0,0 +1,26 @@
+import unittest
+import importlib
+import pkgutil
+import tempfile
+
+
+class TestAllFormats(unittest.TestCase):
+    def test_load_all_modules(self):
+        """Make sure that every format module has been loaded at least once.
+        Otherwise, the code coverage will not know about the file."""
+        package = importlib.import_module("formats")
+        modules = [module.name for module in pkgutil.iter_modules(package.__path__)]
+        for module in modules:
+            format_check_module = importlib.import_module("formats." + module)
+            with tempfile.NamedTemporaryFile(delete=True) as temp_file:
+                resource = {}
+                resource["url"] = "https://test.invalid/data"
+                try:
+                    format_check_module.is_valid(resource, temp_file)
+                except Exception as e:
+                    print(f"Module for format {module} failed.")
+                    raise (e)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/test_format_fidelity_checker.py b/tests/test_format_fidelity_checker.py
index 6c21860..f5798a7 100644
--- a/tests/test_format_fidelity_checker.py
+++ b/tests/test_format_fidelity_checker.py
@@ -11,10 +11,9 @@ from rdflib.namespace import RDF, DCAT
 
 class TestDcatCatalogCheck(unittest.TestCase):
     def setUp(self):
-        self.dcc = DcatCatalogCheck(
-            "http://localhost:8000/", "my_api_key")
+        self.dcc = DcatCatalogCheck("http://test.invalid:8000/", "my_api_key")
         # Mock the logger to capture log messages
-        self.logger_patch = patch.object(self.dcc, 'logger', MagicMock())
+        self.logger_patch = patch.object(self.dcc, "logger", MagicMock())
         self.mock_logger = self.logger_patch.start()
 
     def tearDown(self):
@@ -30,13 +29,10 @@ class TestDcatCatalogCheck(unittest.TestCase):
             "XML": ["application/xml"],
         }
 
-        self.assertTrue(self.dcc.is_mime_type_compatible(
-            "JSON", "application/json"))
-        self.assertFalse(self.dcc.is_mime_type_compatible(
-            "JSON", "application/xml"))
+        self.assertTrue(self.dcc.is_mime_type_compatible("JSON", "application/json"))
+        self.assertFalse(self.dcc.is_mime_type_compatible("JSON", "application/xml"))
         self.assertFalse(
-            self.dcc.is_mime_type_compatible(
-                "UnknownFormat", "application/json")
+            self.dcc.is_mime_type_compatible("UnknownFormat", "application/json")
         )
 
     def test_read_allowed_file_formats(self):
@@ -48,8 +44,7 @@ class TestDcatCatalogCheck(unittest.TestCase):
         ):
             formats = self.dcc.read_allowed_file_formats()
             self.assertEqual(
-                formats, {"JSON": ["application/json"],
-                          "XML": ["application/xml"]}
+                formats, {"JSON": ["application/json"], "XML": ["application/xml"]}
             )
 
     def test_load_uri_replacements(self):
@@ -59,10 +54,8 @@ class TestDcatCatalogCheck(unittest.TestCase):
                 read_data='[{"regex": "old", "replaced_by": "new"}]'
             ),
         ):
-
             replacements = self.dcc.load_uri_replacements()
-            self.assertEqual(
-                replacements, [{"regex": "old", "replaced_by": "new"}])
+            self.assertEqual(replacements, [{"regex": "old", "replaced_by": "new"}])
 
     # Simulate that the file does not exist
 
@@ -111,7 +104,7 @@ class TestDcatCatalogCheck(unittest.TestCase):
         self.dcc.load_http_complete = MagicMock(return_value=mock_response)
 
         resource = {}
-        resource["url"] = "http://localhost/data"
+        resource["url"] = "http://test.invalid/data"
         resource["format"] = "JSON"
         self.dcc.check_resource(resource)
         self.assertEqual(resource["accessible"], True)
@@ -128,7 +121,7 @@ class TestDcatCatalogCheck(unittest.TestCase):
         self.dcc.load_http_complete = MagicMock(return_value=mock_response)
 
         resource = {}
-        resource["url"] = "http://localhost/data"
+        resource["url"] = "http://test.invalid/data"
         resource["format"] = "JSON"
         self.dcc.check_resource(resource)
         self.assertEqual(resource["accessible"], True)
@@ -146,7 +139,7 @@ class TestDcatCatalogCheck(unittest.TestCase):
         self.dcc.load_http_complete = MagicMock(return_value=mock_response)
 
         resource = {}
-        resource["url"] = "http://localhost/data"
+        resource["url"] = "http://test.invalid/data"
         resource["format"] = "JSON"
         self.dcc.check_resource(resource)
         self.assertEqual(resource["accessible"], True)
@@ -164,7 +157,7 @@ class TestDcatCatalogCheck(unittest.TestCase):
         self.dcc.load_http_complete = MagicMock(return_value=mock_response)
 
         resource = {}
-        resource["url"] = "http://localhost/data"
+        resource["url"] = "http://test.invalid/data"
         resource["format"] = "JSON"
         self.dcc.check_resource(resource)
         self.assertEqual(resource["accessible"], True)
@@ -182,7 +175,7 @@ class TestDcatCatalogCheck(unittest.TestCase):
         self.dcc.load_http_complete = MagicMock(return_value=mock_response)
 
         resource = {}
-        resource["url"] = "http://localhost/data"
+        resource["url"] = "http://test.invalid/data"
         resource["format"] = "JSON"
         self.dcc.check_resource(resource)
         self.assertEqual(resource["accessible"], True)
@@ -198,7 +191,7 @@ class TestDcatCatalogCheck(unittest.TestCase):
         self.dcc.load_http_complete = MagicMock(return_value=mock_response)
 
         resource = {}
-        resource["url"] = "http://localhost/data"
+        resource["url"] = "http://test.invalid/data"
         resource["format"] = "XML"
         self.dcc.check_resource(resource)
         self.assertEqual(resource["accessible"], True)
@@ -214,7 +207,7 @@ class TestDcatCatalogCheck(unittest.TestCase):
         self.dcc.load_http_complete = MagicMock(return_value=mock_response)
 
         resource = {}
-        resource["url"] = "http://localhost/data"
+        resource["url"] = "http://test.invalid/data"
         resource["format"] = "PNG"
         resource["checksum_algorithm"] = (
             "http://spdx.org/rdf/terms#checksumAlgorithm_sha1"
@@ -247,7 +240,7 @@ class TestDcatCatalogCheck(unittest.TestCase):
         self.dcc.load_http_complete = MagicMock(return_value=mock_response)
 
         resource = {}
-        resource["url"] = "http://localhost/data"
+        resource["url"] = "http://test.invalid/data"
         resource["format"] = "JSON"
         self.dcc.check_resource(resource)
         self.assertEqual(resource.get("accessible"), True)
@@ -266,7 +259,7 @@ class TestDcatCatalogCheck(unittest.TestCase):
         self.dcc.load_http_complete = MagicMock(return_value=mock_response)
 
         resource = {}
-        resource["url"] = "http://localhost/data"
+        resource["url"] = "http://test.invalid/data"
         resource["format"] = "JSON"
         self.dcc.check_resource(resource)
         self.assertEqual(resource.get("accessible"), True)
@@ -285,7 +278,7 @@ class TestDcatCatalogCheck(unittest.TestCase):
         self.dcc.load_http_complete = MagicMock(return_value=mock_response)
 
         resource = {}
-        resource["url"] = "http://localhost/data"
+        resource["url"] = "http://test.invalid/data"
         resource["format"] = "JSON"
         self.dcc.check_resource(resource)
         self.assertEqual(resource.get("accessible"), True)
@@ -312,7 +305,7 @@ class TestDcatCatalogCheck(unittest.TestCase):
         self.dcc.load_http_complete = MagicMock(return_value=mock_response)
 
         resource = {}
-        resource["url"] = "http://localhost/zos116.zip"
+        resource["url"] = "http://test.invalid/zos116.zip"
         resource["format"] = "SHP"
 
         self.dcc.check_resource(resource)
@@ -326,7 +319,7 @@ class TestDcatCatalogCheck(unittest.TestCase):
         # Test data to simulate the contents of previous_results.json
         test_data = [
             {"url": "http://example.com", "status": "valid", "format": "JSON"},
-            {"url": "http://example.org", "status": "invalid", "format": "XML"}
+            {"url": "http://example.org", "status": "invalid", "format": "XML"},
         ]
 
         # Write test data to a file 'previous_results.json'
@@ -342,9 +335,11 @@ class TestDcatCatalogCheck(unittest.TestCase):
         self.assertIn("http://example.com", self.dcc.previous_results)
         self.assertIn("http://example.org", self.dcc.previous_results)
         self.assertEqual(
-            self.dcc.previous_results["http://example.com"]["status"], "valid")
+            self.dcc.previous_results["http://example.com"]["status"], "valid"
+        )
         self.assertEqual(
-            self.dcc.previous_results["http://example.org"]["status"], "invalid")
+            self.dcc.previous_results["http://example.org"]["status"], "invalid"
+        )
 
     @patch("os.path.exists", return_value=False)
     def test_read_previous_results_file_not_exist(self, mock_exists):
@@ -365,7 +360,12 @@ class TestDcatCatalogCheck(unittest.TestCase):
             "Invalid JSON at line 1: Expecting value: line 1 column 1 (char 0)"
         )
 
-    @patch("builtins.open", mock_open(read_data='{"status": "valid", "format": "JSON"}\n{"url": "http://example.com", "status": "valid", "format": "JSON"}'))
+    @patch(
+        "builtins.open",
+        mock_open(
+            read_data='{"status": "valid", "format": "JSON"}\n{"url": "http://example.com", "status": "valid", "format": "JSON"}'
+        ),
+    )
     @patch("os.path.exists", return_value=True)
     def test_read_previous_results_missing_url(self, mock_exists):
         """Test when the file has a line with missing 'url'."""
diff --git a/tests/test_gml_format.py b/tests/test_gml_format.py
index a2e40a7..e63638c 100644
--- a/tests/test_gml_format.py
+++ b/tests/test_gml_format.py
@@ -7,11 +7,13 @@ class TestGmlFormat(unittest.TestCase):
         resource = {}
         with open("tests/data/bermuda.gml", "r") as file:
             self.assertTrue(is_valid(resource, file))
+            self.assertIsNone(resource.get("error"))
 
     def test_is_valid__invalid(self):
         resource = {}
         with open("tests/data/correct.xml", "r") as file:
             self.assertFalse(is_valid(resource, file))
+            self.assertIsNotNone(resource.get("error"))
 
 
 if __name__ == "__main__":
diff --git a/tests/test_ods_format.py b/tests/test_ods_format.py
new file mode 100644
index 0000000..e152d5e
--- /dev/null
+++ b/tests/test_ods_format.py
@@ -0,0 +1,36 @@
+import unittest
+from formats.ods_format import is_valid
+
+
+class TestOdsFormat(unittest.TestCase):
+    def test_is_valid__valid(self):
+        resource = {}
+        with open("tests/data/valid.ods", "r") as file:
+            self.assertTrue(is_valid(resource, file))
+            self.assertIsNone(resource.get("error"))
+
+    def test_is_valid__invalid_no_zip(self):
+        resource = {}
+        with open("tests/data/correct.json", "r") as file:
+            self.assertFalse(is_valid(resource, file))
+            self.assertIsNotNone(resource.get("error"))
+
+    def test_is_valid__invalid_no_odt(self):
+        resource = {}
+        with open("tests/data/valid.odt", "r") as file:
+            self.assertFalse(is_valid(resource, file))
+            self.assertIsNotNone(resource.get("error"))
+            self.assertEqual(
+                "Incorrect MIME type: application/vnd.oasis.opendocument.text",
+                resource["error"],
+            )
+
+    def test_is_valid__invalid_zip(self):
+        resource = {}
+        with open("tests/data/valid.xlsx", "r") as file:
+            self.assertFalse(is_valid(resource, file))
+            self.assertIsNotNone(resource.get("error"))
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/test_rdf_format.py b/tests/test_rdf_format.py
new file mode 100644
index 0000000..8a313f3
--- /dev/null
+++ b/tests/test_rdf_format.py
@@ -0,0 +1,32 @@
+import unittest
+from formats.rdf_format import is_valid
+
+
+class TestRdfFormat(unittest.TestCase):
+    def test_is_valid__valid_turtle(self):
+        resource = {}
+        with open("tests/data/ufo.ttl", "r") as file:
+            self.assertTrue(is_valid(resource, file))
+            self.assertIsNone(resource.get("error"))
+
+    def test_is_valid__valid_xml(self):
+        resource = {}
+        with open("tests/data/rdf.xml", "r") as file:
+            self.assertTrue(is_valid(resource, file))
+            self.assertIsNone(resource.get("error"))
+
+    def test_is_valid__valid_jsonld(self):
+        resource = {}
+        with open("tests/data/rdf.json", "r") as file:
+            self.assertTrue(is_valid(resource, file))
+            self.assertIsNone(resource.get("error"))
+
+    def test_is_valid__invalid(self):
+        resource = {}
+        with open("tests/data/correct.json", "r") as file:
+            self.assertFalse(is_valid(resource, file))
+            self.assertIsNotNone(resource.get("error"))
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/test_wmts_format.py b/tests/test_wmts_format.py
new file mode 100644
index 0000000..9301e22
--- /dev/null
+++ b/tests/test_wmts_format.py
@@ -0,0 +1,26 @@
+import unittest
+from formats.wmts_srvc_format import is_valid
+
+
+class TestWmtsSrvcFormat(unittest.TestCase):
+    def test_is_valid__valid(self):
+        resource = {}
+        resource["url"] = (
+            "https://dienste.gdi-sh.invalid/WMTS_SH_ALKIS_OpenGBD/wmts/1.0.0/WMTSCapabilities.xml"
+        )
+        with open("tests/data/WMTSCapabilities.xml", "r") as file:
+            self.assertTrue(is_valid(resource, file))
+            self.assertIsNone(resource.get("error"))
+
+    def test_is_valid__invalid(self):
+        resource = {}
+        resource["url"] = (
+            "https://dienste.gdi-sh.invalid/WMTS_SH_ALKIS_OpenGBD/wmts/1.0.0/WMTSCapabilities.xml"
+        )
+        with open("tests/data/correct.xml", "r") as file:
+            self.assertFalse(is_valid(resource, file))
+            self.assertIsNotNone(resource.get("error"))
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/test_xml_format.py b/tests/test_xml_format.py
new file mode 100644
index 0000000..5672f9f
--- /dev/null
+++ b/tests/test_xml_format.py
@@ -0,0 +1,20 @@
+import unittest
+from formats.xml_format import is_valid
+
+
+class TestXmlFormat(unittest.TestCase):
+    def test_is_valid__valid(self):
+        resource = {}
+        with open("tests/data/correct.xml", "r") as file:
+            self.assertTrue(is_valid(resource, file))
+            self.assertIsNone(resource.get("error"))
+
+    def test_is_valid__invalid(self):
+        resource = {}
+        with open("tests/data/incorrect.xml", "r") as file:
+            self.assertFalse(is_valid(resource, file))
+            self.assertIsNotNone(resource.get("error"))
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/test_zip_format.py b/tests/test_zip_format.py
new file mode 100644
index 0000000..6161737
--- /dev/null
+++ b/tests/test_zip_format.py
@@ -0,0 +1,21 @@
+import unittest
+from formats.zip_format import is_valid
+
+
+class TestZipFormat(unittest.TestCase):
+    def test_is_valid__valid(self):
+        resource = {}
+        with open("tests/data/bermuda.zip", "r") as file:
+            self.assertTrue(is_valid(resource, file))
+            self.assertIsNone(resource.get("error"))
+
+    def test_is_valid__invalid(self):
+        resource = {}
+        with open("tests/data/correct.xml", "r") as file:
+            self.assertFalse(is_valid(resource, file))
+            self.assertIsNotNone(resource.get("error"))
+            self.assertEqual("Not a ZIP file.", resource["error"])
+
+
+if __name__ == "__main__":
+    unittest.main()
-- 
GitLab