Skip to content

Commit 9aaf8e9

Browse files
committed
testing(e2e): POC e2e Heureka, UI and scanner
On-behalf-of: SAP Michal Krzyz <michal.krzyz@sap.com> Signed-off-by: Michal Krzyz <michalkrzyz@gmail.com>
1 parent 6993e95 commit 9aaf8e9

11 files changed

Lines changed: 418 additions & 2 deletions

File tree

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,11 @@ build/
3333
internal/mocks/*.go
3434
internal/api/graphql/graph/model/models_gen.go
3535
internal/api/graphql/graph/generated.go
36+
37+
# python venv and compiled files
38+
.venv/
39+
*.pyc
40+
__pycache__/
41+
42+
# e2e reports
43+
e2e/out/

e2e/Makefile

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
OUTPUT_DIR:=out
2+
REPO_DIR:=`pwd`
3+
E2E_RESULTS_DIR:=${OUTPUT_DIR}/e2e/results
4+
E2E_RUNTIME_DIR:=${OUTPUT_DIR}/e2e/runtime
5+
E2E_TESTS_DIR:=tests
6+
E2E_PYTHON_VENV_DIR:=.venv
7+
E2E_REQUIREMENTS_FILE:=e2e-requirements.txt
8+
9+
INCLUDETAG_F:=
10+
ifdef TAG
11+
INCLUDETAG_F:=--include ${TAG}
12+
endif
13+
14+
test: clean
15+
@mkdir -pv ${E2E_RESULTS_DIR} ${E2E_RUNTIME_DIR}
16+
@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"
17+
18+
clean:
19+
@rm -fvr ${E2E_RESULTS_DIR} ${E2E_RUNTIME_DIR}
20+
@rm -fvr ${OUTPUT_DIR}
21+
22+
create-venv:
23+
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"
24+
25+
rm-venv:
26+
rm -fr ${E2E_PYTHON_VENV_DIR}/
27+
28+
robot-help:
29+
@sh -c ". ${E2E_PYTHON_VENV_DIR}/bin/activate && robot -h && deactivate"

e2e/e2e-requirements.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
click >= 8.0
2+
pymysql >= 1.2.0
3+
robotframework >= 4.1.3
4+
robotframework-databaselibrary >= 2.4.1
5+
robotframework-pythonlibcore >= 4.4.1
6+
robotframework-requests >= 0.9.7
7+
robotframework-seleniumlibrary >= 6.9.0
8+
selenium >= 4.25.0

e2e/resources/backend.robot

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
*** Settings ***
5+
Library RequestsLibrary
6+
7+
*** Variables ***
8+
${HEUREKA_BACKEND_URL} http://localhost:80
9+
${HEUREKA_BACKEND_GRAPHQL_ENDPOINT} /query
10+
11+
*** Keywords ***
12+
Backend health request is sent
13+
GET ${HEUREKA_BACKEND_URL}/health

e2e/resources/db.robot

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
*** Settings ***
5+
Library DatabaseLibrary
6+
Library Process
7+
8+
*** Variables ***
9+
${DB_HOST} localhost
10+
${DB_PORT} 3306
11+
${DB_NAME} heureka
12+
${DB_USER_NAME} my_username
13+
${DB_USER_PASSWORD} my_password
14+
${DB_MIGRATIONS_DIR} ${CURDIR}/../../internal/database/mariadb/migrations
15+
${MIGRATE_TOOL} migrate
16+
17+
*** Keywords ***
18+
Connection to database is established
19+
Connect to database
20+
... pymysql
21+
... ${DB_NAME}
22+
... ${DB_USER_NAME}
23+
... ${DB_USER_PASSWORD}
24+
... ${DB_HOST}
25+
... ${DB_PORT}
26+
Test teardown append Disconnect from database
27+
28+
Database migration dirty bit should be ${bitval}
29+
${result}= Query
30+
... SELECT version, dirty FROM schema_migrations
31+
Should not be empty ${result}
32+
Should be equal as strings ${result[0][1]} ${bitval}
33+
34+
User table should contain only systemuser
35+
${result}= Query
36+
... SELECT user_name FROM user;
37+
Length should be ${result} 1
38+
Should be equal ${result[0][0]} systemuser
39+
40+
All data tables should be empty
41+
${query}= Catenate
42+
... SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES
43+
... WHERE TABLE_TYPE = 'BASE TABLE' AND
44+
... TABLE_SCHEMA = 'heureka' AND
45+
... TABLE_NAME != 'schema_migrations' AND
46+
... TABLE_NAME != 'user' AND
47+
... TABLE_ROWS != 0;
48+
${result}= Query ${query}
49+
Should be empty ${result}
50+
51+
Feed database from gzip
52+
[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}
53+
[Documentation] Streams a .sql.gz file directly into the specified MariaDB database.
54+
55+
# Construct the native shell pipeline command
56+
${command}= Catenate
57+
... set -o pipefail;
58+
... gunzip -c "${gzip_path}" |
59+
... mariadb -u"${db_user}" -P"${db_port}" -p"${db_password}" -h"${db_host}" "${db_name}"
60+
61+
# shell=True is mandatory to allow the pipe (|) character to function
62+
${result}= Run process ${command} shell=True stderr=STDOUT
63+
64+
# Validate that the import executed successfully
65+
Log ${result.stdout}
66+
Should be equal as integers ${result.rc} 0
67+
... Database import failed with error: ${result.stdout}
68+
69+
Reload database schema
70+
Connect to database
71+
... pymysql
72+
... ${DB_NAME}
73+
... ${DB_USER_NAME}
74+
... ${DB_USER_PASSWORD}
75+
... ${DB_HOST}
76+
... ${DB_PORT}
77+
78+
Execute sql string DROP DATABASE IF EXISTS ${DB_NAME};
79+
Execute sql string CREATE DATABASE ${DB_NAME};
80+
81+
Disconnect from database
82+
83+
Run database up migrations
84+
[Arguments] ${db_user}=${DB_USER_NAME} ${db_password}=${DB_USER_PASSWORD} ${db_name}=${DB_NAME} ${db_host}=${DB_HOST} ${db_port}=${DB_PORT}
85+
${result}= Run process
86+
... ${MIGRATE_TOOL}
87+
... -path
88+
... ${DB_MIGRATIONS_DIR}
89+
... -database
90+
... 'mysql://${db_user}:${db_password}@tcp(${db_host}:${db_port})/${db_name}'
91+
... up
92+
... shell=True
93+
... stderr=STDOUT
94+
Should be equal as integers ${result.rc} 0
95+
... Database up migration failed with error: ${result.stdout}
96+
97+
Run database down migrations
98+
[Arguments] ${db_user}=${DB_USER_NAME} ${db_password}=${DB_USER_PASSWORD} ${db_name}=${DB_NAME} ${db_host}=${DB_HOST} ${db_port}=${DB_PORT}
99+
${result}= Run process
100+
... ${MIGRATE_TOOL}
101+
... -path
102+
... ${DB_MIGRATIONS_DIR}
103+
... -database
104+
... 'mysql://${db_user}:${db_password}@tcp(${db_host}:${db_port})/${db_name}'
105+
... down
106+
... -all
107+
... shell=True
108+
... stderr=STDOUT
109+
Should be equal as integers ${result.rc} 0
110+
... Database up migration failed with error: ${result.stdout}
111+
112+
Clear database
113+
Reload database schema
114+
Run database up migrations

e2e/resources/graphql.robot

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
*** Settings ***
5+
Library RequestsLibrary
6+
7+
*** Variables ***
8+
9+
*** Keywords ***
10+
Get All User Names
11+
[Documentation] Iterates through Heureka pages using nextPageAfter to compile a flat list of names.
12+
Create Session heureka_session ${HEUREKA_BACKEND_URL} verify=True
13+
14+
${master_name_list}= Create List
15+
${has_next}= Set Variable ${TRUE}
16+
${cursor}= Set Variable ${NONE}
17+
18+
${query_string}= Catenate SEPARATOR=\n
19+
... query ($filter: UserFilter, $first: Int, $after: String) {
20+
... Users (
21+
... filter: $filter,
22+
... first: $first,
23+
... after: $after
24+
... ) {
25+
... totalCount
26+
... pageInfo {
27+
... hasNextPage
28+
... nextPageAfter
29+
... }
30+
... edges {
31+
... node {
32+
... name
33+
... }
34+
... }
35+
... }
36+
... }
37+
38+
39+
WHILE ${has_next}
40+
${empty_list}= Create List
41+
${user_filter}= Create Dictionary userName=${empty_list}
42+
43+
# Pass the dynamic cursor token as the $after variable
44+
${variables}= Create Dictionary filter=${user_filter} first=${10} after=${cursor}
45+
${payload}= Create Dictionary query=${query_string} variables=${variables}
46+
${headers}= Create Dictionary Content-Type=application/json Accept=application/json
47+
48+
${response}= POST On Session heureka_session ${HEUREKA_BACKEND_GRAPHQL_ENDPOINT} json=${payload} headers=${headers}
49+
Status Should Be 200 ${response}
50+
${json_res}= Set Variable ${response.json()}
51+
52+
# 2. FIXED PARSING LOCATIONS: Extract variables using the correct nextPageAfter map pointer
53+
${has_next}= Set Variable ${json_res['data']['Users']['pageInfo']['hasNextPage']}
54+
${cursor}= Set Variable ${json_res['data']['Users']['pageInfo']['nextPageAfter']}
55+
56+
# Append this page's chunk straight to the master array tracking list
57+
${edges}= Set Variable ${json_res['data']['Users']['edges']}
58+
FOR ${edge} IN @{edges}
59+
Append To List ${master_name_list} ${edge['node']['name']}
60+
END
61+
END
62+
63+
RETURN ${master_name_list}

e2e/resources/shadow.robot

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
*** Settings ***
5+
Library SeleniumLibrary
6+
7+
*** Keywords ***
8+
Wait Until Shadow Element Is Visible
9+
[Arguments] ${selector} ${timeout}=10s
10+
11+
Wait Until Keyword Succeeds ${timeout} 500ms
12+
... Shadow Element Should Exist ${selector}
13+
14+
Shadow Element Should Exist
15+
[Arguments] ${selector}
16+
17+
${found}= Execute Javascript
18+
... return document.querySelector('[data-shadow-host="true"]').shadowRoot.querySelector('${selector}') !== null
19+
20+
Should Be True ${found}

e2e/resources/teardown.robot

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
*** Settings ***
5+
Library Collections
6+
7+
*** Keywords ***
8+
Test teardown init
9+
[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
10+
Variable should not exist ${test_teardown_stack} Test teardown init: test_teardown_stack already defined!!!
11+
@{test_teardown_stack} Create list
12+
Set test variable ${test_teardown_stack}
13+
14+
Test teardown run
15+
[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.
16+
${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.
17+
Return from keyword if ${status} != True
18+
Log ============== Test teardown run start ===============
19+
FOR ${item} IN @{test_teardown_stack}
20+
${status} ${return_value} Run keyword and ignore error @{item}
21+
Log ${item[0]} status:::${status}, return:::${return_value}
22+
END
23+
Log ============== Test teardown run end ===============
24+
25+
Test teardown append
26+
[Arguments] @{keyword_and_args}
27+
[Documentation] *@{keyword_and_args}*: the list of keyword and its arguments
28+
Variable should exist ${test_teardown_stack} Test teardown add: test_teardown_stack not defined!!! Run 'Test teardown init' in setup or case.
29+
@{kw_args} Create list @{keyword_and_args}
30+
Append To List ${test_teardown_stack} ${kw_args}
31+
32+
Test teardown add head
33+
[Arguments] @{keyword_and_args}
34+
[Documentation] *@{keyword_and_args}*: the list of keyword and its arguments
35+
Variable should exist ${test_teardown_stack} Test teardown add head: test_teardown_stack not defined!!! Run 'Test teardown init' in setup or case.
36+
@{kw_args} Create list @{keyword_and_args}
37+
Insert into list ${test_teardown_stack} 0 ${kw_args}
38+
39+
Test teardown insert
40+
[Arguments] ${insert_position}=0 @{keyword_and_args}
41+
[Documentation] *${insert_position}*: like 0 (the first), -1(the reversed second), -2(the reversed third), and so on.
42+
...
43+
... *@{keyword_and_args}*: the list of keyword and its arguments
44+
Variable should exist ${test_teardown_stack} Test teardown insert: test_teardown_stack not defined!!! Run 'Test teardown init' in setup or case.
45+
@{kw_args} Create list @{keyword_and_args}
46+
Insert into list ${test_teardown_stack} ${insert_position} ${kw_args}
47+
48+
Keyword teardown init
49+
[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
50+
#Variable should not exist ${keyword_teardown_queue} Keyword teardown init: keyword_teardown_stack already defined!!!
51+
@{keyword_teardown_queue} Create list
52+
Set test variable ${keyword_teardown_queue}
53+
54+
Keyword teardown run
55+
[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.
56+
${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.
57+
Return from keyword if ${status} != True
58+
Log ============== Test teardown run start ===============
59+
FOR ${item} IN @{keyword_teardown_queue}
60+
${status} ${return_value} Run keyword and ignore error @{item}
61+
Log ${item[0]} status:::${status}, return:::${return_value}
62+
END
63+
Log ============== Test teardown run end ===============
64+
@{keyword_teardown_queue} Create list
65+
66+
Keyword teardown add head
67+
[Arguments] @{keyword_and_args}
68+
[Documentation] *@{keyword_and_args}*: the list of keyword and its arguments
69+
Variable should exist ${keyword_teardown_queue} Keyword teardown add head: keyword_teardown_queue \ not defined!!! Run 'Keyword teardown init' in setup or case.
70+
@{kw_args} Create list @{keyword_and_args}
71+
Insert Into List ${keyword_teardown_queue} 0 ${kw_args}
72+
73+
Keyword teardown append
74+
[Arguments] @{keyword_and_args}
75+
[Documentation] *@{keyword_and_args}*: the list of keyword and its arguments
76+
Variable should exist ${keyword_teardown_queue} Keyword teardown append: keyword_teardown_queue not defined!!! Run 'Keyword teardown init' in setup or case.
77+
@{kw_args} Create list @{keyword_and_args}
78+
Append To List ${keyword_teardown_queue} ${kw_args}
79+
80+
Keyword teardown insert
81+
[Arguments] ${insert_position}=0 @{keyword_and_args}
82+
[Documentation] *${insert_position}*: like 0 (the first), -1(the reversed second), -2(the reversed third), and so on.
83+
...
84+
... *@{keyword_and_args}*: the list of keyword and its arguments
85+
Variable should exist ${keyword_teardown_queue} Keyword teardown insert: keyword_teardown_queue \ not defined!!! Run 'Keyword teardown init' in setup or case.
86+
@{kw_args} Create list @{keyword_and_args}
87+
Insert into list ${keyword_teardown_queue} ${insert_position} ${kw_args}

e2e/resources/ui.robot

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Greenhouse contributors
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
*** Settings ***
5+
Library SeleniumLibrary
6+
7+
Resource shadow.robot
8+
9+
*** Variables ***
10+
${HEUREKA_UI_URL} http://localhost:3000
11+
12+
${BROWSER} headlessfirefox
13+
#${BROWSER} firefox
14+
15+
*** Keywords ***
16+
Open browser to Heureka UI
17+
Open browser ${HEUREKA_UI_URL} ${BROWSER}
18+
Maximize browser window
19+
Test teardown append Close Browser
20+
21+
Wait for heureka UI logo
22+
Wait until shadow element is visible [data-testid="default-logo"]

0 commit comments

Comments
 (0)