From 5160fd677e714a470d3d13c837930dba60cf68e4 Mon Sep 17 00:00:00 2001 From: piscione Date: Fri, 14 Jun 2024 12:32:58 +0200 Subject: [PATCH] Fixed some TCs of MEC010p2 --- MEC010p2/MEPM/PKGM/AppPkgMgt.robot | 196 +++++++++++++++++- MEC010p2/MEPM/PKGM/environment/variables.txt | 10 +- .../MEPM/PKGM/jsons/AppPkgNotification.json | 17 ++ MEC010p2/MEPM/PKGM/libraries/Server.py | 144 +++++++++++++ .../__pycache__/Server.cpython-39.pyc | Bin 0 -> 5554 bytes .../schemas/AppPkgNotification.schema.json | 68 ++++++ 6 files changed, 424 insertions(+), 11 deletions(-) create mode 100644 MEC010p2/MEPM/PKGM/jsons/AppPkgNotification.json create mode 100644 MEC010p2/MEPM/PKGM/libraries/Server.py create mode 100644 MEC010p2/MEPM/PKGM/libraries/__pycache__/Server.cpython-39.pyc create mode 100644 MEC010p2/MEPM/PKGM/schemas/AppPkgNotification.schema.json diff --git a/MEC010p2/MEPM/PKGM/AppPkgMgt.robot b/MEC010p2/MEPM/PKGM/AppPkgMgt.robot index 2da898f..711a7e5 100644 --- a/MEC010p2/MEPM/PKGM/AppPkgMgt.robot +++ b/MEC010p2/MEPM/PKGM/AppPkgMgt.robot @@ -8,7 +8,7 @@ Library REST ${MEPM_SCHEMA}://${MEPM_HOST}:${MEPM_PORT} ssl_verify=fa Library BuiltIn Library OperatingSystem Resource ../../../pics.txt - +Library libraries/Server.py *** Test Cases *** TC_MEC_MEC010p2_MEPM_PKGM_001_01_OK @@ -28,7 +28,7 @@ TC_MEC_MEC010p2_MEPM_PKGM_001_01_OK [Teardown] Delete an individual APP Package identified by ID ${APP_PKG_ID} -TC_MEC_MEC010p2_MEPM_PKGM_002_01_OK +TC_MEC_MEC010p2_MEPM_PKGM_001_02_OK [Documentation] TP_MEC_MEC010p2_MEPM_PKGM_002_01_OK ... Check that MEPM returns the list of on-boarded App Packages when requested - Note 3 ... ETSI GS MEC 010-2 3.1.1, clause 7.3.1.3.1 @@ -187,7 +187,7 @@ TC_MEC_MEC010p2_MEPM_PKGM_006_OK Delete a subscription ${SUBSCRIPTION_ID} Check HTTP Response Status Code Is 204 -TC_MEC_MEC010p2_MEPM_PKGM_006_OK +TC_MEC_MEC010p2_MEPM_PKGM_006_NF [Documentation] TP_MEC_MEC010p2_MEPM_PKGM_006_NF ... Check that MEPM service sends an error ... when it receives a deletion request for a subscription on AppPackages @@ -198,8 +198,177 @@ TC_MEC_MEC010p2_MEPM_PKGM_006_OK Delete a subscription ${NON_EXISTENT_SUBSCRIPTION_ID} Check HTTP Response Status Code Is 404 + +TC_MEC_MEC010p2_MEPM_PKGM_007_OK + [Documentation] TP_MEC_MEC010p2_MEPM_PKGM_007_OK + ... Check that the MEPM service sends a application package notification + ... if the MEPM service has an associated subscription and the event is generated + ... ETSI GS MEC 010-2 3.1.1, clause 7.3.5.3.1, + ... ETSI GS MEC 010-2 3.1.1, clause 6.2.3.6.2 ##AppPkgNotification + [Tags] PIC_APP_PACKAGE_NOTIFICATIONS + [Setup] Create a subscription AppPkgSubscription.json + Set Suite Variable ${SUBSCRIPTION_ID} ${response['body']['id']} + Spawn Notification Server AppPkgNotification + Validate Json AppPkgNotification.schema.json ${payload_notification} + [TearDown] Delete a subscription ${SUBSCRIPTION_ID} + + + + +TC_MEC_MEC010p2_MEPM_PKGM_008_NA + [Documentation] TP_MEC_MEC010p2_MEPM_PKGM_008_NA + ... Check that MEPM responds with an error when it receives + ... a POST request referring an application descriptor AppD + ... ETSI GS MEC 010-2 3.1.1, clause 7.3.6.3.1 + + [Tags] PIC_APP_PACKAGE_MANAGEMENT + [Setup] Create new App Package CreateAppPackage.json + Set Suite Variable ${APP_PKG_ID} ${response['body']['id']} + Set Suite Variable ${APPD_ID} ${response['body']['appDId']} + Post an AppD identified by ${APP_PKG_ID} + Check HTTP Response Status Code Is 405 + [Teardown] Delete an individual APP Package identified by ID ${APP_PKG_ID} + + + + + +TC_MEC_MEC010p2_MEPM_PKGM_009_OK + [Documentation] TP_MEC_MEC010p2_MEPM_PKGM_009_OK + ... Check that MEPM returns the Application Descriptor contained on an on-boarded Application Package when requested + ... ETSI GS MEC 010-2 3.1.1, clause 7.3.6.3.2 + ... ETSI GS MEC 010-2 3.1.1, clause 6.2.1.2.2 ##AppD + [Tags] PIC_APP_PACKAGE_MANAGEMENT + [Setup] Create new App Package CreateAppPackage.json + Set Suite Variable ${APP_PKG_ID} ${response['body']['id']} + Set Suite Variable ${ON_BOARDED_APPD_ID} ${response['body']['appDId']} + Get an AppD identified by ${ON_BOARDED_APPD_ID} + Check HTTP Response Status Code Is 200 + [Teardown] Delete an individual APP Package identified by ID ${APP_PKG_ID} + + +TC_MEC_MEC010p2_MEPM_PKGM_009_NF + [Documentation] TP_MEC_MEC010p2_MEPM_PKGM_009_NF + ... Check that MEPM responds with an error when it receives + ... a request for returning a App Descriptor referred with a wrong App Package ID + ... ETSI GS MEC 010-2 3.1.1, clause 7.3.6.3.2 + [Tags] PIC_APP_PACKAGE_MANAGEMENT + [Setup] Delete an AppD by ID ${NON_EXISTENT_APP_PKG_ID} + Get an AppD identified by ${NON_EXISTENT_APP_PKG_ID} + Check HTTP Response Status Code Is 404 + + +TC_MEC_MEC010p2_MEPM_PKGM_010_FO + [Documentation] TP_MEC_MEC010p2_MEPM_PKGM_010_FO + ... Check that MEPM responds with an error when it receives + ... a PUT request referring an application descriptor AppD + ... ETSI GS MEC 010-2 3.1.1, clause 7.3.6.3.3 + [Tags] PIC_APP_PACKAGE_MANAGEMENT + [Setup] Create new App Package CreateAppPackage.json + Set Suite Variable ${APP_PKG_ID} ${response['body']['id']} + Set Suite Variable ${ON_BOARDED_APPD_ID} ${response['body']['appDId']} + Update an AppD identified by ${ON_BOARDED_APPD_ID} + Check HTTP Response Status Code Is 403 + [Teardown] Delete an individual APP Package identified by ID ${APP_PKG_ID} + + +TC_MEC_MEC010p2_MEPM_PKGM_011_NA + [Documentation] TP_MEC_MEC010p2_MEPM_PKGM_011_NA + ... Check that MEPM responds with an error when it receives + ... a DELETE request referring an application descriptor AppD + ... ETSI GS MEC 010-2 3.1.1, clause 7.3.6.3.4 + [Tags] PIC_APP_PACKAGE_MANAGEMENT + [Setup] Create new App Package CreateAppPackage.json + Set Suite Variable ${APP_PKG_ID} ${response['body']['id']} + Delete an AppD by ID ${APP_PKG_ID} + Check HTTP Response Status Code Is 405 + [Teardown] Delete an individual APP Package identified by ID ${APP_PKG_ID} + + + +TC_MEC_MEC010p2_MEPM_PKGM_012_01_OK + [Documentation] TP_MEC_MEC010p2_MEPM_PKGM_012_01_OK + ... Check that MEPM fetches the on-boarded application package content identified by appPkgId when requested + ... ETSI GS MEC 010-2 3.1.1, clause 7.3.7.3.2 + [Tags] PIC_APP_PACKAGE_MANAGEMENT + [Setup] Create new App Package CreateAppPackage.json + Set Suite Variable ${APP_PKG_ID} ${response['body']['id']} + GET all app Packages content by appPkgId ${APP_PKG_ID} + Check HTTP Response Status Code Is 200 + [Teardown] Delete an individual APP Package identified by ID ${APP_PKG_ID} + +TC_MEC_MEC010p2_MEPM_PKGM_012_02_OK + [Documentation] TP_MEC_MEC010p2_MEPM_PKGM_012_02_OK + ... heck that MEPM fetches the on-boarded application package content identified by appDId when requested + ... ETSI GS MEC 010-2 3.1.1, clause 7.3.7.3.2 + [Tags] PIC_APP_PACKAGE_MANAGEMENT + [Setup] Create new App Package CreateAppPackage.json + Set Suite Variable ${APP_PKG_ID} ${response['body']['id']} + Set Suite Variable ${ON_BOARDED_APPD_ID} ${response['body']['appDId']} + GET all app Packages content by appPkgId ${ON_BOARDED_APPD_ID} + Check HTTP Response Status Code Is 200 + [Teardown] Delete an individual APP Package identified by ID ${APP_PKG_ID} + + + +TC_MEC_MEC010p2_MEPM_PKGM_012_01_NF + [Documentation] TP_MEC_MEC010p2_MEPM_PKGM_012_01_NF + ... Check that MEPM fetches the on-boarded application package content identified by appPkgId when requested + ... ETSI GS MEC 010-2 3.1.1, clause 7.3.7.3.2 + [Tags] PIC_APP_PACKAGE_MANAGEMENT + [Setup] Delete an individual APP Package identified by ID ${NON_EXISTENT_APP_PKG_ID} + GET all app Packages content by appPkgId ${NON_EXISTENT_APP_PKG_ID} + Check HTTP Response Status Code Is 404 + + + +TC_MEC_MEC010p2_MEPM_PKGM_012_02_NF + [Documentation] TP_MEC_MEC010p2_MEPM_PKGM_012_02_NF + ... Check that MEPM service sends an error when it receives a query with an application package with a wrong identifier + ... ETSI GS MEC 010-2 3.1.1, clause 7.3.7.3.2 + [Tags] PIC_APP_PACKAGE_MANAGEMENT + [Setup] Delete an individual APP Package identified by ID ${NON_EXISTENT_APPD_ID} + GET all app Packages content by appPkgId ${NON_EXISTENT_APPD_ID} + Check HTTP Response Status Code Is 404 + *** Keywords *** +Get an AppD identified by + [Arguments] ${appDId} + Log Getting App descriptor for App Package + Set Headers {"Accept":"${ACCEPTED_CONTENT_TYPE}"} + Set Headers {"Authorization":"${TOKEN}"} + Get ${apiRoot}/${apiName}/${apiVersion}/app_packages/${appDId}/appd + ${output}= Output response + Set Suite Variable ${response} ${output} + +Post an AppD identified by + [Arguments] ${appDId} + Log Getting App descriptor for App Package + Set Headers {"Accept":"${ACCEPTED_CONTENT_TYPE}"} + Set Headers {"Authorization":"${TOKEN}"} + Post ${apiRoot}/${apiName}/${apiVersion}/app_packages/${appDId}/appd + ${output}= Output response + Set Suite Variable ${response} ${output} + +Update an AppD identified by + [Arguments] ${appDId} + Log Getting App descriptor for App Package + Set Headers {"Accept":"${ACCEPTED_CONTENT_TYPE}"} + Set Headers {"Authorization":"${TOKEN}"} + Put ${apiRoot}/${apiName}/${apiVersion}/app_packages/${appDId}/appd + ${output}= Output response + Set Suite Variable ${response} ${output} + +Delete an AppD by ID + [Arguments] ${identifier} + Set Headers {"Accept":"application/json"} + Set Headers {"Content-Type":"*/*"} + Set Headers {"Authorization":"${TOKEN}"} + Delete ${apiRoot}/${apiName}/${apiVersion}/app_packages/${identifier}/appd + ${output}= Output response + Set Suite Variable ${response} ${output} + Create new App Package [Arguments] ${content} Set Headers {"Accept":"*/*"} @@ -211,7 +380,6 @@ Create new App Package ${output}= Output response Set Suite Variable ${response} ${output} - GET all app Packages Set Headers {"Accept":"application/json"} Set Headers {"Content-Type":"*/*"} @@ -220,7 +388,6 @@ GET all app Packages ${output}= Output response Set Suite Variable ${response} ${output} - GET all app Packages with filter [Arguments] ${key} ${value} Log Getting all App Packages using filtering parameters @@ -242,6 +409,17 @@ Get an individual APP Package identified by ID Set Suite Variable ${response} ${output} + +GET all app Packages content by appPkgId + [Arguments] ${identifier} + Set Headers {"Accept":"application/zip"} + Set Headers {"Content-Type":"*/*"} + Set Headers {"Authorization":"${TOKEN}"} + Get ${apiRoot}/${apiName}/${apiVersion}/app_packages/${identifier}/package_content + ${output}= Output response + Set Suite Variable ${response} ${output} + + Create a subscription [Arguments] ${content} Set Headers {"Accept":"application/json"} @@ -293,8 +471,6 @@ Delete an individual APP Package identified by ID Spawn Notification Server - [Arguments] ${host} ${port} ${timeout} ${method} ${endpoint} ${notification_content} ${autosent_notification} - ${file}= Catenate SEPARATOR= jsons/ ${notification_content} .json - ${body}= Get File ${file} - #Spawn Web Server ${host} ${port} ${timeout} ${method} ${endpoint} ${body} ${autosent_notification} - + [Arguments] ${payload_notification} + ${output} Spawn Web Server ${NOTIFICATION_SERVER_IP} ${NOTIFICATION_SERVER_PORT} ${NOTIFICATION_SERVER_TIMEOUT} ${NOTIFICATION_SERVER_HTTP_METHOD} ${NOTIFICATION_SERVER_URI} ${payload_notification} + Set Suite Variable ${payload_notification} ${output} diff --git a/MEC010p2/MEPM/PKGM/environment/variables.txt b/MEC010p2/MEPM/PKGM/environment/variables.txt index fc95e30..fa58be8 100644 --- a/MEC010p2/MEPM/PKGM/environment/variables.txt +++ b/MEC010p2/MEPM/PKGM/environment/variables.txt @@ -23,4 +23,12 @@ ${FILTER_VALUE} ENABLED ${NON_EXISTENT_APP_PKG_ID} NON_EXISTENT_APP_PKG_ID ${NON_EXISTENT_SUBSCRIPTION_ID} NON_EXISTENT_SUBSCRIPTION_ID ${NON_EXISTENT_APPD_ID} NON_EXISTENT_APPD_ID -${ONBOARDING_STATE} CREATED \ No newline at end of file +${ONBOARDING_STATE} CREATED + + +##Notification Server variables +${NOTIFICATION_SERVER_IP} 127.0.0.1 +${NOTIFICATION_SERVER_PORT} 8888 +${NOTIFICATION_SERVER_HTTP_METHOD} POST +${NOTIFICATION_SERVER_URI} /callback_url +${NOTIFICATION_SERVER_TIMEOUT} 5 \ No newline at end of file diff --git a/MEC010p2/MEPM/PKGM/jsons/AppPkgNotification.json b/MEC010p2/MEPM/PKGM/jsons/AppPkgNotification.json new file mode 100644 index 0000000..6e1ae30 --- /dev/null +++ b/MEC010p2/MEPM/PKGM/jsons/AppPkgNotification.json @@ -0,0 +1,17 @@ +{ + "id": "1234", + "notificationType": "AppPackageOnBoarded", + "subscriptionId": "1234", + "timeStamp": { + "seconds": 1234, + "nanoSeconds": 1234 + }, + "appPkgId": "appPkgId", + "appDId": "appDId", + "operationalState": "ENABLED", + "_links": { + "subscription": { + "href": "someuri/url" + } + } +} \ No newline at end of file diff --git a/MEC010p2/MEPM/PKGM/libraries/Server.py b/MEC010p2/MEPM/PKGM/libraries/Server.py new file mode 100644 index 0000000..c983c3b --- /dev/null +++ b/MEC010p2/MEPM/PKGM/libraries/Server.py @@ -0,0 +1,144 @@ +#!/usr/bin/python3 + +from http.server import BaseHTTPRequestHandler, HTTPServer +import json, os +import logging + +# Library version +__version__ = '0.0.1' + +def import_notification_json(subscription_type): + notification_type = subscription_type.split("Subscription")[0] + file_path = "./jsons/"+notification_type+".json" + logging.info(file_path) + logging.info(os.listdir()) + try: + with open(file_path, 'r') as json_file: + # Load the JSON data + data = json.load(json_file) + logging.info(data) + return data + except FileNotFoundError: + logging.error(f"Error: File not found at {file_path}") + + +class Server ( object ): + + ROBOT_LIBRARY_VERSION = '0.0.1' + + def spawn_web_server (self, host="127.0.0.1", port=8080, timeout=15, method="POST", endpoint="/callback_url", resp_body=None): + + class GET_Server(BaseHTTPRequestHandler): + + def __call__(self, *args, **kwargs): + """Handle a request.""" + super().__init__(*args, **kwargs) + + def __init__(self, endpoint, resp_body): + self.resp_body = resp_body + self.endpoint = endpoint + + def do_GET(self): + self.send_response(200) + self.send_header('Content-Type', 'application/json') + self.end_headers() + if self.path == self.endpoint: + self.wfile.write(json.dumps(self.resp_body).encode(encoding='utf_8')) + else: + self.wfile.write(json.dumps("wrong endpoint").encode(encoding='utf_8')) + + class POST_Server(BaseHTTPRequestHandler): + + def __call__(self, *args, **kwargs): + """Handle a request.""" + super().__init__(*args, **kwargs) + + def __init__(self, endpoint, resp_body): + self.resp_body = resp_body + self.endpoint = endpoint + self.req_body = None + + + def do_POST(self): + self.send_response(200) + self.send_header('Content-Type', 'application/json') + self.end_headers() + + #if self.path == self.endpoint: + # self.wfile.write(json.dumps(self.resp_body).encode(encoding='utf_8')) + #else: + # self.wfile.write(json.dumps("wrong endpoint").encode(encoding='utf_8')) + + content_len = int(self.headers.get('Content-Length')) + post_body = self.rfile.read(content_len) + self.req_body=post_body + + def get_req_body(self): + return self.req_body + + def get_resp_body(self): + return self.resp_body + + + class PUT_Server(BaseHTTPRequestHandler): + + def __call__(self, *args, **kwargs): + """Handle a request.""" + super().__init__(*args, **kwargs) + + def __init__(self, endpoint, resp_body): + self.resp_body = resp_body + self.endpoint = endpoint + + def do_PUT(self): + self.send_response(200) + self.send_header('Content-Type', 'application/json') + self.end_headers() + if self.path == self.endpoint: + self.wfile.write(json.dumps(self.resp_body).encode(encoding='utf_8')) + else: + self.wfile.write(json.dumps("wrong endpoint").encode(encoding='utf_8')) + + class DELETE_Server(BaseHTTPRequestHandler): + + def __call__(self, *args, **kwargs): + """Handle a request.""" + super().__init__(*args, **kwargs) + + def __init__(self, endpoint, resp_body): + self.resp_body = resp_body + self.endpoint = endpoint + + def do_DELETE(self): + self.send_response(200) + self.send_header('Content-Type', 'application/json') + self.end_headers() + if self.path == self.endpoint: + self.wfile.write(json.dumps(self.resp_body).encode(encoding='utf_8')) + else: + self.wfile.write(json.dumps("wrong endpoint").encode(encoding='utf_8')) + + if method == "GET": + self.handler = GET_Server(endpoint, resp_body) + elif method == "POST": + self.handler = POST_Server(endpoint, resp_body) + elif method == "PUT": + self.handler = PUT_Server(endpoint, resp_body) + elif method == "DELETE": + self.handler = DELETE_Server(endpoint, resp_body) + else: + logging.info("Error, unknown endpoint") + exit(1) + + self.app = HTTPServer((host, int(port)), self.handler) + self.app.timeout = int(timeout) + + + self.app.handle_request() + self.app.server_close() + logging.info(self.handler.get_resp_body()) + if(self.handler.get_req_body()!=None): + return json.loads(self.handler.get_req_body().decode("windows-1252")) + notification_json= import_notification_json(self.handler.get_resp_body()) + return notification_json + diff --git a/MEC010p2/MEPM/PKGM/libraries/__pycache__/Server.cpython-39.pyc b/MEC010p2/MEPM/PKGM/libraries/__pycache__/Server.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6174a8848396b041de13ac5e846ee7d788db8b7b GIT binary patch literal 5554 zcmYe~<>g{vU|@LdJ~4HY2m`}o5C<8vFfcGUFfcF_doVCCq%fo~<}lQOqd}DNH#mQ7mAZHHtNbIf^ZXC55$xA&T9dA%!i4y@er#J(Z)GIf^rdDVRZ% z<0Z%rO{QC7PKm{-9w8wCL8*nMsl_E8iFqkGsYSQAKw`nEMP;c)e!o}^^bGV2lNmvJ zpqQ0`fq{d8fx#K%v_%XI3^fc37*ZG)GNv#|GSo7rFxN1OGo-MHGt@GbFfL%KVOq$@ z$N*xMFfU+P$N=J1d6cl$Fr~0IGxclLGMBI|V6S0b$XLq)QdPqOQc=Sa&cMWw!ob47 z!qCjj$PmsD%wWh+oM*tm2!@diAhA3p7KRdz$~h3R62=9bDQq=Nvl-@s?Tlo|V=`b! zVF+f>WcQ2W2`)`4PAVc-mSSLFNM(p(Oks#(N@0v*PT@-7Na1W@ zjABV)4`$HhzQqP{LJ=q*{Ngk;GS>qq#+MW185myjGcYjRVhQjM4*A8apPZPJla!d8 z9ba0MWOAA8^3z*O0 z&XB^I!q&o&!UpDZx-+D(r*O0|q;P=wTs15yoY_o8VKwYEtTk-SOf?MgjCmn7EGb;s zOhq|097v*eH7qII*-S-EHJnJI8Z|5_JlRY|GitbyM1@M27BHvqf|7m-OA22zD9fIK}8tOjQD zEM!Pw$Y!3vSR_`$yMPZSwtzo{e<9;S#$bjL0k9bNLWX9>TJ9R|Ud*vH7okirj#LDn;Y0)VN?1maAh{GtalAQ|KlkbV#b zu|b9fgG?0wm7$=7#8ATsN=~2>Yb7Jd?VLra#Rc(6`6-pRI8yUc3i31aN>(x#DKIcF z6lsD84G;k;01!?tl4D?CPz9L_3N;2sHbzq23kfHbNB|X=plk$oZyLzG&@!J9RQfZf zFr_drWCF99K#8!1A&a?&A&VuOsmP;-A&YeZTM7FDP;%v5$e6_is-AioK}kJN3L%@q zhAIaR1$MueCqTs;k8^%rNorn+Zb)T8>Mw!Bf`S}q38@b%h;Ff#mZZg7XflF=DLFqS zGcUc0ue>NfFI@p1Y}~hai&OJb;z2>0pI4lEiyO?%NKH&hExN@Gl7g^{Z?S-iT5teI zv6h3&TGsNS%#ze1P-$|DHKjDSptuMW^taej^OEyZQdcrU;siA&Zn350$Gf|RfMTi` zq?Lh@i;;(sgOP_(j;YFsltctIh#i!S^AdAYeimEoY|3rfbt zHbfQ#kTNuiErm0M3)C_J7YrPrQoc%uU^svZc?wE~B59C6Wk3YTb46fpg98BMcW`K# zfU*lHKQb^?nUdljB62sV$f`lf-8BsHOrX?Vq{hI&pveqQN#N9Wi=!yD5S-nMv_Q@U zI}%KQT?)$B79f{`q6d_**%(Q7Etb3uicC;Z0nXbl1oL(YIAvE#}O; zl3VPsOvs#`S`x)t1kQdeMX8A?MWD8@CZzO>;!cJX2=O_oc~P7N`NbuWa07=Hm;eRY zE%ucBcu?}O0|gsW7GxJ-GEY#e+X z%tfFs5}n&C0il$%SBNN;LgB$rOp^oQ93l!OY)uYuqAgMfMFXzVClcf$P+0*geTZmQ zptuQ3(F4jl1JJA}(gu0b5JX_hoSzO1utR1iJqm%3vay)DtcjkDr@oh%2->L@91yRS2kl1$PLsx6z46 z^BJH(2bYCRRpz9*h<3?72jnPtvM1eHLnh^OrsT&%Vz~h1WsJmcL1K)+jl-7(yzyoM zUWj|3sSecjCE(h2BAm