diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml
index 2ff95bdac..d82bb7058 100644
--- a/.github/workflows/pull-request.yaml
+++ b/.github/workflows/pull-request.yaml
@@ -47,6 +47,34 @@ jobs:
run: |
make build-image
+ - name: Install CI dependencies
+ run: python3 -m pip install --user --requirement=ci/requirements.txt
+
+ - name: Run acceptance tests datadir
+ run: |
+ make acceptance-tests-datadir
+
+ - name: Print docker compose logs datadir
+ run: (cd compose && c2cciutils-docker-logs)
+ if: always()
+
+ - name: Cleanup acceptance tests datadir
+ run: |
+ make clean-acceptance-tests-datadir
+
+ # FIXME: fix pgconfig discrepancies before reactivating
+ # - name: Run acceptance tests pgconfig
+ # run: |
+ # make acceptance-tests-pgconfig
+
+ # - name: Print docker compose logs pgconfig
+ # run: (cd compose && c2cciutils-docker-logs)
+ # if: always()
+
+ # - name: Cleanup acceptance tests pgconfig
+ # run: |
+ # make clean-acceptance-tests-pgconfig
+
- name: Remove project jars from cached repository
run: |
rm -rf ~/.m2/repository/org/geoserver
diff --git a/Makefile b/Makefile
index 369298356..f23a4f28e 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,13 @@
all: install test build-image
-TAG=`mvn help:evaluate -Dexpression=project.version -q -DforceStdout`
+TAG=$(shell mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
COSIGN_PASSWORD := $(COSIGN_PASSWORD)
+COMPOSE_PGCONFIG_OPTIONS ?= -f compose.yml -f catalog-pgconfig.yml
+COMPOSE_DATADIR_OPTIONS ?= -f compose.yml -f catalog-datadir.yml
+COMPOSE_ACCEPTANCE_PGCONFIG_OPTIONS ?= $(COMPOSE_PGCONFIG_OPTIONS) -f acceptance.yml
+COMPOSE_ACCEPTANCE_DATADIR_OPTIONS ?= $(COMPOSE_DATADIR_OPTIONS) -f acceptance.yml
+UID=$(shell id -u)
+GID=$(shell id -g)
clean:
./mvnw clean
@@ -78,3 +84,28 @@ verify-image:
fi; \
done'
+.PHONY: build-acceptance
+build-acceptance:
+ docker build --tag=acceptance:$(TAG) acceptance_tests
+
+.PHONY: acceptance-tests-pgconfig
+acceptance-tests-pgconfig:
+acceptance-tests-pgconfig: build-acceptance
+ (cd compose/ && TAG=$(TAG) GS_USER=$(UID):$(GID) docker compose $(COMPOSE_ACCEPTANCE_PGCONFIG_OPTIONS) up -d)
+ (cd compose/ && TAG=$(TAG) GS_USER=$(UID):$(GID) docker compose $(COMPOSE_ACCEPTANCE_PGCONFIG_OPTIONS) exec -T acceptance bash -c 'until [ -f /tmp/healthcheck ]; do echo "Waiting for /tmp/healthcheck to be available..."; sleep 5; done && pytest . -vvv --color=yes')
+
+.PHONY: clean-acceptance-tests-pgconfig
+clean-acceptance-tests-pgconfig:
+ (cd compose/ && TAG=$(TAG) GS_USER=$(UID):$(GID) docker compose $(COMPOSE_ACCEPTANCE_PGCONFIG_OPTIONS) down -v)
+
+.PHONY: acceptance-tests-datadir
+acceptance-tests-datadir:
+acceptance-tests-datadir: build-acceptance
+ (cd compose/ && TAG=$(TAG) GS_USER=$(UID):$(GID) docker compose $(COMPOSE_ACCEPTANCE_DATADIR_OPTIONS) up -d)
+ (cd compose/ && TAG=$(TAG) GS_USER=$(UID):$(GID) docker compose $(COMPOSE_ACCEPTANCE_DATADIR_OPTIONS) exec -T acceptance bash -c 'until [ -f /tmp/healthcheck ]; do echo "Waiting for /tmp/healthcheck to be available..."; sleep 5; done && pytest . -vvv --color=yes')
+
+.PHONY: clean-acceptance-tests-datadir
+clean-acceptance-tests-datadir:
+ (cd compose/ && TAG=$(TAG) GS_USER=$(UID):$(GID) docker compose $(COMPOSE_ACCEPTANCE_DATADIR_OPTIONS) down -v)
+ rm -rf compose/catalog-datadir/*
+ touch compose/catalog-datadir/.keep
diff --git a/acceptance_tests/.gitignore b/acceptance_tests/.gitignore
new file mode 100644
index 000000000..c82b9e662
--- /dev/null
+++ b/acceptance_tests/.gitignore
@@ -0,0 +1,3 @@
+# ignore all python cache files recursively
+**/__pycache__/
+poetry.lock
diff --git a/acceptance_tests/Dockerfile b/acceptance_tests/Dockerfile
new file mode 100644
index 000000000..d48591306
--- /dev/null
+++ b/acceptance_tests/Dockerfile
@@ -0,0 +1,22 @@
+FROM ubuntu:24.04 AS base
+
+RUN apt-get update \
+ && apt-get upgrade --assume-yes \
+ && apt-get install --assume-yes --no-install-recommends \
+ vim curl jq libmagic1 zip python3-pip libpq-dev python3-dev gcc \
+ && rm -rf /var/lib/apt/lists/* \
+ && rm /usr/lib/python*/EXTERNALLY-MANAGED
+
+COPY . /acceptance_tests
+
+WORKDIR /acceptance_tests
+RUN python3 -m pip install --disable-pip-version-check .
+
+COPY entrypoint.py /bin/entrypoint.py
+
+ENV PYTHONUNBUFFERED=1
+ENTRYPOINT ["/bin/entrypoint.py"]
+
+HEALTHCHECK --interval=5s --start-period=15s --retries=20 CMD test -f /tmp/healthcheck || exit 1
+
+CMD ["sleep", "infinity"]
diff --git a/acceptance_tests/README.md b/acceptance_tests/README.md
new file mode 100644
index 000000000..50ba5b580
--- /dev/null
+++ b/acceptance_tests/README.md
@@ -0,0 +1,18 @@
+# GeoServer Cloud acceptance tests
+
+## Requirements
+
+[Poetry](https://python-poetry.org/docs/#installing-with-the-official-installer)
+
+## Installation
+
+```shell
+poetry install
+```
+
+# Run the tests
+First start the docker composition then run:
+
+```shell
+GEOSERVER_URL=http://localhost:9090/geoserver/cloud poetry run pytest -vvv .
+```
\ No newline at end of file
diff --git a/acceptance_tests/entrypoint.py b/acceptance_tests/entrypoint.py
new file mode 100755
index 000000000..033418b38
--- /dev/null
+++ b/acceptance_tests/entrypoint.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python3
+import time
+import os
+import requests
+import sys
+
+# Set variables
+start_time = time.time()
+
+# Set a maximum timeout from the environment or use 60 seconds as default
+max_time = int(os.getenv('MAX_TIMEOUT', 60))
+
+# Set the GEOSERVER_URL from the environment or use the default value
+GEOSERVER_URL = os.getenv('GEOSERVER_URL', 'http://gateway:8080/geoserver/cloud')
+GEOSERVER_USERNAME = os.getenv('GEOSERVER_USERNAME', 'admin')
+GEOSERVER_PASSWORD = os.getenv('GEOSERVER_PASSWORD', 'geoserver')
+
+# is we want to start directly with the passed command
+IGNORE_HEALTH_CHECK = os.getenv('IGNORE_HEALTH_CHECK', False)
+
+# Timeout function
+def timeout():
+ current_time = time.time()
+ if current_time - start_time > max_time:
+ return True
+ return False
+
+# Array of endpoints to check
+endpoints = [
+ f"{GEOSERVER_URL}/wms?SERVICE=WMS&REQUEST=GetCapabilities",
+ f"{GEOSERVER_URL}/wfs?SERVICE=WFS&REQUEST=GetCapabilities",
+ f"{GEOSERVER_URL}/wps?SERVICE=WPS&REQUEST=GetCapabilities",
+ f"{GEOSERVER_URL}/wcs?SERVICE=WCS&REQUEST=GetCapabilities",
+ f"{GEOSERVER_URL}/ows?SERVICE=WMS&REQUEST=GetCapabilities",
+ f"{GEOSERVER_URL}/gwc",
+ f"{GEOSERVER_URL}/rest",
+]
+
+if not IGNORE_HEALTH_CHECK:
+ # Loop through each endpoint and check if it's available
+ for endpoint in endpoints:
+ print(f"Waiting for {endpoint} to be available...")
+ if timeout():
+ print("Timeout")
+ break
+
+ while True:
+ try:
+ # Make a request to the endpoint
+ response = requests.get(endpoint, auth=(GEOSERVER_USERNAME, GEOSERVER_PASSWORD))
+ if response.status_code == 200:
+ print(f"{endpoint} is up")
+ break
+ else:
+ print(f"{endpoint} returned status code {response.status_code}")
+ except requests.exceptions.RequestException as e:
+ print(f"{endpoint} is not available - retrying...")
+
+ if timeout():
+ print("Timeout reached")
+ break
+
+ time.sleep(1)
+
+# create /tmp/healthcheck file to signal that the healthcheck is done
+with open("/tmp/healthcheck", "w") as f:
+ f.write("done")
+# Execute the command passed to the script anyway, this is useful for
+# running the tests and see what breaks
+if len(sys.argv) > 1:
+ command = sys.argv[1:]
+ os.execvp(command[0], command)
diff --git a/acceptance_tests/pyproject.toml b/acceptance_tests/pyproject.toml
new file mode 100644
index 000000000..956567cfa
--- /dev/null
+++ b/acceptance_tests/pyproject.toml
@@ -0,0 +1,19 @@
+[tool.poetry]
+name = "acceptance-tests"
+version = "0.1.0"
+description = "todo"
+authors = ["todo"]
+readme = "README.md"
+packages = [{ include = "tests" }]
+
+[tool.poetry.dependencies]
+python = "^3.10"
+pytest = "^8.3.3"
+psycopg2-binary = "^2.9.9"
+geoservercloud = "^0.2.5"
+sqlalchemy = "^2.0.35"
+
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
diff --git a/acceptance_tests/tests/conftest.py b/acceptance_tests/tests/conftest.py
new file mode 100644
index 000000000..e450c66ca
--- /dev/null
+++ b/acceptance_tests/tests/conftest.py
@@ -0,0 +1,60 @@
+import os
+from pathlib import Path
+
+import pytest
+import sqlalchemy
+from geoservercloud import GeoServerCloud
+
+GEOSERVER_URL = os.getenv("GEOSERVER_URL", "http://gateway:8080/geoserver/cloud")
+RESOURCE_DIR = Path(__file__).parent / "resources"
+# Database connection
+PGHOST = "geodatabase"
+PGPORT = 5432
+PGDATABASE = "geodata"
+PGUSER = "geodata"
+PGPASSWORD = "geodata"
+PGSCHEMA = "test1"
+WORKSPACE = "test_workspace"
+DATASTORE = "test_datastore"
+
+
+@pytest.fixture(scope="session", autouse=True)
+def engine():
+ yield sqlalchemy.create_engine(
+ f"postgresql://{PGUSER}:{PGPASSWORD}@{PGHOST}:{PGPORT}/{PGDATABASE}",
+ )
+
+
+@pytest.fixture(scope="session", autouse=True)
+def db_session(engine):
+ with engine.connect() as connection:
+ connection.execute(
+ sqlalchemy.sql.text(f"CREATE SCHEMA IF NOT EXISTS {PGSCHEMA}")
+ )
+ connection.execute(sqlalchemy.sql.text(f"SET SEARCH_PATH = {PGSCHEMA}"))
+ connection.commit()
+ yield connection
+ connection.execute(
+ sqlalchemy.sql.text(f"DROP SCHEMA IF EXISTS {PGSCHEMA} CASCADE")
+ )
+ connection.commit()
+
+
+@pytest.fixture(scope="module")
+def geoserver():
+ geoserver = GeoServerCloud(GEOSERVER_URL)
+ geoserver.recreate_workspace(WORKSPACE, set_default_workspace=True)
+ geoserver.create_pg_datastore(
+ workspace=WORKSPACE,
+ datastore=DATASTORE,
+ pg_host=PGHOST,
+ pg_port=PGPORT,
+ pg_db=PGDATABASE,
+ pg_user=PGUSER,
+ pg_password=PGPASSWORD,
+ pg_schema=PGSCHEMA,
+ set_default_datastore=True,
+ )
+ geoserver.publish_workspace(WORKSPACE)
+ yield geoserver
+ geoserver.delete_workspace(WORKSPACE)
diff --git a/acceptance_tests/tests/lib/utils.py b/acceptance_tests/tests/lib/utils.py
new file mode 100644
index 000000000..7212866d5
--- /dev/null
+++ b/acceptance_tests/tests/lib/utils.py
@@ -0,0 +1,15 @@
+from pathlib import Path
+
+
+def write_actual_image(response, tag):
+ file = Path(f"/tmp/{tag}_actual.png")
+ file.parent.mkdir(parents=True, exist_ok=True)
+ with open(file, "wb") as fs:
+ fs.write(response.read())
+
+
+def compare_images(dir, tag):
+ actual = f"/tmp/{tag}_actual.png"
+ expected = f"{dir}/{tag}_expected.png"
+ with open(actual, "rb") as fs1, open(expected, "rb") as fs2:
+ assert fs1.read() == fs2.read()
diff --git a/acceptance_tests/tests/resources/getmap_expected.png b/acceptance_tests/tests/resources/getmap_expected.png
new file mode 100644
index 000000000..c93b6740a
Binary files /dev/null and b/acceptance_tests/tests/resources/getmap_expected.png differ
diff --git a/acceptance_tests/tests/resources/labels/default_locale/default_value/language_None_expected.png b/acceptance_tests/tests/resources/labels/default_locale/default_value/language_None_expected.png
new file mode 100644
index 000000000..3c3b1829f
Binary files /dev/null and b/acceptance_tests/tests/resources/labels/default_locale/default_value/language_None_expected.png differ
diff --git a/acceptance_tests/tests/resources/labels/default_locale/default_value/language__expected.png b/acceptance_tests/tests/resources/labels/default_locale/default_value/language__expected.png
new file mode 100644
index 000000000..3272ee184
Binary files /dev/null and b/acceptance_tests/tests/resources/labels/default_locale/default_value/language__expected.png differ
diff --git a/acceptance_tests/tests/resources/labels/default_locale/default_value/language_de_expected.png b/acceptance_tests/tests/resources/labels/default_locale/default_value/language_de_expected.png
new file mode 100644
index 000000000..eb3c68630
Binary files /dev/null and b/acceptance_tests/tests/resources/labels/default_locale/default_value/language_de_expected.png differ
diff --git a/acceptance_tests/tests/resources/labels/default_locale/default_value/language_fr_expected.png b/acceptance_tests/tests/resources/labels/default_locale/default_value/language_fr_expected.png
new file mode 100644
index 000000000..3c3b1829f
Binary files /dev/null and b/acceptance_tests/tests/resources/labels/default_locale/default_value/language_fr_expected.png differ
diff --git a/acceptance_tests/tests/resources/labels/default_locale/default_value/language_it_expected.png b/acceptance_tests/tests/resources/labels/default_locale/default_value/language_it_expected.png
new file mode 100644
index 000000000..84cc65658
Binary files /dev/null and b/acceptance_tests/tests/resources/labels/default_locale/default_value/language_it_expected.png differ
diff --git a/acceptance_tests/tests/resources/labels/default_locale/no_default_value/language_None_expected.png b/acceptance_tests/tests/resources/labels/default_locale/no_default_value/language_None_expected.png
new file mode 100644
index 000000000..3c3b1829f
Binary files /dev/null and b/acceptance_tests/tests/resources/labels/default_locale/no_default_value/language_None_expected.png differ
diff --git a/acceptance_tests/tests/resources/labels/default_locale/no_default_value/language__expected.png b/acceptance_tests/tests/resources/labels/default_locale/no_default_value/language__expected.png
new file mode 100644
index 000000000..84cc65658
Binary files /dev/null and b/acceptance_tests/tests/resources/labels/default_locale/no_default_value/language__expected.png differ
diff --git a/acceptance_tests/tests/resources/labels/default_locale/no_default_value/language_it_expected.png b/acceptance_tests/tests/resources/labels/default_locale/no_default_value/language_it_expected.png
new file mode 100644
index 000000000..84cc65658
Binary files /dev/null and b/acceptance_tests/tests/resources/labels/default_locale/no_default_value/language_it_expected.png differ
diff --git a/acceptance_tests/tests/resources/labels/no_default_locale/default_value/language_None_expected.png b/acceptance_tests/tests/resources/labels/no_default_locale/default_value/language_None_expected.png
new file mode 100644
index 000000000..3272ee184
Binary files /dev/null and b/acceptance_tests/tests/resources/labels/no_default_locale/default_value/language_None_expected.png differ
diff --git a/acceptance_tests/tests/resources/labels/no_default_locale/default_value/language__expected.png b/acceptance_tests/tests/resources/labels/no_default_locale/default_value/language__expected.png
new file mode 100644
index 000000000..3272ee184
Binary files /dev/null and b/acceptance_tests/tests/resources/labels/no_default_locale/default_value/language__expected.png differ
diff --git a/acceptance_tests/tests/resources/labels/no_default_locale/default_value/language_de_expected.png b/acceptance_tests/tests/resources/labels/no_default_locale/default_value/language_de_expected.png
new file mode 100644
index 000000000..eb3c68630
Binary files /dev/null and b/acceptance_tests/tests/resources/labels/no_default_locale/default_value/language_de_expected.png differ
diff --git a/acceptance_tests/tests/resources/labels/no_default_locale/default_value/language_fr_expected.png b/acceptance_tests/tests/resources/labels/no_default_locale/default_value/language_fr_expected.png
new file mode 100644
index 000000000..3c3b1829f
Binary files /dev/null and b/acceptance_tests/tests/resources/labels/no_default_locale/default_value/language_fr_expected.png differ
diff --git a/acceptance_tests/tests/resources/labels/no_default_locale/default_value/language_it_expected.png b/acceptance_tests/tests/resources/labels/no_default_locale/default_value/language_it_expected.png
new file mode 100644
index 000000000..84cc65658
Binary files /dev/null and b/acceptance_tests/tests/resources/labels/no_default_locale/default_value/language_it_expected.png differ
diff --git a/acceptance_tests/tests/resources/labels/no_default_locale/no_default_value/language_None_expected.png b/acceptance_tests/tests/resources/labels/no_default_locale/no_default_value/language_None_expected.png
new file mode 100644
index 000000000..84cc65658
Binary files /dev/null and b/acceptance_tests/tests/resources/labels/no_default_locale/no_default_value/language_None_expected.png differ
diff --git a/acceptance_tests/tests/resources/labels/no_default_locale/no_default_value/language__expected.png b/acceptance_tests/tests/resources/labels/no_default_locale/no_default_value/language__expected.png
new file mode 100644
index 000000000..84cc65658
Binary files /dev/null and b/acceptance_tests/tests/resources/labels/no_default_locale/no_default_value/language__expected.png differ
diff --git a/acceptance_tests/tests/resources/labels/no_default_locale/no_default_value/language_it_expected.png b/acceptance_tests/tests/resources/labels/no_default_locale/no_default_value/language_it_expected.png
new file mode 100644
index 000000000..84cc65658
Binary files /dev/null and b/acceptance_tests/tests/resources/labels/no_default_locale/no_default_value/language_it_expected.png differ
diff --git a/acceptance_tests/tests/resources/localized_labels.sld b/acceptance_tests/tests/resources/localized_labels.sld
new file mode 100644
index 000000000..1dd2e011b
--- /dev/null
+++ b/acceptance_tests/tests/resources/localized_labels.sld
@@ -0,0 +1,36 @@
+
+
+
+ localized_labels
+
+ localized_labels
+
+
+ Localized labels
+
+
+
+ #000000
+
+
+
+
+
+
+
diff --git a/acceptance_tests/tests/resources/localized_no_default.sld b/acceptance_tests/tests/resources/localized_no_default.sld
new file mode 100644
index 000000000..bc1d5a8ad
--- /dev/null
+++ b/acceptance_tests/tests/resources/localized_no_default.sld
@@ -0,0 +1,37 @@
+
+
+
+ localized_style
+
+ localized_no_default
+
+
+ Localized, no default value
+
+ English
+ Deutsch
+ Français
+ Italiano
+
+
+
+
+ circle
+
+ #FF0000
+
+
+ 6
+
+
+
+
+
+
+
diff --git a/acceptance_tests/tests/resources/localized_with_default.sld b/acceptance_tests/tests/resources/localized_with_default.sld
new file mode 100644
index 000000000..316966c6b
--- /dev/null
+++ b/acceptance_tests/tests/resources/localized_with_default.sld
@@ -0,0 +1,37 @@
+
+
+
+ localized_style
+
+ localized_with_default
+
+
+ Localized, with default value
+
+ Default labelEnglish
+ Deutsch
+ Français
+ Italiano
+
+
+
+
+ circle
+
+ #FF0000
+
+
+ 6
+
+
+
+
+
+
+
diff --git a/acceptance_tests/tests/resources/wfs_payload.xml b/acceptance_tests/tests/resources/wfs_payload.xml
new file mode 100644
index 000000000..eefd14e03
--- /dev/null
+++ b/acceptance_tests/tests/resources/wfs_payload.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+ 2600000.0 1200000.0
+
+
+ 2024-05-13T08:14:48.763Z
+ 10
+ Title
+
+
+
\ No newline at end of file
diff --git a/acceptance_tests/tests/test_cascaded_stores.py b/acceptance_tests/tests/test_cascaded_stores.py
new file mode 100644
index 000000000..a5061c78e
--- /dev/null
+++ b/acceptance_tests/tests/test_cascaded_stores.py
@@ -0,0 +1,140 @@
+import json
+
+import pytest
+from conftest import GEOSERVER_URL
+from geoservercloud import GeoServerCloud
+
+WORKSPACE = "test_cascade"
+WMS_STORE = "test_cascaded_wms_store"
+WMS_URL = "https://wms.geo.admin.ch/?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetCapabilities"
+WMS_LAYER = "ch.swisstopo.swissboundaries3d-gemeinde-flaeche.fill"
+WMTS_STORE = "test_cascaded_wmts_store"
+WMTS_URL = "https://wmts.geo.admin.ch/EPSG/4326/1.0.0/WMTSCapabilities.xml"
+WMTS_LAYER = "ch.swisstopo.pixelkarte-grau"
+
+
+def create_cascaded_wms_store_payload():
+ return {
+ "wmsStore": {
+ "name": WMS_STORE,
+ "type": "WMS",
+ "enabled": "true",
+ "workspace": {"name": WORKSPACE},
+ "metadata": {"entry": {"@key": "useConnectionPooling", "$": "true"}},
+ "_default": "false",
+ "disableOnConnFailure": "false",
+ "capabilitiesURL": WMS_URL,
+ "maxConnections": 6,
+ "readTimeout": 60,
+ "connectTimeout": 30,
+ }
+ }
+
+
+def delete_wms_store(geoserver):
+ geoserver.delete_request(
+ f"/rest/workspaces/{WORKSPACE}/wmsstores/{WMS_STORE}?recurse=true"
+ )
+
+
+def delete_wmts_store(geoserver):
+ geoserver.delete_request(
+ f"/rest/workspaces/{WORKSPACE}/wmtsstores/{WMTS_STORE}?recurse=true"
+ )
+
+
+@pytest.fixture(scope="module")
+def geoserver():
+ geoserver = GeoServerCloud(url=GEOSERVER_URL)
+ geoserver.create_workspace(WORKSPACE, set_default_workspace=True)
+ geoserver.publish_workspace(WORKSPACE)
+ yield geoserver
+ # geoserver.delete_workspace(WORKSPACE)
+
+
+def test_cascaded_wms(geoserver):
+ format = "image/jpeg"
+
+ # Create WMS store
+ payload = create_cascaded_wms_store_payload()
+ response = geoserver.post_request(
+ f"/rest/workspaces/{WORKSPACE}/wmsstores", json=payload
+ )
+ assert response.status_code == 201
+
+ # Publish layer
+ payload = {
+ "wmsLayer": {
+ "name": WMS_LAYER,
+ }
+ }
+ response = geoserver.post_request(
+ f"/rest/workspaces/{WORKSPACE}/wmsstores/{WMS_STORE}/wmslayers",
+ json=payload,
+ )
+ assert response.status_code == 201
+
+ # Perform GetMap request
+ response = geoserver.get_map(
+ layers=[WMS_LAYER],
+ bbox=(2590000, 1196000, 2605000, 1203000),
+ size=(10, 10),
+ format=format,
+ )
+ assert response.info().get("Content-Type") == format
+
+ # Perform GetFeatureInfo request
+ response = geoserver.get_feature_info(
+ layers=[WMS_LAYER],
+ bbox=(2599999.5, 1199999.5, 2600000.5, 1200000.5),
+ size=(40, 40),
+ info_format="application/json",
+ xy=(20, 20),
+ )
+
+ # Due to conflicting formats, the forwarding of GetFeatureInfo requests from map.geo.admin (MapServer)
+ # through GeoServer is not possible as of 2.25.0.
+ # See https://sourceforge.net/p/geoserver/mailman/message/30757977/
+ data = json.loads(response.read().decode("utf-8"))
+ assert data.get("features") == []
+
+ delete_wms_store(geoserver)
+
+
+def test_cascaded_wmts(geoserver):
+ format = "image/jpeg"
+
+ # Create WMTS store
+ response = geoserver.create_wmts_store(
+ WORKSPACE,
+ WMTS_STORE,
+ capabilities="https://wmts.geo.admin.ch/EPSG/4326/1.0.0/WMTSCapabilities.xml",
+ )
+ assert response.status_code == 201
+
+ # Publish layer (GeoServer)
+ response = geoserver.create_wmts_layer(WORKSPACE, WMTS_STORE, WMTS_LAYER)
+ assert response.status_code == 201
+ response = geoserver.get_request(
+ f"/rest/workspaces/{WORKSPACE}/wmtsstores/{WMTS_STORE}/layers/{WMTS_LAYER}.json"
+ )
+ assert response.status_code == 200
+
+ # Publish the layer in GWC
+ response = geoserver.publish_gwc_layer(WORKSPACE, WMTS_LAYER)
+ assert response.status_code == 200
+
+ # Perform GetTile request (GWC)
+ response = geoserver.get_tile(
+ layer=f"{WORKSPACE}:{WMTS_LAYER}",
+ tile_matrix_set="EPSG:4326",
+ tile_matrix="EPSG:4326:9",
+ row=122,
+ column=534,
+ format=format,
+ )
+ assert response.info().get("Content-Type") == format
+
+ response = geoserver.delete_request(f"/gwc/rest/layers/{WORKSPACE}:{WMTS_LAYER}")
+ assert response.status_code == 200
+ delete_wmts_store(geoserver)
diff --git a/acceptance_tests/tests/test_datastore.py b/acceptance_tests/tests/test_datastore.py
new file mode 100644
index 000000000..b44d8212c
--- /dev/null
+++ b/acceptance_tests/tests/test_datastore.py
@@ -0,0 +1,24 @@
+from conftest import PGHOST, PGPORT, PGDATABASE, PGUSER, PGPASSWORD, PGSCHEMA
+
+
+def test_create_get_and_delete_datastore(geoserver):
+ workspace = datastore = "test_create_pg_datastore"
+ geoserver.create_workspace(workspace)
+ response = geoserver.create_pg_datastore(
+ workspace=workspace,
+ datastore=datastore,
+ pg_host=PGHOST,
+ pg_port=PGPORT,
+ pg_db=PGDATABASE,
+ pg_user=PGUSER,
+ pg_password=PGPASSWORD,
+ pg_schema=PGSCHEMA,
+ set_default_datastore=True,
+ )
+ assert response.status_code == 201
+ response = geoserver.get_request(
+ f"/rest/workspaces/{workspace}/datastores/{datastore}.json"
+ )
+ assert response.status_code == 200
+ response = geoserver.delete_workspace(workspace)
+ assert response.status_code == 200
diff --git a/acceptance_tests/tests/test_gwc.py b/acceptance_tests/tests/test_gwc.py
new file mode 100644
index 000000000..d5af14f84
--- /dev/null
+++ b/acceptance_tests/tests/test_gwc.py
@@ -0,0 +1,47 @@
+import pytest
+
+WORKSPACE = "test_gwc"
+WMTS_STORE = "test_gwc_store"
+WMTS_LAYER = "ch.swisstopo.swissimage"
+
+
+@pytest.fixture(scope="module")
+def geoserver_with_gwc_layers(geoserver):
+ geoserver.create_workspace(WORKSPACE)
+ geoserver.create_wmts_store(
+ WORKSPACE,
+ WMTS_STORE,
+ capabilities="https://wmts.geo.admin.ch/EPSG/4326/1.0.0/WMTSCapabilities.xml",
+ )
+ geoserver.create_wmts_layer(WORKSPACE, WMTS_STORE, WMTS_LAYER)
+ geoserver.get_request(
+ f"/rest/workspaces/{WORKSPACE}/wmtsstores/{WMTS_STORE}/layers/{WMTS_LAYER}.json"
+ )
+ response = geoserver.publish_gwc_layer(WORKSPACE, WMTS_LAYER)
+ assert response.status_code == 200
+ yield geoserver
+ geoserver.delete_workspace(WORKSPACE)
+
+
+def test_tile_cache(geoserver_with_gwc_layers):
+
+ response = geoserver_with_gwc_layers.get_tile(
+ format="image/png",
+ layer=f"{WORKSPACE}:{WMTS_LAYER}",
+ tile_matrix_set="EPSG:4326",
+ tile_matrix="EPSG:4326:9",
+ row=122,
+ column=534,
+ )
+ assert response.info().get("Content-Type") == "image/png"
+ assert response.info().get("Geowebcache-Cache-Result") == "MISS"
+
+ response = geoserver_with_gwc_layers.get_tile(
+ format="image/png",
+ layer=f"{WORKSPACE}:{WMTS_LAYER}",
+ tile_matrix_set="EPSG:4326",
+ tile_matrix="EPSG:4326:9",
+ row=122,
+ column=534,
+ )
+ assert response.info().get("Geowebcache-Cache-Result") == "HIT"
diff --git a/acceptance_tests/tests/test_i18n.py b/acceptance_tests/tests/test_i18n.py
new file mode 100755
index 000000000..81640659e
--- /dev/null
+++ b/acceptance_tests/tests/test_i18n.py
@@ -0,0 +1,417 @@
+#!/bin/env python
+
+import pytest
+from conftest import RESOURCE_DIR, WORKSPACE
+from lib.utils import compare_images, write_actual_image
+from requests.exceptions import JSONDecodeError
+from sqlalchemy.sql import text
+
+
+def international_title(default=True, de=True, fr=True, it=True, rm=True):
+ title = {}
+ if default:
+ title["default"] = "Default title"
+ if de:
+ title["de"] = "Punkte"
+ if fr:
+ title["fr"] = "Points"
+ if it:
+ title["it"] = "Punti"
+ if rm:
+ title["rm"] = "Puncts"
+ return title
+
+
+def assert_legend(geoserver, style, language, expected_label):
+ response = geoserver.get_legend_graphic(
+ "i18n_legend",
+ format="application/json",
+ language=language,
+ style=style,
+ workspace=WORKSPACE,
+ )
+ try:
+ label = response.json()["Legend"][0]["rules"][0]["title"]
+ assert label == expected_label
+ except (KeyError, JSONDecodeError):
+ print(f"Invalid response for language '{language}:'\n{response.content}")
+ assert False
+
+
+@pytest.fixture(scope="module")
+def geoserver_with_i18n_layers(geoserver):
+
+ # Create feature type with all languages
+ layer1 = "layer_all_languages"
+ title1 = international_title(default=True, de=True, fr=True, it=True, rm=True)
+ geoserver.create_feature_type(layer1, title=title1, epsg=2056)
+
+ # Create feature type without Rumantsch
+ layer2 = "layer_no_rumantsch"
+ title2 = international_title(default=True, de=True, fr=True, it=True, rm=False)
+ geoserver.create_feature_type(layer2, title=title2, epsg=2056)
+
+ # Create feature type without default language nor Rumantsch
+ layer3 = "layer_no_default_no_rumantsch"
+ title3 = international_title(default=False, de=True, fr=True, it=True, rm=False)
+ geoserver.create_feature_type(layer3, title=title3, epsg=2056)
+
+ yield geoserver
+
+
+@pytest.fixture(scope="module")
+def geoserver_default_locale_it(geoserver_with_i18n_layers):
+ geoserver_with_i18n_layers.set_default_locale_for_service(WORKSPACE, "it")
+ yield geoserver_with_i18n_layers
+ geoserver_with_i18n_layers.unset_default_locale_for_service(WORKSPACE)
+
+
+@pytest.fixture(scope="module")
+def geoserver_i18n_legend_layer(geoserver):
+ geoserver.create_workspace(WORKSPACE, set_default_workspace=True)
+ geoserver.create_feature_type("i18n_legend", epsg=2056)
+ geoserver.create_style_from_file(
+ "localized_with_default.sld",
+ f"{RESOURCE_DIR}/localized_with_default.sld",
+ workspace=WORKSPACE,
+ )
+ geoserver.create_style_from_file(
+ "localized_no_default.sld",
+ f"{RESOURCE_DIR}/localized_no_default.sld",
+ workspace=WORKSPACE,
+ )
+ yield geoserver
+
+
+@pytest.fixture(scope="function")
+def geoserver_i18n_legend_layer_and_default_locale_it(geoserver_i18n_legend_layer):
+ geoserver_i18n_legend_layer.set_default_locale_for_service(WORKSPACE, "it")
+ yield geoserver_i18n_legend_layer
+ geoserver_i18n_legend_layer.unset_default_locale_for_service(WORKSPACE)
+
+
+@pytest.fixture(scope="module")
+def geoserver_i18n_label_layer(geoserver, db_session):
+ feature_type = "i18n_labels"
+ style = "localized_labels"
+ file = f"{RESOURCE_DIR}/{style}.sld"
+ attributes = {
+ "geom": {"type": "Point", "required": True},
+ "label_default": {"type": "string", "required": False},
+ "label_de": {"type": "string", "required": False},
+ "label_fr": {"type": "string", "required": False},
+ }
+ geoserver.create_feature_type(feature_type, attributes=attributes, epsg=2056)
+ geoserver.create_style_from_file(style, file, workspace=WORKSPACE)
+ # Feature with labels in German, French and a default value
+ db_session.execute(
+ text(
+ f"INSERT INTO {feature_type} (geom, label_default, label_de, label_fr) VALUES "
+ "(public.ST_SetSRID(public.ST_MakePoint(2600000, 1200000), 2056), 'Default label', 'Deutsches Label', 'Étiquette française')"
+ )
+ )
+ # Feature with labels in German, French and no default value
+ db_session.execute(
+ text(
+ f"INSERT INTO {feature_type} (geom, label_de, label_fr) VALUES "
+ "(public.ST_SetSRID(public.ST_MakePoint(2700000, 1300000), 2056), 'Deutsches Label', 'Étiquette française')"
+ )
+ )
+ db_session.commit()
+ yield geoserver
+
+
+@pytest.fixture(scope="module")
+def geoserver_i18n_label_default_locale_fr(geoserver_i18n_label_layer):
+ geoserver_i18n_label_layer.set_default_locale_for_service(WORKSPACE, "fr")
+ yield geoserver_i18n_label_layer
+ geoserver_i18n_label_layer.unset_default_locale_for_service(WORKSPACE)
+
+
+@pytest.mark.parametrize(
+ "language,expected_titles",
+ [
+ (
+ "de",
+ {
+ "layer_all_languages": "Punkte",
+ "layer_no_rumantsch": "Punkte",
+ "layer_no_default_no_rumantsch": "Punkte",
+ },
+ ),
+ (
+ "de,fr",
+ {
+ "layer_all_languages": "Punkte",
+ "layer_no_rumantsch": "Punkte",
+ "layer_no_default_no_rumantsch": "Punkte",
+ },
+ ),
+ (
+ "fr,de",
+ {
+ "layer_all_languages": "Points",
+ "layer_no_rumantsch": "Points",
+ "layer_no_default_no_rumantsch": "Points",
+ },
+ ),
+ (
+ "rm",
+ {
+ "layer_all_languages": "Puncts",
+ "layer_no_rumantsch": "Default title",
+ "layer_no_default_no_rumantsch": "DID NOT FIND i18n CONTENT FOR THIS ELEMENT",
+ },
+ ),
+ (
+ "en",
+ {},
+ ),
+ (
+ None,
+ {
+ "layer_all_languages": "Default title",
+ "layer_no_rumantsch": "Default title",
+ "layer_no_default_no_rumantsch": "Punkte",
+ },
+ ),
+ (
+ "foobar",
+ {},
+ ),
+ ],
+)
+def test_i18n_layers(geoserver_with_i18n_layers, language, expected_titles):
+ capabilities = geoserver_with_i18n_layers.get_wms_layers(WORKSPACE, language)
+ layers = capabilities.get("Layer")
+ if type(layers) is list:
+ for expected_layer, expected_title in expected_titles.items():
+ actual_layer = next(
+ (layer for layer in layers if layer["Name"] == expected_layer), {}
+ )
+ assert actual_layer.get("Title") == expected_title
+ else:
+ print(capabilities)
+ assert expected_titles == {}
+ assert "ServiceExceptionReport" in capabilities
+
+
+@pytest.mark.parametrize(
+ "language,expected_titles",
+ [
+ (
+ "de",
+ {
+ "layer_all_languages": "Punkte",
+ "layer_no_rumantsch": "Punkte",
+ "layer_no_default_no_rumantsch": "Punkte",
+ },
+ ),
+ (
+ "rm",
+ {
+ "layer_all_languages": "Puncts",
+ "layer_no_rumantsch": "Default title",
+ "layer_no_default_no_rumantsch": "DID NOT FIND i18n CONTENT FOR THIS ELEMENT",
+ },
+ ),
+ (
+ "en",
+ {},
+ ),
+ (
+ None,
+ {
+ "layer_all_languages": "Punti",
+ "layer_no_rumantsch": "Punti",
+ "layer_no_default_no_rumantsch": "Punti",
+ },
+ ),
+ ],
+)
+@pytest.mark.skip(reason="Default locale is ignored in gs-cloud 1.6.1")
+def test_i18n_layers_default_locale(
+ geoserver_default_locale_it, language, expected_titles
+):
+ layers = geoserver_default_locale_it.get_wms_layers(WORKSPACE, language)
+ if type(layers) is list:
+ for expected_layer, expected_title in expected_titles.items():
+ actual_layer = next(
+ (layer for layer in layers if layer["Name"] == expected_layer), {}
+ )
+ print(actual_layer["Name"])
+ assert actual_layer.get("Title") == expected_title
+ else:
+ print(layers)
+ assert expected_titles == {}
+ assert "ServiceExceptionReport" in layers
+
+
+@pytest.mark.parametrize(
+ "language,expected_label",
+ [
+ ("en", "English"),
+ ("de", "Deutsch"),
+ ("fr", "Français"),
+ ("it", "Italiano"),
+ ("rm", "Default label"),
+ (None, "Default label"),
+ ("ru", "Default label"),
+ ("foobar", "Default label"),
+ ("it,fr,de", "Default label"),
+ ],
+)
+def test_i18n_legend_with_default_value(
+ geoserver_i18n_legend_layer, language, expected_label
+):
+ assert_legend(
+ geoserver_i18n_legend_layer,
+ "localized_with_default",
+ language,
+ expected_label,
+ )
+
+
+@pytest.mark.parametrize(
+ "language,expected_label",
+ [
+ ("it", "Italiano"),
+ ("rm", ""),
+ (None, ""),
+ ("ru", ""),
+ ("foobar", ""),
+ ("it,fr,de", ""),
+ ],
+)
+def test_i18n_legend_no_default_value(
+ geoserver_i18n_legend_layer, language, expected_label
+):
+
+ assert_legend(
+ geoserver_i18n_legend_layer,
+ "localized_no_default",
+ language,
+ expected_label,
+ )
+
+
+@pytest.mark.parametrize(
+ "language,expected_label",
+ [
+ ("en", "English"),
+ ("de", "Deutsch"),
+ ("fr", "Français"),
+ ("it", "Italiano"),
+ ("rm", "Default label"),
+ (None, "Default label"),
+ ("ru", "Default label"),
+ ("foobar", "Default label"),
+ ("it,fr,de", "Default label"),
+ ],
+)
+def test_i18n_legend_with_default_value_and_default_locale(
+ geoserver_i18n_legend_layer_and_default_locale_it, language, expected_label
+):
+ assert_legend(
+ geoserver_i18n_legend_layer_and_default_locale_it,
+ "localized_with_default",
+ language,
+ expected_label,
+ )
+
+
+@pytest.mark.parametrize(
+ "language,expected_label",
+ [
+ ("it", "Italiano"),
+ ("rm", ""),
+ (None, ""),
+ ("ru", ""),
+ ("foobar", ""),
+ ("it,fr,de", ""),
+ ],
+)
+def test_i18n_legend_no_default_value_default_locale(
+ geoserver_i18n_legend_layer_and_default_locale_it, language, expected_label
+):
+
+ assert_legend(
+ geoserver_i18n_legend_layer_and_default_locale_it,
+ "localized_no_default",
+ language,
+ expected_label,
+ )
+
+
+@pytest.mark.parametrize("language", ["de", "fr", "it", None, ""])
+def test_i18n_labels(geoserver_i18n_label_layer, language):
+
+ response = geoserver_i18n_label_layer.get_map(
+ layers=["i18n_labels"],
+ bbox=(2599999.5, 1199999.5, 2600000.5, 1200000.5),
+ size=(300, 100),
+ format="image/png",
+ transparent=False,
+ styles=["localized_labels"],
+ language=language,
+ )
+
+ file_root = f"labels/no_default_locale/default_value/language_{language}"
+ write_actual_image(response, file_root)
+ compare_images(RESOURCE_DIR, file_root)
+
+
+@pytest.mark.parametrize("language", ["it", "", None])
+def test_i18n_labels_no_default_value(geoserver_i18n_label_layer, language):
+
+ response = geoserver_i18n_label_layer.get_map(
+ layers=["i18n_labels"],
+ bbox=(2699999.5, 1299999.5, 2700000.5, 1300000.5),
+ size=(300, 100),
+ format="image/png",
+ transparent=False,
+ styles=["localized_labels"],
+ language=language,
+ )
+
+ file_root = f"labels/no_default_locale/no_default_value/language_{language}"
+ write_actual_image(response, file_root)
+ compare_images(RESOURCE_DIR, file_root)
+
+
+@pytest.mark.parametrize("language", ["de", "fr", "it", None, ""])
+def test_i18n_labels_default_locale(geoserver_i18n_label_default_locale_fr, language):
+
+ response = geoserver_i18n_label_default_locale_fr.get_map(
+ layers=["i18n_labels"],
+ bbox=(2599999.5, 1199999.5, 2600000.5, 1200000.5),
+ size=(300, 100),
+ format="image/png",
+ transparent=False,
+ styles=["localized_labels"],
+ language=language,
+ )
+
+ file_root = f"labels/default_locale/default_value/language_{language}"
+ write_actual_image(response, file_root)
+ compare_images(RESOURCE_DIR, file_root)
+
+
+@pytest.mark.parametrize("language", ["it", "", None])
+def test_i18n_labels_no_default_value_default_locale(
+ geoserver_i18n_label_default_locale_fr, language
+):
+
+ response = geoserver_i18n_label_default_locale_fr.get_map(
+ layers=["i18n_labels"],
+ bbox=(2699999.5, 1299999.5, 2700000.5, 1300000.5),
+ size=(300, 100),
+ format="image/png",
+ transparent=False,
+ styles=["localized_labels"],
+ language=language,
+ )
+
+ file_root = f"labels/default_locale/no_default_value/language_{language}"
+ write_actual_image(response, file_root)
+ compare_images(RESOURCE_DIR, file_root)
diff --git a/acceptance_tests/tests/test_wfs.py b/acceptance_tests/tests/test_wfs.py
new file mode 100644
index 000000000..fddc26e93
--- /dev/null
+++ b/acceptance_tests/tests/test_wfs.py
@@ -0,0 +1,76 @@
+from conftest import (
+ PGDATABASE,
+ PGHOST,
+ PGPASSWORD,
+ PGPORT,
+ PGSCHEMA,
+ PGUSER,
+ RESOURCE_DIR,
+)
+
+
+def test_wfs(geoserver):
+ workspace = datastore = feature_type = "test_wfs"
+ attributes = {
+ "geom": {
+ "type": "Point",
+ "required": True,
+ },
+ "id": {
+ "type": "integer",
+ "required": True,
+ },
+ "title": {
+ "type": "string",
+ "required": False,
+ },
+ "timestamp": {
+ "type": "datetime",
+ "required": False,
+ },
+ }
+ response = geoserver.create_workspace(workspace, set_default_workspace=True)
+ assert response.status_code == 201
+ response = geoserver.create_pg_datastore(
+ workspace=workspace,
+ datastore=datastore,
+ pg_host=PGHOST,
+ pg_port=PGPORT,
+ pg_db=PGDATABASE,
+ pg_user=PGUSER,
+ pg_password=PGPASSWORD,
+ pg_schema=PGSCHEMA,
+ set_default_datastore=True,
+ )
+ assert response.status_code == 201
+ response = geoserver.create_feature_type(
+ feature_type, attributes=attributes, epsg=2056
+ )
+ assert response.status_code == 201
+
+ # Post a feature through a WFS request
+ with open(f"{RESOURCE_DIR}/wfs_payload.xml") as file:
+ data = file.read()
+ response = geoserver.post_request(f"/{workspace}/wfs/", data=data)
+ assert response.status_code == 200
+
+ # GetFeature request
+ feature_collection = geoserver.get_feature(workspace, feature_type)
+ assert type(feature_collection) is dict
+ assert type(feature_collection.get("features")) is list
+ feature = feature_collection["features"][0]
+ properties = feature.get("properties")
+ assert properties.get("id") == 10
+ assert properties.get("title") == "Title"
+ assert properties.get("timestamp") == "2024-05-13T08:14:48.763Z"
+ assert feature.get("geometry", {}) == {
+ "type": "Point",
+ "coordinates": [2600000, 1200000],
+ }
+ assert feature_collection.get("crs") == {
+ "type": "name",
+ "properties": {"name": "urn:ogc:def:crs:EPSG::2056"},
+ }
+
+ response = geoserver.delete_workspace(workspace)
+ assert response.status_code == 200
diff --git a/acceptance_tests/tests/test_wms.py b/acceptance_tests/tests/test_wms.py
new file mode 100644
index 000000000..73af7b43f
--- /dev/null
+++ b/acceptance_tests/tests/test_wms.py
@@ -0,0 +1,128 @@
+import json
+
+from conftest import (
+ PGDATABASE,
+ PGHOST,
+ PGPASSWORD,
+ PGPORT,
+ PGSCHEMA,
+ PGUSER,
+ RESOURCE_DIR,
+)
+from lib.utils import compare_images, write_actual_image
+from sqlalchemy.sql import text
+
+
+def test_create_and_feature_type_and_get_map(db_session, geoserver):
+ workspace = datastore = feature_type = "test_create_feature_type"
+ geoserver.create_workspace(workspace, set_default_workspace=True)
+ geoserver.create_pg_datastore(
+ workspace=workspace,
+ datastore=datastore,
+ pg_host=PGHOST,
+ pg_port=PGPORT,
+ pg_db=PGDATABASE,
+ pg_user=PGUSER,
+ pg_password=PGPASSWORD,
+ pg_schema=PGSCHEMA,
+ set_default_datastore=True,
+ )
+ response = geoserver.create_feature_type(
+ feature_type,
+ epsg=2056,
+ )
+ assert response.status_code == 201
+
+ # Create feature
+ db_session.execute(
+ text(
+ f"INSERT INTO {feature_type} (geom) VALUES (public.ST_SetSRID(public.ST_MakePoint(2600000, 1200000), 2056))"
+ )
+ )
+ db_session.commit()
+
+ # GetMap request
+ response = geoserver.get_map(
+ layers=[feature_type],
+ bbox=(2599999.5, 1199999.5, 2600000.5, 1200000.5),
+ size=(40, 40),
+ format="image/png",
+ transparent=False,
+ )
+
+ file_root = f"getmap"
+ write_actual_image(response, file_root)
+ compare_images(RESOURCE_DIR, file_root)
+
+ geoserver.delete_workspace(workspace)
+
+
+def test_get_feature_info(db_session, geoserver):
+ workspace = datastore = feature_type = "test_get_feature_info"
+ attributes = {
+ "geom": {
+ "type": "Point",
+ "required": True,
+ },
+ "label": {
+ "type": "string",
+ "required": False,
+ },
+ }
+ response = geoserver.create_workspace(workspace, set_default_workspace=True)
+ assert response.status_code == 201
+ response = geoserver.create_pg_datastore(
+ workspace=workspace,
+ datastore=datastore,
+ pg_host=PGHOST,
+ pg_port=PGPORT,
+ pg_db=PGDATABASE,
+ pg_user=PGUSER,
+ pg_password=PGPASSWORD,
+ pg_schema=PGSCHEMA,
+ set_default_datastore=True,
+ )
+ assert response.status_code == 201
+ response = geoserver.create_feature_type(
+ feature_type, attributes=attributes, epsg=2056
+ )
+ assert response.status_code == 201
+
+ # Create feature
+ db_session.execute(
+ text(
+ f"INSERT INTO {feature_type} (geom, label) VALUES "
+ "(public.ST_SetSRID(public.ST_MakePoint(2600000, 1200000), 2056), 'Label')"
+ )
+ )
+ db_session.commit()
+
+ # Test that layer is published
+ response = geoserver.get_request(f"/rest/layers/{workspace}:{feature_type}.json")
+ assert response.status_code == 200
+
+ # GetFeatureInfo request
+ response = geoserver.get_feature_info(
+ layers=[feature_type],
+ bbox=(2599999.5, 1199999.5, 2600000.5, 1200000.5),
+ size=(40, 40),
+ info_format="application/json",
+ xy=(20, 20),
+ )
+
+ data = json.loads(response.read().decode("utf-8"))
+
+ feature = data.get("features", [])[0]
+ assert feature
+ assert feature.get("properties").get("label") == "Label"
+ assert feature.get("geometry") == {
+ "type": "Point",
+ "coordinates": [2600000, 1200000],
+ }
+ assert data.get("crs") == {
+ "type": "name",
+ "properties": {"name": "urn:ogc:def:crs:EPSG::2056"},
+ }
+
+ response = geoserver.delete_workspace(workspace)
+ assert response.status_code == 200
diff --git a/acceptance_tests/tests/test_workspace.py b/acceptance_tests/tests/test_workspace.py
new file mode 100644
index 000000000..23a8bc9bc
--- /dev/null
+++ b/acceptance_tests/tests/test_workspace.py
@@ -0,0 +1,23 @@
+def test_create_get_and_delete_workspace(geoserver):
+ workspace = "test_create_workspace"
+ response = geoserver.create_workspace(workspace)
+ assert response.status_code == 201
+ response = geoserver.get_request(f"/rest/workspaces/{workspace}.json")
+ assert response.status_code == 200
+ response = geoserver.publish_workspace(workspace)
+ assert response.status_code == 200
+ response = geoserver.delete_workspace(workspace)
+ assert response.status_code == 200
+
+
+def test_update_workspace(geoserver):
+ workspace = "update_workspace"
+ response = geoserver.create_workspace(workspace, isolated=True)
+ assert response.status_code == 201
+ response = geoserver.get_request(f"/rest/workspaces/{workspace}.json")
+ assert response.json().get("workspace").get("isolated") == True
+ response = geoserver.create_workspace(workspace, isolated=False)
+ assert response.status_code == 200
+ response = geoserver.get_request(f"/rest/workspaces/{workspace}.json")
+ assert response.json().get("workspace").get("isolated") == False
+ geoserver.delete_workspace(workspace)
diff --git a/ci/requirements.txt b/ci/requirements.txt
new file mode 100644
index 000000000..101fbee83
--- /dev/null
+++ b/ci/requirements.txt
@@ -0,0 +1 @@
+c2cciutils[checks]==1.6.18
diff --git a/compose/acceptance.yml b/compose/acceptance.yml
new file mode 100644
index 000000000..f1244be38
--- /dev/null
+++ b/compose/acceptance.yml
@@ -0,0 +1,111 @@
+services:
+ geodatabase:
+ image: imresamu/postgis:15-3.4
+ environment:
+ POSTGRES_DB: geodata
+ POSTGRES_USER: geodata
+ POSTGRES_PASSWORD: geodata
+ restart: always
+ volumes:
+ - ./acceptance_pg_entrypoint:/docker-entrypoint-initdb.d:ro
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U geodata"]
+ interval: 30s
+ timeout: 10s
+ retries: 5
+
+ acceptance:
+ image: acceptance:${TAG}
+ user: ${GS_USER}
+ depends_on:
+ geodatabase:
+ condition: service_healthy
+ gateway:
+ condition: service_healthy
+ discovery:
+ condition: service_healthy
+ config:
+ condition: service_healthy
+ wms:
+ condition: service_healthy
+ # TODO: add wcs and wps
+ wfs:
+ condition: service_healthy
+ gwc:
+ condition: service_healthy
+ webui:
+ condition: service_started
+ rest:
+ condition: service_healthy
+ acl:
+ condition: service_started
+
+ # For github CI
+ acl:
+ deploy:
+ resources:
+ limits:
+ cpus: "1.0"
+ memory: 512M
+ gateway:
+ deploy:
+ resources:
+ limits:
+ cpus: "1.0"
+ memory: 512M
+ discovery:
+ deploy:
+ resources:
+ limits:
+ cpus: "1.0"
+ memory: 512M
+ config:
+ deploy:
+ resources:
+ limits:
+ cpus: "1.0"
+ memory: 512M
+ wms:
+ deploy:
+ resources:
+ limits:
+ cpus: "1.0"
+ memory: 512M
+ healthcheck:
+ retries: 20
+ wfs:
+ deploy:
+ resources:
+ limits:
+ cpus: "1.0"
+ memory: 512M
+ healthcheck:
+ retries: 20
+ rest:
+ deploy:
+ resources:
+ limits:
+ cpus: "1.0"
+ memory: 512M
+ healthcheck:
+ retries: 20
+ gwc:
+ deploy:
+ resources:
+ limits:
+ cpus: "1.0"
+ memory: 512M
+ healthcheck:
+ retries: 20
+ webui:
+ deploy:
+ resources:
+ limits:
+ cpus: "1.0"
+ memory: 512M
+ rabbitmq:
+ deploy:
+ resources:
+ limits:
+ cpus: "1.0"
+ memory: 512M
diff --git a/compose/acceptance_pg_entrypoint/001_create_schemas.sql b/compose/acceptance_pg_entrypoint/001_create_schemas.sql
new file mode 100755
index 000000000..4658ff476
--- /dev/null
+++ b/compose/acceptance_pg_entrypoint/001_create_schemas.sql
@@ -0,0 +1,4 @@
+\c geodata
+CREATE SCHEMA IF NOT EXISTS test1;
+CREATE SCHEMA IF NOT EXISTS test2;
+CREATE EXTENSION IF NOT EXISTS postgis;
diff --git a/compose/compose.yml b/compose/compose.yml
index 24bf33204..1a272b5d4 100644
--- a/compose/compose.yml
+++ b/compose/compose.yml
@@ -8,6 +8,7 @@ volumes:
type: none
o: bind
device: $PWD/../config
+ driver: local
x-gs-dependencies: &gs-dependencies
rabbitmq: