Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,11 @@ build/
internal/mocks/*.go
internal/api/graphql/graph/model/models_gen.go
internal/api/graphql/graph/generated.go

# python venv and compiled files
.venv/
*.pyc
__pycache__/

# e2e reports
e2e/out/
29 changes: 29 additions & 0 deletions e2e/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
OUTPUT_DIR:=out
REPO_DIR:=`pwd`
E2E_RESULTS_DIR:=${OUTPUT_DIR}/e2e/results
E2E_RUNTIME_DIR:=${OUTPUT_DIR}/e2e/runtime
E2E_TESTS_DIR:=tests
E2E_PYTHON_VENV_DIR:=.venv
E2E_REQUIREMENTS_FILE:=e2e-requirements.txt

INCLUDETAG_F:=
ifdef TAG
INCLUDETAG_F:=--include ${TAG}
endif

test: clean
@mkdir -pv ${E2E_RESULTS_DIR} ${E2E_RUNTIME_DIR}
@sh -c "export PATH=\"\$$PATH:\$$HOME/go/bin\" && . ${E2E_PYTHON_VENV_DIR}/bin/activate && cd ${E2E_RUNTIME_DIR} && robot ${ROBOT_FLAGS} --pythonpath ${REPO_DIR}/tests ${INCLUDETAG_F} --outputdir ${REPO_DIR}/${E2E_RESULTS_DIR} ${REPO_DIR}/${E2E_TESTS_DIR};deactivate"

clean:
@rm -fvr ${E2E_RESULTS_DIR} ${E2E_RUNTIME_DIR}
@rm -fvr ${OUTPUT_DIR}

create-venv:
sh -c "python3 -m venv ${E2E_PYTHON_VENV_DIR} && . ${E2E_PYTHON_VENV_DIR}/bin/activate && pip install --upgrade pip && pip install -r ${E2E_REQUIREMENTS_FILE} && deactivate"

rm-venv:
rm -fr ${E2E_PYTHON_VENV_DIR}/

robot-help:
@sh -c ". ${E2E_PYTHON_VENV_DIR}/bin/activate && robot -h && deactivate"
8 changes: 8 additions & 0 deletions e2e/e2e-requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
click >= 8.0
pymysql >= 1.2.0
robotframework >= 4.1.3
robotframework-databaselibrary >= 2.4.1
robotframework-pythonlibcore >= 4.4.1
robotframework-requests >= 0.9.7
robotframework-seleniumlibrary >= 6.9.0
selenium >= 4.25.0
13 changes: 13 additions & 0 deletions e2e/resources/backend.robot
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors
# SPDX-License-Identifier: Apache-2.0

*** Settings ***
Library RequestsLibrary

*** Variables ***
${HEUREKA_BACKEND_URL} http://localhost:80
${HEUREKA_BACKEND_GRAPHQL_ENDPOINT} /query

*** Keywords ***
Backend health request is sent
GET ${HEUREKA_BACKEND_URL}/health
114 changes: 114 additions & 0 deletions e2e/resources/db.robot
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors
# SPDX-License-Identifier: Apache-2.0

*** Settings ***
Library DatabaseLibrary
Library Process

*** Variables ***
${DB_HOST} localhost
${DB_PORT} 3306
${DB_NAME} heureka
${DB_USER_NAME} my_username
${DB_USER_PASSWORD} my_password
${DB_MIGRATIONS_DIR} ${CURDIR}/../../internal/database/mariadb/migrations
${MIGRATE_TOOL} migrate

*** Keywords ***
Connection to database is established
Connect to database
... pymysql
... ${DB_NAME}
... ${DB_USER_NAME}
... ${DB_USER_PASSWORD}
... ${DB_HOST}
... ${DB_PORT}
Test teardown append Disconnect from database

Database migration dirty bit should be ${bitval}
${result}= Query
... SELECT version, dirty FROM schema_migrations
Should not be empty ${result}
Should be equal as strings ${result[0][1]} ${bitval}

User table should contain only systemuser
${result}= Query
... SELECT user_name FROM user;
Length should be ${result} 1
Should be equal ${result[0][0]} systemuser

All data tables should be empty
${query}= Catenate
... SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES
... WHERE TABLE_TYPE = 'BASE TABLE' AND
... TABLE_SCHEMA = 'heureka' AND
... TABLE_NAME != 'schema_migrations' AND
... TABLE_NAME != 'user' AND
... TABLE_ROWS != 0;
${result}= Query ${query}
Should be empty ${result}

Feed database from gzip
[Arguments] ${gzip_path} ${db_user}=${DB_USER_NAME} ${db_password}=${DB_USER_PASSWORD} ${db_name}=${DB_NAME} ${db_host}=${DB_HOST} ${db_port}=${DB_PORT}
[Documentation] Streams a .sql.gz file directly into the specified MariaDB database.

# Construct the native shell pipeline command
${command}= Catenate
... set -o pipefail;
... gunzip -c "${gzip_path}" |
... mariadb -u"${db_user}" -P"${db_port}" -p"${db_password}" -h"${db_host}" "${db_name}"

# shell=True is mandatory to allow the pipe (|) character to function
${result}= Run process ${command} shell=True stderr=STDOUT

# Validate that the import executed successfully
Log ${result.stdout}
Should be equal as integers ${result.rc} 0
... Database import failed with error: ${result.stdout}

Reload database schema
Connect to database
... pymysql
... ${DB_NAME}
... ${DB_USER_NAME}
... ${DB_USER_PASSWORD}
... ${DB_HOST}
... ${DB_PORT}

Execute sql string DROP DATABASE IF EXISTS ${DB_NAME};
Execute sql string CREATE DATABASE ${DB_NAME};

Disconnect from database

Run database up migrations
[Arguments] ${db_user}=${DB_USER_NAME} ${db_password}=${DB_USER_PASSWORD} ${db_name}=${DB_NAME} ${db_host}=${DB_HOST} ${db_port}=${DB_PORT}
${result}= Run process
... ${MIGRATE_TOOL}
... -path
... ${DB_MIGRATIONS_DIR}
... -database
... 'mysql://${db_user}:${db_password}@tcp(${db_host}:${db_port})/${db_name}'
... up
... shell=True
... stderr=STDOUT
Should be equal as integers ${result.rc} 0
... Database up migration failed with error: ${result.stdout}

Run database down migrations
[Arguments] ${db_user}=${DB_USER_NAME} ${db_password}=${DB_USER_PASSWORD} ${db_name}=${DB_NAME} ${db_host}=${DB_HOST} ${db_port}=${DB_PORT}
${result}= Run process
... ${MIGRATE_TOOL}
... -path
... ${DB_MIGRATIONS_DIR}
... -database
... 'mysql://${db_user}:${db_password}@tcp(${db_host}:${db_port})/${db_name}'
... down
... -all
... shell=True
... stderr=STDOUT
Should be equal as integers ${result.rc} 0
... Database up migration failed with error: ${result.stdout}

Clear database
Reload database schema
Run database up migrations
91 changes: 91 additions & 0 deletions e2e/resources/graphql.robot
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors
# SPDX-License-Identifier: Apache-2.0

*** Settings ***
Library RequestsLibrary

*** Variables ***

*** Keywords ***
Get all user names
[Documentation] Iterates through Heureka pages using nextPageAfter to compile a flat list of names.
Create Session heureka_session ${HEUREKA_BACKEND_URL} verify=True

${master_name_list}= Create list
${has_next}= Set variable ${TRUE}
${cursor}= Set variable ${NONE}

${query_string}= Catenate SEPARATOR=\n
... query ($filter: UserFilter, $first: Int, $after: String) {
... Users (
... filter: $filter,
... first: $first,
... after: $after
... ) {
... totalCount
... pageInfo {
... hasNextPage
... nextPageAfter
... }
... edges {
... node {
... name
... }
... }
... }
... }


WHILE ${has_next}
${empty_list}= Create List
${user_filter}= Create dictionary userName=${empty_list}

# Pass the dynamic cursor token as the $after variable
${variables}= Create dictionary filter=${user_filter} first=${10} after=${cursor}
${payload}= Create dictionary query=${query_string} variables=${variables}
${headers}= Create dictionary Content-Type=application/json Accept=application/json

${response}= POST on session heureka_session ${HEUREKA_BACKEND_GRAPHQL_ENDPOINT} json=${payload} headers=${headers}
Status should be 200 ${response}
${json_res}= Set variable ${response.json()}

# 2. FIXED PARSING LOCATIONS: Extract variables using the correct nextPageAfter map pointer
${has_next}= Set variable ${json_res['data']['Users']['pageInfo']['hasNextPage']}
${cursor}= Set variable ${json_res['data']['Users']['pageInfo']['nextPageAfter']}

# Append this page's chunk straight to the master array tracking list
${edges}= Set variable ${json_res['data']['Users']['edges']}
FOR ${edge} IN @{edges}
Append to list ${master_name_list} ${edge['node']['name']}
END
END

RETURN ${master_name_list}

Create service
[Arguments] ${ccrn} ${domain} ${region}
Create Session heureka_session ${HEUREKA_BACKEND_URL} verify=True

${mutation_string}= Catenate SEPARATOR=\n
... mutation ($input: ServiceInput!) {
... createService (
... input: $input
... ) {
... id
... ccrn
... domain
... region
... }
... }

${service_input}= Create dictionary ccrn=${ccrn} domain=${domain} region=${region}

${variables}= Create dictionary input=${service_input}
${payload}= Create dictionary query=${mutation_string} variables=${variables}
${headers}= Create dictionary Content-Type=application/json Accept=application/json

${response}= POST on session heureka_session ${HEUREKA_BACKEND_GRAPHQL_ENDPOINT} json=${payload} headers=${headers}
Status should be 200 ${response}
${json_res}= Set variable ${response.json()}

Return from keyword ${json_res['data']['createService']['id']}
32 changes: 32 additions & 0 deletions e2e/resources/shadow.robot
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors
# SPDX-License-Identifier: Apache-2.0

*** Settings ***
Library SeleniumLibrary

*** Keywords ***
Wait until shadow element is visible
[Arguments] ${selector} ${timeout}=10s
Wait until keyword succeeds ${timeout} 500ms
... Shadow element should exist ${selector}

Wait for shadow element
[Arguments] ${selector} ${timeout}=10s
${element}= Wait until keyword succeeds ${timeout} 500ms
... Get shadow element ${selector}
Return from keyword ${element}

Shadow element should exist
[Arguments] ${selector}

${found}= Execute javascript
... return document.querySelector('[data-shadow-host="true"]').shadowRoot.querySelector('${selector}') !== null

Should Be True ${found}

Get shadow element
[Arguments] ${selector}
${element}= Execute javascript
... return document.querySelector('[data-shadow-host="true"]').shadowRoot.querySelector('${selector}')
Run keyword if ${{ $element is None }} Fail Element ('${selector}') not found
Return from keyword ${element}
87 changes: 87 additions & 0 deletions e2e/resources/teardown.robot
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors
# SPDX-License-Identifier: Apache-2.0

*** Settings ***
Library Collections

*** Keywords ***
Test teardown init
[Documentation] Initialize a list of teardown actions which can be executed by 'Test teardown run' automatically as long as it is put into Test teardown field. Run this init in setup or case steps
Variable should not exist ${test_teardown_stack} Test teardown init: test_teardown_stack already defined!!!
@{test_teardown_stack} Create list
Set test variable ${test_teardown_stack}

Test teardown run
[Documentation] Get list objects from list '*${test_teardown_stack}*', and then execute these list objects (keyword and its arguments) one by one. Failure will be ignored so all list object can be executed.
${status} Run keyword and return status Variable should exist ${test_teardown_stack} Test teardown run: test_teardown_stack not defined!!! Run 'Test teardown init' in setup or case.
Return from keyword if ${status} != True
Log ============== Test teardown run start ===============
FOR ${item} IN @{test_teardown_stack}
${status} ${return_value} Run keyword and ignore error @{item}
Log ${item[0]} status:::${status}, return:::${return_value}
END
Log ============== Test teardown run end ===============

Test teardown append
[Arguments] @{keyword_and_args}
[Documentation] *@{keyword_and_args}*: the list of keyword and its arguments
Variable should exist ${test_teardown_stack} Test teardown add: test_teardown_stack not defined!!! Run 'Test teardown init' in setup or case.
@{kw_args} Create list @{keyword_and_args}
Append To List ${test_teardown_stack} ${kw_args}

Test teardown add head
[Arguments] @{keyword_and_args}
[Documentation] *@{keyword_and_args}*: the list of keyword and its arguments
Variable should exist ${test_teardown_stack} Test teardown add head: test_teardown_stack not defined!!! Run 'Test teardown init' in setup or case.
@{kw_args} Create list @{keyword_and_args}
Insert into list ${test_teardown_stack} 0 ${kw_args}

Test teardown insert
[Arguments] ${insert_position}=0 @{keyword_and_args}
[Documentation] *${insert_position}*: like 0 (the first), -1(the reversed second), -2(the reversed third), and so on.
...
... *@{keyword_and_args}*: the list of keyword and its arguments
Variable should exist ${test_teardown_stack} Test teardown insert: test_teardown_stack not defined!!! Run 'Test teardown init' in setup or case.
@{kw_args} Create list @{keyword_and_args}
Insert into list ${test_teardown_stack} ${insert_position} ${kw_args}

Keyword teardown init
[Documentation] Initialize a list of teardown actions which can be executed by 'Keyword Teardown Run' automatically as long as it is put into Keyword's Teardown field. Run this init in keyword steps
#Variable should not exist ${keyword_teardown_queue} Keyword teardown init: keyword_teardown_stack already defined!!!
@{keyword_teardown_queue} Create list
Set test variable ${keyword_teardown_queue}

Keyword teardown run
[Documentation] Get list objects from list '*${keyword_teardown_stack}*', and then execute these list objects (keyword and its arguments) one by one. Failure will be ignored so all list object can be executed.
${status} Run keyword and return status Variable should exist ${keyword_teardown_queue} Keyword teardown Run: keyword_teardown_queue not defined!!! Run 'Keyword teardown init' in setup or case.
Return from keyword if ${status} != True
Log ============== Test teardown run start ===============
FOR ${item} IN @{keyword_teardown_queue}
${status} ${return_value} Run keyword and ignore error @{item}
Log ${item[0]} status:::${status}, return:::${return_value}
END
Log ============== Test teardown run end ===============
@{keyword_teardown_queue} Create list

Keyword teardown add head
[Arguments] @{keyword_and_args}
[Documentation] *@{keyword_and_args}*: the list of keyword and its arguments
Variable should exist ${keyword_teardown_queue} Keyword teardown add head: keyword_teardown_queue \ not defined!!! Run 'Keyword teardown init' in setup or case.
@{kw_args} Create list @{keyword_and_args}
Insert Into List ${keyword_teardown_queue} 0 ${kw_args}

Keyword teardown append
[Arguments] @{keyword_and_args}
[Documentation] *@{keyword_and_args}*: the list of keyword and its arguments
Variable should exist ${keyword_teardown_queue} Keyword teardown append: keyword_teardown_queue not defined!!! Run 'Keyword teardown init' in setup or case.
@{kw_args} Create list @{keyword_and_args}
Append To List ${keyword_teardown_queue} ${kw_args}

Keyword teardown insert
[Arguments] ${insert_position}=0 @{keyword_and_args}
[Documentation] *${insert_position}*: like 0 (the first), -1(the reversed second), -2(the reversed third), and so on.
...
... *@{keyword_and_args}*: the list of keyword and its arguments
Variable should exist ${keyword_teardown_queue} Keyword teardown insert: keyword_teardown_queue \ not defined!!! Run 'Keyword teardown init' in setup or case.
@{kw_args} Create list @{keyword_and_args}
Insert into list ${keyword_teardown_queue} ${insert_position} ${kw_args}
Loading
Loading