Skip to content

Commit e3c5b80

Browse files
authored
improving tests (#40)
Finishing tests for most stuff that is currently implemented --------- Signed-off-by: Alexander Piskun <bigcat88@icloud.com>
1 parent 218314f commit e3c5b80

File tree

13 files changed

+191
-29
lines changed

13 files changed

+191
-29
lines changed

.github/workflows/analysis-coverage.yml

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,9 @@ jobs:
131131
cd nc_py_api
132132
coverage run --data-file=.coverage.ci_install tests/_install.py &
133133
echo $! > /tmp/_install.pid
134+
python3 tests/_install_wait.py http://127.0.0.1:$APP_PORT/heartbeat "\"status\":\"ok\"" 15 0.5
135+
python3 tests/_app_security_checks.py http://127.0.0.1:$APP_PORT
134136
cd ..
135-
sleep 5s
136137
php occ app_ecosystem_v2:daemon:register manual_install "Manual Install" manual-install 0 0 0
137138
php occ app_ecosystem_v2:app:register $APP_ID manual_install --json-info \
138139
"{\"appid\":\"$APP_ID\",\"name\":\"$APP_ID\",\"daemon_config_name\":\"manual_install\",\"version\":\"$APP_VERSION\",\"secret\":\"$APP_SECRET\",\"host\":\"localhost\",\"port\":$APP_PORT,\"protocol\":\"http\",\"system_app\":1}" \
@@ -257,8 +258,9 @@ jobs:
257258
cd nc_py_api
258259
coverage run --data-file=.coverage.ci_install tests/_install.py &
259260
echo $! > /tmp/_install.pid
261+
python3 tests/_install_wait.py http://127.0.0.1:$APP_PORT/heartbeat "\"status\":\"ok\"" 15 0.5
262+
python3 tests/_app_security_checks.py http://127.0.0.1:$APP_PORT
260263
cd ..
261-
sleep 5s
262264
php occ app_ecosystem_v2:daemon:register manual_install "Manual Install" manual-install 0 0 0
263265
php occ app_ecosystem_v2:app:register $APP_ID manual_install --json-info \
264266
"{\"appid\":\"$APP_ID\",\"name\":\"$APP_ID\",\"daemon_config_name\":\"manual_install\",\"version\":\"$APP_VERSION\",\"secret\":\"$APP_SECRET\",\"host\":\"localhost\",\"port\":$APP_PORT,\"protocol\":\"http\",\"system_app\":1}" \
@@ -376,8 +378,9 @@ jobs:
376378
cd nc_py_api
377379
coverage run --data-file=.coverage.ci_install tests/_install.py &
378380
echo $! > /tmp/_install.pid
381+
python3 tests/_install_wait.py http://127.0.0.1:$APP_PORT/heartbeat "\"status\":\"ok\"" 15 0.5
382+
python3 tests/_app_security_checks.py http://127.0.0.1:$APP_PORT
379383
cd ..
380-
sleep 5s
381384
php occ app_ecosystem_v2:daemon:register manual_install "Manual Install" manual-install 0 0 0
382385
php occ app_ecosystem_v2:app:register $APP_ID manual_install --json-info \
383386
"{\"appid\":\"$APP_ID\",\"name\":\"$APP_ID\",\"daemon_config_name\":\"manual_install\",\"version\":\"$APP_VERSION\",\"secret\":\"$APP_SECRET\",\"host\":\"localhost\",\"port\":$APP_PORT,\"protocol\":\"http\",\"system_app\":1}" \
@@ -486,8 +489,9 @@ jobs:
486489
cd nc_py_api
487490
coverage run --data-file=.coverage.ci_install tests/_install.py &
488491
echo $! > /tmp/_install.pid
492+
python3 tests/_install_wait.py http://127.0.0.1:$APP_PORT/heartbeat "\"status\":\"ok\"" 15 0.5
493+
python3 tests/_app_security_checks.py http://127.0.0.1:$APP_PORT
489494
cd ..
490-
sleep 5s
491495
php occ app_ecosystem_v2:daemon:register manual_install "Manual Install" manual-install 0 0 0
492496
php occ app_ecosystem_v2:app:register $APP_ID manual_install --json-info \
493497
"{\"appid\":\"$APP_ID\",\"name\":\"$APP_ID\",\"daemon_config_name\":\"manual_install\",\"version\":\"$APP_VERSION\",\"secret\":\"$APP_SECRET\",\"host\":\"localhost\",\"port\":$APP_PORT,\"protocol\":\"http\",\"system_app\":1}" \
@@ -586,8 +590,9 @@ jobs:
586590
cd nc_py_api
587591
coverage run --data-file=.coverage.ci_install tests/_install.py &
588592
echo $! > /tmp/_install.pid
593+
python3 tests/_install_wait.py http://127.0.0.1:$APP_PORT/heartbeat "\"status\":\"ok\"" 15 0.5
594+
python3 tests/_app_security_checks.py http://127.0.0.1:$APP_PORT
589595
cd ..
590-
sleep 5s
591596
php occ app_ecosystem_v2:daemon:register manual_install "Manual Install" manual-install 0 0 0
592597
php occ app_ecosystem_v2:app:register $APP_ID manual_install --json-info \
593598
"{\"appid\":\"$APP_ID\",\"name\":\"$APP_ID\",\"daemon_config_name\":\"manual_install\",\"version\":\"$APP_VERSION\",\"secret\":\"$APP_SECRET\",\"host\":\"localhost\",\"protocol\":\"http\",\"port\":$APP_PORT,\"system_app\":1}" \

nc_py_api/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
""" Version of nc_py_api"""
22

3-
__version__ = "0.0.24"
3+
__version__ = "0.0.25-dev"

nc_py_api/appconfig_preferences_ex.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,9 @@ def get_value(self, key: str, default=None) -> Optional[str]:
2424
if not key:
2525
raise ValueError("`key` parameter can not be empty")
2626
require_capabilities("app_ecosystem_v2", self._session.capabilities)
27-
try:
28-
r = self.get_values([key])
29-
if r:
30-
return r[0]["configvalue"]
31-
except NextcloudExceptionNotFound:
32-
pass
27+
r = self.get_values([key])
28+
if r:
29+
return r[0]["configvalue"]
3330
return default
3431

3532
def get_values(self, keys: list[str]) -> list[AppCfgPrefRecord]:

nc_py_api/misc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def kwargs_to_dict(keys: list[str], **kwargs) -> dict:
2121
def require_capabilities(capabilities: Union[str, list[str]], srv_capabilities: dict) -> None:
2222
result = check_capabilities(capabilities, srv_capabilities)
2323
if result:
24-
raise NextcloudException(404, f"{result} capability is not available")
24+
raise NextcloudException(404, f"{result} is not available")
2525

2626

2727
def check_capabilities(capabilities: Union[str, list[str]], srv_capabilities: dict) -> list[str]:

nc_py_api/theming.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class ThemingInfo(TypedDict):
2020
background_default: bool
2121

2222

23-
def __convert_str_color(theming_capability: dict, key: str) -> tuple[int, int, int]:
23+
def convert_str_color(theming_capability: dict, key: str) -> tuple[int, int, int]:
2424
if key not in theming_capability:
2525
return 0, 0, 0
2626
value = theming_capability[key]
@@ -35,11 +35,11 @@ def get_parsed_theme(theming_capability: dict) -> ThemingInfo:
3535
name=i["name"],
3636
url=i["url"],
3737
slogan=i["slogan"],
38-
color=__convert_str_color(i, "color"),
39-
color_text=__convert_str_color(i, "color-text"),
40-
color_element=__convert_str_color(i, "color-element"),
41-
color_element_bright=__convert_str_color(i, "color-element-bright"),
42-
color_element_dark=__convert_str_color(i, "color-element-dark"),
38+
color=convert_str_color(i, "color"),
39+
color_text=convert_str_color(i, "color-text"),
40+
color_element=convert_str_color(i, "color-element"),
41+
color_element_bright=convert_str_color(i, "color-element-bright"),
42+
color_element_dark=convert_str_color(i, "color-element-dark"),
4343
logo=i["logo"],
4444
background=i.get("background", ""),
4545
background_plain=i.get("background-plain", False),

nc_py_api/users_status.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from typing import Literal, Optional, TypedDict, Union
66

77
from ._session import NcSessionBasic
8-
from .exceptions import NextcloudException
8+
from .exceptions import NextcloudExceptionNotFound
99
from .misc import check_capabilities, kwargs_to_dict, require_capabilities
1010

1111
ENDPOINT = "/ocs/v1.php/apps/user_status/api/v1"
@@ -53,10 +53,8 @@ def get(self, user_id: str) -> Optional[UserStatus]:
5353
require_capabilities("user_status", self._session.capabilities)
5454
try:
5555
return self._session.ocs(method="GET", path=f"{ENDPOINT}/statuses/{user_id}")
56-
except NextcloudException as e:
57-
if e.status_code == 404:
58-
return None
59-
raise e from None
56+
except NextcloudExceptionNotFound:
57+
return None
6058

6159
def get_predefined(self) -> list[PredefinedStatus]:
6260
if self._session.nc_version["major"] < 27:

scripts/dev_register.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ echo "registering nc_py_api as an app for $1 container"
1111
NEXTCLOUD_URL="http://$2" APP_PORT=9009 APP_ID="nc_py_api" APP_SECRET="12345" APP_VERSION="1.0.0" \
1212
python3 tests/_install.py > /dev/null 2>&1 &
1313
echo $! > /tmp/_install.pid
14-
sleep 7
14+
python3 tests/_install_wait.py "http://localhost:9009/heartbeat" "\"status\":\"ok\"" 15 0.5
1515
docker exec "$1" sudo -u www-data php occ app_ecosystem_v2:app:register nc_py_api manual_install --json-info \
1616
"{\"appid\":\"nc_py_api\",\"name\":\"NC_Py_API\",\"daemon_config_name\":\"manual_install\",\"version\":\"1.0.0\",\"secret\":\"12345\",\"host\":\"host.docker.internal\",\"port\":9009,\"protocol\":\"http\",\"system_app\":1}" \
1717
-e --force-scopes

tests/_app_security_checks.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import hmac
2+
from datetime import datetime, timezone
3+
from hashlib import sha256
4+
from json import dumps
5+
from os import environ
6+
from sys import argv
7+
8+
import requests
9+
from xxhash import xxh64
10+
11+
12+
def sign_request(url: str, req_headers: dict, time: int = 0):
13+
data_hash = xxh64()
14+
req_headers["AE-DATA-HASH"] = data_hash.hexdigest()
15+
if time:
16+
req_headers["AE-SIGN-TIME"] = str(time)
17+
else:
18+
req_headers["AE-SIGN-TIME"] = str(int(datetime.now(timezone.utc).timestamp()))
19+
req_headers.pop("AE-SIGNATURE", None)
20+
request_to_sign = "PUT" + url + dumps(req_headers, separators=(",", ":"))
21+
hmac_sign = hmac.new(environ["APP_SECRET"].encode("UTF-8"), request_to_sign.encode("UTF-8"), digestmod=sha256)
22+
req_headers["AE-SIGNATURE"] = hmac_sign.hexdigest()
23+
24+
25+
# params: app base url
26+
if __name__ == "__main__":
27+
request_url = argv[1] + "/sec_check?value=1"
28+
headers = {}
29+
result = requests.put(request_url, headers=headers)
30+
assert result.status_code == 401 # Missing headers
31+
headers.update(
32+
{
33+
"AE-VERSION": environ.get("AE_VERSION", "1.0.0"),
34+
"EX-APP-ID": environ.get("APP_ID", "nc_py_api"),
35+
"EX-APP-VERSION": environ.get("APP_VERSION", "1.0.0"),
36+
}
37+
)
38+
sign_request("/sec_check?value=1", headers)
39+
result = requests.put(request_url, headers=headers)
40+
assert result.status_code == 200
41+
# Invalid AE-SIGNATURE
42+
request_url = argv[1] + "/sec_check?value=0"
43+
result = requests.put(request_url, headers=headers)
44+
assert result.status_code == 401
45+
sign_request("/sec_check?value=0", headers)
46+
result = requests.put(request_url, headers=headers)
47+
assert result.status_code == 200
48+
# Invalid EX-APP-ID
49+
old_app_name = headers["EX-APP-ID"]
50+
headers["EX-APP-ID"] = "unknown_app"
51+
sign_request("/sec_check?value=0", headers)
52+
result = requests.put(request_url, headers=headers)
53+
assert result.status_code == 401
54+
headers["EX-APP-ID"] = old_app_name
55+
sign_request("/sec_check?value=0", headers)
56+
result = requests.put(request_url, headers=headers)
57+
assert result.status_code == 200
58+
# Invalid AE-DATA-HASH
59+
result = requests.put(request_url, headers=headers, data=b"some_data")
60+
assert result.status_code == 401
61+
# Sign time
62+
sign_request("/sec_check?value=0", headers, time=int(datetime.now(timezone.utc).timestamp()))
63+
result = requests.put(request_url, headers=headers)
64+
assert result.status_code == 200
65+
sign_request("/sec_check?value=0", headers, time=int(datetime.now(timezone.utc).timestamp() - 4.0 * 60))
66+
result = requests.put(request_url, headers=headers)
67+
assert result.status_code == 200
68+
sign_request("/sec_check?value=0", headers, time=int(datetime.now(timezone.utc).timestamp() - 5.0 * 60 - 3.0))
69+
result = requests.put(request_url, headers=headers)
70+
assert result.status_code == 401
71+
sign_request("/sec_check?value=0", headers, time=int(datetime.now(timezone.utc).timestamp() + 4.0 * 60))
72+
result = requests.put(request_url, headers=headers)
73+
assert result.status_code == 200
74+
sign_request("/sec_check?value=0", headers, time=int(datetime.now(timezone.utc).timestamp() + 5.0 * 60 + 3.0))
75+
result = requests.put(request_url, headers=headers)
76+
assert result.status_code == 401

tests/_install.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,33 @@
11
from os import environ
2+
from typing import Annotated
23

34
import uvicorn
4-
from fastapi import FastAPI
5+
from fastapi import Depends, FastAPI
6+
from fastapi.responses import JSONResponse
57

68
from nc_py_api import (
79
ApiScope,
810
LogLvl,
911
NextcloudApp,
1012
enable_heartbeat,
13+
nc_app,
1114
set_enabled_handler,
1215
set_scopes,
1316
)
1417

1518
APP = FastAPI()
1619

1720

21+
@APP.put("/sec_check")
22+
def sec_check(
23+
value: int,
24+
nc: Annotated[NextcloudApp, Depends(nc_app)],
25+
):
26+
print(value)
27+
_ = nc
28+
return JSONResponse(content={"error": ""}, status_code=200)
29+
30+
1831
def enabled_handler(enabled: bool, nc: NextcloudApp) -> str:
1932
print(f"enabled_handler: enabled={enabled}", flush=True)
2033
if enabled:
@@ -24,6 +37,10 @@ def enabled_handler(enabled: bool, nc: NextcloudApp) -> str:
2437
return ""
2538

2639

40+
def heartbeat_callback():
41+
return "ok"
42+
43+
2744
@APP.on_event("startup")
2845
def initialization():
2946
set_enabled_handler(APP, enabled_handler)
@@ -41,7 +58,7 @@ def initialization():
4158
"optional": [],
4259
},
4360
)
44-
enable_heartbeat(APP)
61+
enable_heartbeat(APP, heartbeat_callback)
4562

4663

4764
if __name__ == "__main__":

tests/_install_wait.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import re
2+
from sys import argv
3+
from time import sleep
4+
5+
from requests import get
6+
7+
# params: app heartbeat url, string to check, number of tries, time to sleep between retries
8+
if __name__ == "__main__":
9+
for i in range(int(argv[3])):
10+
try:
11+
result = get(argv[1])
12+
if result.text:
13+
if re.search(str(argv[2]), result.text, re.IGNORECASE) is not None:
14+
exit(0)
15+
except Exception as _:
16+
_ = _
17+
sleep(float(argv[4]))
18+
exit(2)

0 commit comments

Comments
 (0)