diff --git a/poetry.lock b/poetry.lock index 20e8d40c4..1f2455bca 100644 --- a/poetry.lock +++ b/poetry.lock @@ -217,18 +217,6 @@ files = [ [package.extras] tests = ["mypy (>=1.14.0)", "pytest", "pytest-asyncio"] -[[package]] -name = "asn1crypto" -version = "1.5.1" -description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67"}, - {file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"}, -] - [[package]] name = "attrs" version = "25.3.0" @@ -456,14 +444,14 @@ files = [ [[package]] name = "cachetools" -version = "6.1.0" +version = "7.0.1" description = "Extensible memoizing collections and decorators" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "cachetools-6.1.0-py3-none-any.whl", hash = "sha256:1c7bb3cf9193deaf3508b7c5f2a79986c13ea38965c5adcff1f84519cf39163e"}, - {file = "cachetools-6.1.0.tar.gz", hash = "sha256:b4c4f404392848db3ce7aac34950d17be4d864da4b8b66911008e430bc544587"}, + {file = "cachetools-7.0.1-py3-none-any.whl", hash = "sha256:8f086515c254d5664ae2146d14fc7f65c9a4bce75152eb247e5a9c5e6d7b2ecf"}, + {file = "cachetools-7.0.1.tar.gz", hash = "sha256:e31e579d2c5b6e2944177a0397150d312888ddf4e16e12f1016068f0c03b8341"}, ] [[package]] @@ -684,7 +672,7 @@ version = "8.2.1" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, @@ -949,54 +937,6 @@ wrapt = ">=1.10,<2" [package.extras] dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools ; python_version >= \"3.12\"", "tox"] -[[package]] -name = "dill" -version = "0.3.6" -description = "serialize all of python" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "dill-0.3.6-py3-none-any.whl", hash = "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0"}, - {file = "dill-0.3.6.tar.gz", hash = "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"}, -] - -[package.extras] -graph = ["objgraph (>=1.7.2)"] - -[[package]] -name = "dnslib" -version = "0.9.26" -description = "Simple library to encode/decode DNS wire-format packets" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "dnslib-0.9.26-py3-none-any.whl", hash = "sha256:e68719e633d761747c7e91bd241019ef5a2b61a63f56025939e144c841a70e0d"}, - {file = "dnslib-0.9.26.tar.gz", hash = "sha256:be56857534390b2fbd02935270019bacc5e6b411d156cb3921ac55a7fb51f1a8"}, -] - -[[package]] -name = "dnspython" -version = "2.7.0" -description = "DNS toolkit" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86"}, - {file = "dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"}, -] - -[package.extras] -dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.16.0)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "quart-trio (>=0.11.0)", "sphinx (>=7.2.0)", "sphinx-rtd-theme (>=2.0.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] -dnssec = ["cryptography (>=43)"] -doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] -doq = ["aioquic (>=1.0.0)"] -idna = ["idna (>=3.7)"] -trio = ["trio (>=0.23)"] -wmi = ["wmi (>=1.5.1)"] - [[package]] name = "docopt" version = "0.6.2" @@ -1477,24 +1417,6 @@ files = [ {file = "lazy_object_proxy-1.11.0.tar.gz", hash = "sha256:18874411864c9fbbbaa47f9fc1dd7aea754c86cfde21278ef427639d1dd78e9c"}, ] -[[package]] -name = "localstack" -version = "4.12.0" -description = "LocalStack - A fully functional local Cloud stack" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "localstack-4.12.0.tar.gz", hash = "sha256:87e0824d3115fc72fe78efa6d8bda942e68e69e38b74173c4bff3091efaea1ea"}, -] - -[package.dependencies] -localstack-core = "*" -localstack-ext = "4.12.0" - -[package.extras] -runtime = ["localstack-core[runtime]", "localstack-ext[runtime] (==4.12.0)"] - [[package]] name = "localstack-client" version = "2.10" @@ -1512,73 +1434,6 @@ boto3 = "*" [package.extras] test = ["black", "coverage", "flake8", "isort", "localstack", "pytest"] -[[package]] -name = "localstack-core" -version = "4.12.0" -description = "The core library and runtime of LocalStack" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "localstack_core-4.12.0-py3-none-any.whl", hash = "sha256:730de1c4d71aff00512523b41ba3250f869f9cc774b713e04736851b7ac6cedb"}, - {file = "localstack_core-4.12.0.tar.gz", hash = "sha256:893359fb2392b95a1587e109120904dd4001e640238a2d4a87f8b46c6cb67ba6"}, -] - -[package.dependencies] -asn1crypto = ">=1.5.1" -cachetools = ">=5.0" -click = ">=8.2.0" -cryptography = "*" -dill = "0.3.6" -dnslib = ">=0.9.10" -dnspython = ">=1.16.0" -plux = ">=1.10" -psutil = ">=5.4.8" -python-dotenv = ">=0.19.1" -pyyaml = ">=5.1" -requests = ">=2.20.0" -rich = ">=12.3.0" -semver = ">=2.10" - -[package.extras] -base-runtime = ["Werkzeug (>=3.1.3)", "awscrt (>=0.13.14,!=0.27.1)", "boto3 (==1.42.4)", "botocore (==1.42.4)", "cbor2 (>=5.5.0)", "dnspython (>=1.16.0)", "docker (>=6.1.1)", "hypercorn (>=0.14.4)", "jsonpatch (>=1.24)", "jsonpointer (>=3.0.0)", "jsonschema (>=4.25.1)", "localstack-twisted (>=23.0)", "openapi-core (>=0.19.2)", "pyopenssl (>=23.0.0)", "python-dateutil (>=2.9.0)", "readerwriterlock (>=1.0.7)", "requests-aws4auth (>=1.0)", "rolo (>=0.7)", "typing-extensions (>=4.15.0)", "urllib3 (>=2.0.7)", "xmltodict (>=0.13.0)"] -dev = ["Cython", "coveralls (>=3.3.1)", "deptry (>=0.13.0)", "localstack-core[test]", "mypy", "networkx (>=2.8.4)", "openapi-spec-validator (>=0.7.1)", "pandoc", "pre-commit (>=3.5.0)", "pypandoc", "rstr (>=3.2.0)", "ruff (>=0.3.3)"] -runtime = ["airspeed-ext (>=0.6.3)", "antlr4-python3-runtime (==4.13.2)", "apispec (>=5.1.1)", "aws-sam-translator (>=1.105.0)", "awscli (==1.43.10)", "crontab (>=0.22.6)", "cryptography (>=41.0.5)", "jinja2 (>=3.1.6)", "jpype1 (>=1.6.0)", "jsonpath-ng (>=1.6.1)", "jsonpath-rw (>=1.4.0)", "kclpy-ext (>=3.0.0)", "localstack-core[base-runtime]", "moto-ext[all] (>=5.1.12.post22)", "opensearch-py (>=2.4.1)", "pydantic (>=2.11.9)", "pymongo (>=4.2.0)", "pyopenssl (>=23.0.0)", "responses (>=0.25.8)"] -test = ["aws-cdk-lib (>=2.88.0)", "coverage[toml] (>=5.5)", "httpx[http2] (>=0.25)", "json5 (>=0.12.1)", "localstack-core[runtime]", "localstack-snapshot (>=0.1.1)", "pluggy (>=1.3.0)", "pytest (>=7.4.2)", "pytest-httpserver (>=1.1.2)", "pytest-rerunfailures (>=12.0)", "pytest-split (>=0.8.0)", "pytest-tinybird (>=0.5.0)", "websocket-client (>=1.7.0)"] -typehint = ["boto3-stubs[acm,acm-pca,amplify,apigateway,apigatewayv2,appconfig,appconfigdata,application-autoscaling,appsync,athena,autoscaling,backup,batch,ce,cloudcontrol,cloudformation,cloudfront,cloudtrail,cloudwatch,codebuild,codecommit,codeconnections,codedeploy,codepipeline,codestar-connections,cognito-identity,cognito-idp,dms,docdb,dynamodb,dynamodbstreams,ec2,ecr,ecs,efs,eks,elasticache,elasticbeanstalk,elbv2,emr,emr-serverless,es,events,firehose,fis,glacier,glue,iam,identitystore,iot,iot-data,iotanalytics,iotwireless,kafka,kinesis,kinesisanalyticsv2,kms,lakeformation,lambda,logs,managedblockchain,mediaconvert,mediastore,mq,mwaa,neptune,opensearch,organizations,pi,pinpoint,pipes,rds,rds-data,redshift,redshift-data,resource-groups,resourcegroupstaggingapi,route53,route53resolver,s3,s3control,sagemaker,sagemaker-runtime,secretsmanager,serverlessrepo,servicediscovery,ses,sesv2,sns,sqs,ssm,sso-admin,stepfunctions,sts,timestream-query,timestream-write,transcribe,verifiedpermissions,wafv2,xray]", "localstack-core[dev]"] - -[[package]] -name = "localstack-ext" -version = "4.12.0" -description = "Extensions for LocalStack" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "localstack_ext-4.12.0.tar.gz", hash = "sha256:010ac6ea245305aae29eb1a025e2ebe4e620bae37b296cb145460d307cf89a51"}, -] - -[package.dependencies] -click = ">=7.1" -cryptography = "*" -localstack-core = "4.12.0" -packaging = "*" -plux = ">=1.10.0" -PyJWT = {version = ">=1.7.0", extras = ["crypto"]} -pyotp = ">=2.9.0" -python-dateutil = ">=2.8" -pyyaml = ">=5.1" -requests = ">=2.20.0" -rich = ">=12.3.0" -tabulate = "*" -windows-curses = {version = "*", markers = "platform_system == \"Windows\""} - -[package.extras] -package = ["localstack-obfuscator (>=0.3.0)"] -runtime = ["Whoosh (>=2.7.4)", "airspeed-ext (>=0.6.9)", "alembic (>=1.16.0)", "avro (>=1.11.0)", "aws-encryption-sdk (>=3.1.0)", "aws-json-term-matcher (>=0.1.5)", "aws-sam-translator (>=1.105.0)", "boto3", "botocore", "cachetools (>=5.5.2)", "cedarpy (>=4.1.0)", "confluent-kafka", "distro", "docker (>=7.1.0)", "dulwich (>=0.19.16)", "fastavro (>=1.12.0)", "graphql-core (>=3.0.3)", "hypercorn (>=0.14.4)", "janus (>=0.5.0)", "javascript", "jinja2 (>=3.1.6)", "jsonpatch (>=1.32)", "jsonpath-ng (>=1.7.0)", "jsonschema (>=4.25.1)", "kubernetes (>=21.7.0)", "libvirt-python", "localstack-core[runtime] (==4.12.0)", "localstack-py-avro-schema (==3.9.9)", "mysql-replication", "opentelemetry-api", "opentelemetry-propagator-aws-xray", "opentelemetry-sdk", "orjson", "paho-mqtt (>=1.5)", "parquet[snappy] (>=1.3.1)", "parse (>=1.19.0)", "pg8000 (>=1.10)", "postgres (>=2.2.2)", "postgresql-proxy (>=0.2.0)", "psutil (>=7.1.0)", "psycopg2-binary (>=2.9.10)", "pycdlib (>=1.14.0)", "pycognito (>=2024.5.1)", "pydantic (<2.12)", "pydantic (>=2.11.9)", "pyftpdlib (>=1.5.6)", "pyhive[hive-pure-sasl] (>=0.7.0)", "pyiceberg (>=0.9.0)", "pymysql", "pyopenssl (>=25.3.0)", "pyparsing (>=3.2.5)", "python-dxf (>=12.1.1)", "readerwriterlock (>=1.0.7)", "redis (>=5.0,<6.0)", "rolo", "rsa (>=4.0)", "semver (>=3.0.4)", "setuptools", "sql-metadata (>=2.6.0)", "sqlalchemy (>=2.0.0)", "sqlglot[rs]", "srp-ext (>=1.0.7.1)", "testing.common.database (>=1.1.0)", "trino (>=0.328.0)", "typing-extensions (>=4.15.0)", "urllib3 (>=2.5.0)", "websocket-client (>=1.8.0)", "websockets (>=8.1,<14)", "werkzeug (>=3.1.3)", "xmltodict (>=1.0.2)"] -test = ["PyAthena[pandas]", "aiohttp", "async-timeout", "aws-cdk-lib (>=2.88.0)", "aws-cdk.aws-cognito-identitypool-alpha", "aws_cdk.aws_neptune_alpha", "aws_cdk.aws_redshift_alpha", "aws_xray_sdk (>=2.4.2)", "awsiotsdk", "awsiotsdk", "awswrangler (>=3.5.2)", "coverage[toml] (>=5.0.0)", "deepdiff (>=5.5.0)", "deptry (>=0.13.0)", "dnslib (>=0.9.10)", "dnspython (>=1.16.0)", "gremlinpython (<3.8.0)", "jws (>=0.1.3)", "kafka-python", "localstack-core[test] (==4.12.0)", "localstack-ext[runtime]", "msal", "msal-extensions", "msrest", "mysql-connector-python", "neo4j", "nest-asyncio (>=1.4.1)", "paramiko", "playwright", "portalocker", "pre-commit (>=3.5.0)", "pyarrow", "pymongo", "pymssql (>=2.2.8)", "pypandoc", "pytest-httpserver (>=1.0.1)", "pytest-instafail (>=0.4.2)", "pytest-mock (>=3.14.0)", "pytest-playwright", "python-terraform", "redshift_connector", "ruff (>=0.1.0)", "stomp.py (>=8.0.1)", "uefivars (>=1.2)"] -typehint = ["boto3-stubs[acm,amplify,apigateway,apigatewayv2,appconfig,appsync,athena,autoscaling,backup,batch,bedrock,bedrock-runtime,ce,cloudcontrol,cloudformation,cloudfront,cloudtrail,cloudwatch,codecommit,cognito-identity,cognito-idp,dms,docdb,dynamodb,dynamodbstreams,ec2,ecr,ecs,efs,eks,elasticache,elasticbeanstalk,elbv2,emr,emr-serverless,es,events,firehose,fis,glacier,glue,iam,iot,iot-data,iotanalytics,iotwireless,kafka,kinesis,kinesisanalytics,kinesisanalyticsv2,kms,lakeformation,lambda,logs,mediaconvert,mediastore,mq,mwaa,neptune,opensearch,organizations,pi,rds,rds-data,redshift,redshift-data,resource-groups,resourcegroupstaggingapi,route53,route53resolver,s3,s3control,s3tables,sagemaker,sagemaker-runtime,secretsmanager,serverlessrepo,servicediscovery,ses,sesv2,sns,sqs,ssm,sso-admin,stepfunctions,sts,timestream-query,timestream-write,transcribe,xray]", "localstack-ext[test]"] - [[package]] name = "lxml" version = "5.4.0" @@ -1743,31 +1598,6 @@ files = [ [package.dependencies] typing-extensions = "*" -[[package]] -name = "markdown-it-py" -version = "3.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] -profiling = ["gprof2dot"] -rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - [[package]] name = "markupsafe" version = "3.0.2" @@ -1839,18 +1669,6 @@ files = [ {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - [[package]] name = "moto" version = "5.1.19" @@ -2170,21 +1988,6 @@ files = [ dev = ["pre-commit", "tox"] testing = ["coverage", "pytest", "pytest-benchmark"] -[[package]] -name = "plux" -version = "1.12.1" -description = "A dynamic code loading framework for building pluggable Python distributions" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "plux-1.12.1-py3-none-any.whl", hash = "sha256:b4aa4e67329f2fcd73fb28096a8a9304f5912ee6cce39994eac567e8eec65488"}, - {file = "plux-1.12.1.tar.gz", hash = "sha256:1ed44a6edbb7343f4711ff75ddaf87bed066c53625d41b63a0b4edd3f77792ba"}, -] - -[package.extras] -dev = ["black (==22.3.0)", "isort (==5.9.1)", "pytest (==6.2.4)", "setuptools"] - [[package]] name = "ply" version = "3.11" @@ -2348,30 +2151,6 @@ files = [ {file = "propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168"}, ] -[[package]] -name = "psutil" -version = "7.0.0" -description = "Cross-platform lib for process and system monitoring in Python. NOTE: the syntax of this script MUST be kept compatible with Python 2.7." -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25"}, - {file = "psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da"}, - {file = "psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91"}, - {file = "psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34"}, - {file = "psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993"}, - {file = "psutil-7.0.0-cp36-cp36m-win32.whl", hash = "sha256:84df4eb63e16849689f76b1ffcb36db7b8de703d1bc1fe41773db487621b6c17"}, - {file = "psutil-7.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1e744154a6580bc968a0195fd25e80432d3afec619daf145b9e5ba16cc1d688e"}, - {file = "psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99"}, - {file = "psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553"}, - {file = "psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456"}, -] - -[package.extras] -dev = ["abi3audit", "black (==24.10.0)", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest", "pytest-cov", "pytest-xdist", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "vulture", "wheel"] -test = ["pytest", "pytest-xdist", "setuptools"] - [[package]] name = "pyasn1" version = "0.6.2" @@ -2673,9 +2452,6 @@ files = [ {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, ] -[package.dependencies] -cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} - [package.extras] crypto = ["cryptography (>=3.4.0)"] dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] @@ -3024,25 +2800,6 @@ files = [ [package.dependencies] six = "*" -[[package]] -name = "rich" -version = "14.0.0" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.8.0" -groups = ["dev"] -files = [ - {file = "rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0"}, - {file = "rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - [[package]] name = "rpds-py" version = "0.25.1" @@ -3328,21 +3085,6 @@ files = [ [package.dependencies] tenacity = "*" -[[package]] -name = "tabulate" -version = "0.9.0" -description = "Pretty-print tabular data" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, - {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, -] - -[package.extras] -widechars = ["wcwidth"] - [[package]] name = "tenacity" version = "9.1.2" @@ -3473,33 +3215,6 @@ files = [ [package.extras] test = ["pytest (>=3.0.0)"] -[[package]] -name = "windows-curses" -version = "2.4.1" -description = "Support for the standard curses module on Windows" -optional = false -python-versions = "*" -groups = ["dev"] -markers = "platform_system == \"Windows\"" -files = [ - {file = "windows_curses-2.4.1-cp310-cp310-win32.whl", hash = "sha256:53d711e07194d0d3ff7ceff29e0955b35479bc01465d46c3041de67b8141db2f"}, - {file = "windows_curses-2.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:325439cd4f37897a1de8a9c068a5b4c432f9244bf9c855ee2fbeb3fa721a770c"}, - {file = "windows_curses-2.4.1-cp311-cp311-win32.whl", hash = "sha256:4fa1a176bfcf098d0c9bb7bc03dce6e83a4257fc0c66ad721f5745ebf0c00746"}, - {file = "windows_curses-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fd7d7a9cf6c1758f46ed76b8c67f608bc5fcd5f0ca91f1580fd2d84cf41c7f4f"}, - {file = "windows_curses-2.4.1-cp312-cp312-win32.whl", hash = "sha256:bdbe7d58747408aef8a9128b2654acf6fbd11c821b91224b9a046faba8c6b6ca"}, - {file = "windows_curses-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c9c2635faf171a229caca80e1dd760ab00db078e2a285ba2f667bbfcc31777c"}, - {file = "windows_curses-2.4.1-cp313-cp313-win32.whl", hash = "sha256:05d1ca01e5199a435ccb6c8c2978df4a169cdff1ec99ab15f11ded9de8e5be26"}, - {file = "windows_curses-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8cf653f8928af19c103ae11cfed38124f418dcdd92643c4cd17239c0cec2f9da"}, - {file = "windows_curses-2.4.1-cp36-cp36m-win32.whl", hash = "sha256:6a5a831cabaadde41a6856fea5a0c68c74b7d11d332a816e5a5e6c84577aef3a"}, - {file = "windows_curses-2.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e61be805edc390ccfdeaf0e0c39736d931d3c4a007d6bf0f98d1e792ce437796"}, - {file = "windows_curses-2.4.1-cp37-cp37m-win32.whl", hash = "sha256:a36b8fd4e410ddfb1a8eb65af2116c588e9f99b2ff3404412317440106755485"}, - {file = "windows_curses-2.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:db776df70c10bd523c4a1ab0a7624a1d58c7d47f83ec49c6988f05bc1189e7b8"}, - {file = "windows_curses-2.4.1-cp38-cp38-win32.whl", hash = "sha256:e9ce84559f80de7ec770d28c3b2991e0da51748def04e25a3c08ada727cfac2d"}, - {file = "windows_curses-2.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:618e31458fedba2cf8105485ff00533ece780026c544142fc1647a20dc6c7641"}, - {file = "windows_curses-2.4.1-cp39-cp39-win32.whl", hash = "sha256:775a2e0fefeddfdb0e00b3fa6c4f21caf9982db34df30e4e62c49caaef7b5e56"}, - {file = "windows_curses-2.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:4588213f7ef3b0c24c5cb9e309653d7a84c1792c707561e8b471d466ca79f2b8"}, -] - [[package]] name = "wireup" version = "2.2.2" @@ -3741,4 +3456,4 @@ propcache = ">=0.2.1" [metadata] lock-version = "2.1" python-versions = "^3.13" -content-hash = "383eb0e3581570630fc52690d61866a180122fe61c891bea564a82f354a99992" +content-hash = "c5064b43e402173391286c84cff772c1776fdf816a8fbd229cfdafa26da4b456" diff --git a/pyproject.toml b/pyproject.toml index cf9a9c3a4..4fbaa3f8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,6 @@ awscli-local = "^0.22.2" polyfactory = "^3.2.0" pyright = "^1.1.407" brunns-matchers = "^2.9.0" -localstack = "^4.12.0" pytest-docker = "^3.2.3" stamina = "^25.2.0" pytest-freezer = "^0.4.9" @@ -63,7 +62,7 @@ behave = "^1.3.3" python-dotenv = "^1.2.1" openapi-spec-validator = "^0.7.2" pip-licenses = "^5.5.0" - +cachetools = "^7.0.1" [tool.poetry-plugin-lambda-build] docker-image = "public.ecr.aws/sam/build-python3.13:1.139-x86_64" # See https://gallery.ecr.aws/search?searchTerm=%22python%22&architecture=x86-64&popularRegistries=amazon&verified=verified&operatingSystems=Linux diff --git a/src/eligibility_signposting_api/config/config.py b/src/eligibility_signposting_api/config/config.py index 52f3111cc..bb23991a5 100644 --- a/src/eligibility_signposting_api/config/config.py +++ b/src/eligibility_signposting_api/config/config.py @@ -26,7 +26,7 @@ def config() -> dict[str, Any]: audit_bucket_name = BucketName(os.getenv("AUDIT_BUCKET_NAME", "test-audit-bucket")) hashing_secret_name = HashSecretName(os.getenv("HASHING_SECRET_NAME", "test_secret")) aws_default_region = AwsRegion(os.getenv("AWS_DEFAULT_REGION", "eu-west-1")) - enable_xray_patching = bool(os.getenv("ENABLE_XRAY_PATCHING", "false")) + enable_xray_patching = os.getenv("ENABLE_XRAY_PATCHING", "false").lower() == "true" kinesis_audit_stream_to_s3 = AwsKinesisFirehoseStreamName( os.getenv("KINESIS_AUDIT_STREAM_TO_S3", "test_kinesis_audit_stream_to_s3") ) @@ -51,21 +51,21 @@ def config() -> dict[str, Any]: "log_level": log_level, } - local_stack_endpoint = "http://localhost:4566" + moto_server_endpoint = "http://localhost:4566" return { "aws_access_key_id": AwsAccessKey(os.getenv("AWS_ACCESS_KEY_ID", "dummy_key")), "aws_default_region": aws_default_region, "aws_secret_access_key": AwsSecretAccessKey(os.getenv("AWS_SECRET_ACCESS_KEY", "dummy_secret")), - "dynamodb_endpoint": URL(os.getenv("DYNAMODB_ENDPOINT", local_stack_endpoint)), + "dynamodb_endpoint": URL(os.getenv("DYNAMODB_ENDPOINT", moto_server_endpoint)), "person_table_name": person_table_name, - "s3_endpoint": URL(os.getenv("S3_ENDPOINT", local_stack_endpoint)), + "s3_endpoint": URL(os.getenv("S3_ENDPOINT", moto_server_endpoint)), "rules_bucket_name": rules_bucket_name, "audit_bucket_name": audit_bucket_name, "consumer_mapping_bucket_name": consumer_mapping_bucket_name, - "firehose_endpoint": URL(os.getenv("FIREHOSE_ENDPOINT", local_stack_endpoint)), + "firehose_endpoint": URL(os.getenv("FIREHOSE_ENDPOINT", moto_server_endpoint)), "kinesis_audit_stream_to_s3": kinesis_audit_stream_to_s3, "enable_xray_patching": enable_xray_patching, - "secretsmanager_endpoint": URL(os.getenv("SECRET_MANAGER_ENDPOINT", local_stack_endpoint)), + "secretsmanager_endpoint": URL(os.getenv("SECRET_MANAGER_ENDPOINT", moto_server_endpoint)), "hashing_secret_name": hashing_secret_name, "log_level": log_level, } diff --git a/tests/docker-compose.mock_aws.yml b/tests/docker-compose.mock_aws.yml new file mode 100644 index 000000000..a4388136b --- /dev/null +++ b/tests/docker-compose.mock_aws.yml @@ -0,0 +1,50 @@ +services: + moto-server: + #used for s3, dynamodb, kinesis, secret manager + # lambda cannot be used, because its 3.11 (older) + image: motoserver/moto:latest + container_name: moto-server + ports: + - "4566:5000" + networks: + - test-network + healthcheck: + test: [ "CMD", "curl", "-f", "http://localhost:5000/" ] + interval: 1s + timeout: 1s + retries: 30 + lambda-api: + image: public.ecr.aws/lambda/python:3.13 + container_name: lambda-api + ports: + - "4567:8080" + platform: linux/amd64 + volumes: + - ../dist/lambda.zip:/tmp/lambda.zip:ro + environment: + - AWS_ACCESS_KEY_ID=dummy_key + - AWS_SECRET_ACCESS_KEY=dummy_secret + - AWS_DEFAULT_REGION=eu-west-1 + - PYTHONPATH=/var/task + - AWS_ENDPOINT_URL=http://moto-server:5000 + - DYNAMODB_ENDPOINT=http://moto-server:5000 + - S3_ENDPOINT=http://moto-server:5000 + - SECRET_MANAGER_ENDPOINT=http://moto-server:5000 + - FIREHOSE_ENDPOINT=http://moto-server:5000 + - LOG_LEVEL=INFO + entrypoint: /bin/sh + command: + - "-c" + - | + mkdir -p /var/task && + python3 -m zipfile -e /tmp/lambda.zip /var/task && + exec /usr/local/bin/aws-lambda-rie python3 -m awslambdaric eligibility_signposting_api.app.lambda_handler + networks: + - test-network + depends_on: + moto-server: + condition: service_healthy + +networks: + test-network: + driver: bridge diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml deleted file mode 100644 index e6b90dd5a..000000000 --- a/tests/docker-compose.yml +++ /dev/null @@ -1,20 +0,0 @@ -services: - localstack: -# container_name: "${LOCALSTACK_DOCKER_NAME:-localstack-main}" - image: localstack/localstack:4.4.0 # See https://hub.docker.com/r/localstack/localstack/tags - ports: - - "4566:4566" # LocalStack Gateway - - "4510-4559:4510-4559" # external services port range - environment: - # LocalStack configuration: https://docs.localstack.cloud/references/configuration/ - - DEBUG=${LOCALSTACK_DEBUG:-0} - - DEFAULT_REGION=${AWS_DEFAULT_REGION:-eu-west-1} - - LAMBDA_EXECUTOR=docker - volumes: - - "${LOCALSTACK_VOLUME_DIR:-../volume}:/var/lib/localstack" - - "/var/run/docker.sock:/var/run/docker.sock" - healthcheck: - test: "curl -f http://localhost:4566/health || exit 1" - interval: 5s - timeout: 5s - retries: 10 diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 42d50c670..4293be080 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -2,10 +2,9 @@ import json import logging import os -import subprocess from collections.abc import Callable, Generator from pathlib import Path -from typing import TYPE_CHECKING, Any +from typing import Any import httpx import pytest @@ -13,6 +12,7 @@ from boto3 import Session from boto3.resources.base import ServiceResource from botocore.client import BaseClient +from botocore.exceptions import ClientError from faker import Faker from httpx import RequestError from yarl import URL @@ -39,9 +39,6 @@ from tests.fixtures.builders.model.rule import RulesMapperFactory from tests.fixtures.builders.repos.person import person_rows_builder -if TYPE_CHECKING: - from pytest_docker.plugin import Services - logger = logging.getLogger(__name__) AWS_REGION = "eu-west-1" @@ -52,21 +49,57 @@ UNIQUE_CONSUMER_HEADER = "nhse-product-id" +MOTO_PORT = 5000 + +HTTP_SERVER_ERROR = 500 + + +@pytest.fixture(scope="session", autouse=True) +def aws_credentials(): + """Mocked AWS Credentials for moto.""" + os.environ["AWS_ACCESS_KEY_ID"] = "dummy_key" + os.environ["AWS_SECRET_ACCESS_KEY"] = "dummy_secret" # noqa: S105 + os.environ["AWS_SECURITY_TOKEN"] = "dummy_token" # noqa: S105 + os.environ["AWS_SESSION_TOKEN"] = "dummy_session_token" # noqa: S105 + os.environ["AWS_DEFAULT_REGION"] = AWS_REGION + + +@pytest.fixture(scope="session") +def docker_compose_file(pytestconfig): + root = Path(pytestconfig.rootpath) / "tests" + return [str(root / "docker-compose.mock_aws.yml")] + + +@pytest.fixture(scope="session") +def docker_compose_project_name(): + return "eligibility-integration" + @pytest.fixture(scope="session") -def localstack(request: pytest.FixtureRequest) -> URL: - if url := os.getenv("RUNNING_LOCALSTACK_URL", None): - logger.info("localstack already running on %s", url) - return URL(url) +def docker_setup(): + return [] - docker_ip: str = request.getfixturevalue("docker_ip") - docker_services: Services = request.getfixturevalue("docker_services") - logger.info("Starting localstack") - port = docker_services.port_for("localstack", 4566) +@pytest.fixture(scope="session") +def docker_compose_services(): + return [] + + +@pytest.fixture(scope="session") +def moto_server(request: pytest.FixtureRequest) -> URL: + docker_services = request.getfixturevalue("docker_services") + docker_ip = request.getfixturevalue("docker_ip") + + docker_services._docker_compose.execute("up -d moto-server") # noqa : SLF001 + + port = docker_services.port_for("moto-server", 5000) url = URL(f"http://{docker_ip}:{port}") - docker_services.wait_until_responsive(timeout=30.0, pause=0.1, check=lambda: is_responsive(url)) - logger.info("localstack running on %s", url) + + docker_services.wait_until_responsive( + timeout=30.0, + pause=0.2, + check=lambda: is_responsive(url), + ) return url @@ -82,57 +115,58 @@ def is_responsive(url: URL) -> bool: @pytest.fixture(scope="session") def boto3_session() -> Session: - return Session(aws_access_key_id="fake", aws_secret_access_key="fake", region_name=AWS_REGION) - - -@pytest.fixture(scope="session") -def api_gateway_client(boto3_session: Session, localstack: URL) -> BaseClient: - return boto3_session.client("apigateway", endpoint_url=str(localstack)) - - -@pytest.fixture(scope="session") -def lambda_client(boto3_session: Session, localstack: URL) -> BaseClient: - return boto3_session.client("lambda", endpoint_url=str(localstack)) + return Session( + aws_access_key_id="fake", aws_secret_access_key="fake", aws_session_token="fake", region_name=AWS_REGION + ) @pytest.fixture(scope="session") -def dynamodb_client(boto3_session: Session, localstack: URL) -> BaseClient: - return boto3_session.client("dynamodb", endpoint_url=str(localstack)) +def dynamodb_client(boto3_session: Session, moto_server: URL) -> BaseClient: + return boto3_session.client("dynamodb", endpoint_url=str(moto_server)) @pytest.fixture(scope="session") -def dynamodb_resource(boto3_session: Session, localstack: URL) -> ServiceResource: - return boto3_session.resource("dynamodb", endpoint_url=str(localstack)) +def dynamodb_resource(boto3_session: Session, moto_server: URL) -> ServiceResource: + return boto3_session.resource("dynamodb", endpoint_url=str(moto_server)) -@pytest.fixture(scope="session") -def logs_client(boto3_session: Session, localstack: URL) -> BaseClient: - return boto3_session.client("logs", endpoint_url=str(localstack)) +def get_log_messages(flask_function: str, logs_client: BaseClient) -> list[str]: + for attempt in stamina.retry_context(on=ClientError, attempts=20, timeout=120): + with attempt: + log_streams = logs_client.describe_log_streams( + logGroupName=f"/aws/lambda/{flask_function}", orderBy="LastEventTime", descending=True + ) + assert log_streams["logStreams"] != [] + log_stream_name = log_streams["logStreams"][0]["logStreamName"] + log_events = logs_client.get_log_events( + logGroupName=f"/aws/lambda/{flask_function}", logStreamName=log_stream_name, limit=100 + ) + return [e["message"] for e in log_events["events"]] @pytest.fixture(scope="session") -def iam_client(boto3_session: Session, localstack: URL) -> BaseClient: - return boto3_session.client("iam", endpoint_url=str(localstack)) +def iam_client(boto3_session: Session, moto_server: URL) -> BaseClient: + return boto3_session.client("iam", endpoint_url=str(moto_server)) @pytest.fixture(scope="session") -def s3_client(boto3_session: Session, localstack: URL) -> BaseClient: - return boto3_session.client("s3", endpoint_url=str(localstack)) +def s3_client(boto3_session: Session, moto_server: URL) -> BaseClient: + return boto3_session.client("s3", endpoint_url=str(moto_server)) @pytest.fixture(scope="session") -def firehose_client(boto3_session: Session, localstack: URL) -> BaseClient: - return boto3_session.client("firehose", endpoint_url=str(localstack)) +def firehose_client(boto3_session: Session, moto_server: URL) -> BaseClient: + return boto3_session.client("firehose", endpoint_url=str(moto_server)) @pytest.fixture(scope="session") -def secretsmanager_client(boto3_session: Session, localstack: URL) -> BaseClient: +def secretsmanager_client(boto3_session: Session, moto_server: URL) -> BaseClient: """ - Provides a boto3 Secrets Manager client bound to LocalStack. + Provides a boto3 Secrets Manager client bound to Moto. Seeds a test secret for use in integration tests. """ client: BaseClient = boto3_session.client( - service_name="secretsmanager", endpoint_url=str(localstack), region_name="eu-west-1" + service_name="secretsmanager", endpoint_url=str(moto_server), region_name="eu-west-1" ) secret_name = AWS_SECRET_NAME @@ -159,113 +193,6 @@ def secretsmanager_client(boto3_session: Session, localstack: URL) -> BaseClient return client -@pytest.fixture(scope="session") -def iam_role(iam_client: BaseClient) -> Generator[str]: - role_name = "LambdaExecutionRole" - policy_name = "LambdaCloudWatchPolicy" - - # Define IAM Trust Policy for Lambda Execution Role - trust_policy = { - "Version": "2012-10-17", - "Statement": [ - {"Effect": "Allow", "Principal": {"Service": "lambda.amazonaws.com"}, "Action": "sts:AssumeRole"} - ], - } - - # Create IAM Role - role = iam_client.create_role( - RoleName=role_name, - AssumeRolePolicyDocument=json.dumps(trust_policy), - Description="Role for Lambda execution with CloudWatch logging permissions", - ) - - # Define IAM Policy for CloudWatch Logs - log_policy = { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"], - "Resource": "arn:aws:logs:*:*:*", - } - ], - } - dynamodb_policy = { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "dynamodb:GetItem", - "dynamodb:PutItem", - "dynamodb:UpdateItem", - "dynamodb:DeleteItem", - "dynamodb:Scan", - "dynamodb:Query", - ], - "Resource": "arn:aws:dynamodb:*:*:table/*", - } - ], - } - - # Create CloudWatch Logs policy (as before) - log_policy_resp = iam_client.create_policy(PolicyName=policy_name, PolicyDocument=json.dumps(log_policy)) - log_policy_arn = log_policy_resp["Policy"]["Arn"] - iam_client.attach_role_policy(RoleName=role_name, PolicyArn=log_policy_arn) - - # Create DynamoDB policy - ddb_policy_resp = iam_client.create_policy( - PolicyName="LambdaDynamoDBPolicy", PolicyDocument=json.dumps(dynamodb_policy) - ) - ddb_policy_arn = ddb_policy_resp["Policy"]["Arn"] - iam_client.attach_role_policy(RoleName=role_name, PolicyArn=ddb_policy_arn) - - yield role["Role"]["Arn"] - - iam_client.detach_role_policy(RoleName=role_name, PolicyArn=log_policy_arn) - iam_client.delete_policy(PolicyArn=log_policy_arn) - iam_client.detach_role_policy(RoleName=role_name, PolicyArn=ddb_policy_arn) - iam_client.delete_policy(PolicyArn=ddb_policy_arn) - iam_client.delete_role(RoleName=role_name) - - -@pytest.fixture(scope="session") -def lambda_zip() -> Path: - build_result = subprocess.run(["make", "build"], capture_output=True, text=True, check=False) # noqa: S607 - assert build_result.returncode == 0, f"'make build' failed: {build_result.stderr}" - return Path("dist/lambda.zip") - - -@pytest.fixture(scope="session") -def flask_function(lambda_client: BaseClient, iam_role: str, lambda_zip: Path) -> Generator[str]: - function_name = "eligibility_signposting_api" - with lambda_zip.open("rb") as zipfile: - lambda_client.create_function( - FunctionName=function_name, - Runtime="python3.13", - Role=iam_role, - Handler="eligibility_signposting_api.app.lambda_handler", - Code={"ZipFile": zipfile.read()}, - Architectures=["x86_64"], - Timeout=180, - Environment={ - "Variables": { - "DYNAMODB_ENDPOINT": os.getenv("LOCALSTACK_INTERNAL_ENDPOINT", "http://localstack:4566/"), - "S3_ENDPOINT": os.getenv("LOCALSTACK_INTERNAL_ENDPOINT", "http://localstack:4566/"), - "FIREHOSE_ENDPOINT": os.getenv("LOCALSTACK_INTERNAL_ENDPOINT", "http://localstack:4566/"), - "SECRET_MANAGER_ENDPOINT": os.getenv("LOCALSTACK_INTERNAL_ENDPOINT", "http://localstack:4566/"), - "AWS_REGION": AWS_REGION, - "LOG_LEVEL": "DEBUG", - } - }, - ) - logger.info("loaded zip") - wait_for_function_active(function_name, lambda_client) - logger.info("function active") - yield function_name - lambda_client.delete_function(FunctionName=function_name) - - @pytest.fixture(autouse=True) def clean_audit_bucket(s3_client: BaseClient, audit_bucket: str): objects_to_delete = [] @@ -282,92 +209,6 @@ def clean_audit_bucket(s3_client: BaseClient, audit_bucket: str): ) -@pytest.fixture(scope="session") -def flask_function_url(lambda_client: BaseClient, flask_function: str) -> URL: - response = lambda_client.create_function_url_config(FunctionName=flask_function, AuthType="NONE") - return URL(response["FunctionUrl"]) - - -class FunctionNotActiveError(Exception): - """Lambda Function not yet active""" - - -def wait_for_function_active(function_name, lambda_client): - for attempt in stamina.retry_context(on=FunctionNotActiveError, attempts=20, timeout=120): - with attempt: - logger.info("waiting") - response = lambda_client.get_function(FunctionName=function_name) - function_state = response["Configuration"]["State"] - logger.info("function_state %s", function_state) - if function_state != "Active": - raise FunctionNotActiveError - - -@pytest.fixture(scope="session") -def configured_api_gateway(api_gateway_client, lambda_client, flask_function: str): - region = lambda_client.meta.region_name - - api = api_gateway_client.create_rest_api(name="API Gateway Lambda integration") - rest_api_id = api["id"] - - resources = api_gateway_client.get_resources(restApiId=rest_api_id) - root_id = next(item["id"] for item in resources["items"] if item["path"] == "/") - - patient_check_res = api_gateway_client.create_resource( - restApiId=rest_api_id, parentId=root_id, pathPart="patient-check" - ) - patient_check_id = patient_check_res["id"] - - id_res = api_gateway_client.create_resource(restApiId=rest_api_id, parentId=patient_check_id, pathPart="{id}") - resource_id = id_res["id"] - - api_gateway_client.put_method( - restApiId=rest_api_id, - resourceId=resource_id, - httpMethod="GET", - authorizationType="NONE", - requestParameters={"method.request.path.id": True}, - ) - - # Integration with actual region - lambda_uri = ( - f"arn:aws:apigateway:{region}:lambda:path/2015-03-31/functions/" - f"arn:aws:lambda:{region}:000000000000:function:{flask_function}/invocations" - ) - api_gateway_client.put_integration( - restApiId=rest_api_id, - resourceId=resource_id, - httpMethod="GET", - type="AWS_PROXY", - integrationHttpMethod="POST", - uri=lambda_uri, - passthroughBehavior="WHEN_NO_MATCH", - ) - - # Permission with matching region - lambda_client.add_permission( - FunctionName=flask_function, - StatementId="apigateway-access", - Action="lambda:InvokeFunction", - Principal="apigateway.amazonaws.com", - SourceArn=f"arn:aws:execute-api:{region}:000000000000:{rest_api_id}/*/GET/patient-check/*", - ) - - # Deploy the API - api_gateway_client.create_deployment(restApiId=rest_api_id, stageName="dev") - - return { - "rest_api_id": rest_api_id, - "resource_id": resource_id, - "invoke_url": f"http://{rest_api_id}.execute-api.localhost.localstack.cloud:4566/dev/patient-check/{{id}}", - } - - -@pytest.fixture -def api_gateway_endpoint(configured_api_gateway: dict) -> URL: - return URL(f"http://{configured_api_gateway['rest_api_id']}.execute-api.localhost.localstack.cloud:4566/dev") - - @pytest.fixture(scope="session") def person_table(dynamodb_resource: ServiceResource) -> Generator[Any]: table = dynamodb_resource.create_table( @@ -727,17 +568,22 @@ def audit_bucket(s3_client: BaseClient) -> Generator[BucketName]: @pytest.fixture(autouse=True) def firehose_delivery_stream(firehose_client: BaseClient, audit_bucket: BucketName) -> dict[str, Any]: - return firehose_client.create_delivery_stream( - DeliveryStreamName="test_kinesis_audit_stream_to_s3", - DeliveryStreamType="DirectPut", - ExtendedS3DestinationConfiguration={ - "BucketARN": f"arn:aws:s3:::{audit_bucket}", - "RoleARN": "arn:aws:iam::000000000000:role/firehose_delivery_role", - "Prefix": "audit-logs/", - "BufferingHints": {"SizeInMBs": 1, "IntervalInSeconds": 60}, - "CompressionFormat": "UNCOMPRESSED", - }, - ) + stream_name = "test_kinesis_audit_stream_to_s3" + + try: + return firehose_client.create_delivery_stream( + DeliveryStreamName=stream_name, + DeliveryStreamType="DirectPut", + ExtendedS3DestinationConfiguration={ + "BucketARN": f"arn:aws:s3:::{audit_bucket}", + "RoleARN": "arn:aws:iam::123456789012:role/firehose_delivery_role", + "Prefix": "audit-logs/", + "BufferingHints": {"SizeInMBs": 1, "IntervalInSeconds": 60}, + "CompressionFormat": "UNCOMPRESSED", + }, + ) + except firehose_client.exceptions.ResourceInUseException: + return firehose_client.describe_delivery_stream(DeliveryStreamName=stream_name) @pytest.fixture(scope="class") diff --git a/tests/integration/lambda/conftest.py b/tests/integration/lambda/conftest.py new file mode 100644 index 000000000..fb275e591 --- /dev/null +++ b/tests/integration/lambda/conftest.py @@ -0,0 +1,176 @@ +import shutil +import subprocess +import urllib.parse +from collections.abc import Callable +from http import HTTPStatus +from pathlib import Path + +import httpx +import pytest +from boto3 import Session +from botocore.client import BaseClient +from pytest_docker import Services +from yarl import URL + +from tests.integration.conftest import is_responsive + + +def get_project_root() -> Path: + """Find the project root by locating 'dist' or '.git'.""" + current = Path(__file__).resolve() + for parent in current.parents: + if (parent / "dist").exists() or (parent / ".git").exists(): + return parent + return Path(__file__).resolve().parents[3] + + +@pytest.fixture(scope="session") +def lambda_zip() -> Path: + """Build the lambda.zip artifact using `make build`.""" + project_root = get_project_root() + + make_path = shutil.which("make") + if not make_path: + pytest.fail("The 'make' executable was not found in the system PATH.") + + build_result = subprocess.run( # noqa: S603 + [make_path, "build"], + cwd=project_root, + capture_output=True, + text=True, + check=False, + ) + + if build_result.returncode != 0: + pytest.fail( + f"'make build' failed with code {build_result.returncode}.\n" + f"STDOUT:\n{build_result.stdout}\n" + f"STDERR:\n{build_result.stderr}" + ) + + zip_path = project_root / "dist" / "lambda.zip" + if not zip_path.exists(): + pytest.fail(f"Build succeeded but {zip_path} was not created.") + + return zip_path + + +@pytest.fixture(scope="session") +def lambda_runtime_url(request, lambda_zip: Path) -> URL: # noqa : ARG001 + docker_services = request.getfixturevalue("docker_services") + docker_ip = request.getfixturevalue("docker_ip") + + docker_services._docker_compose.execute("up -d lambda-api") # noqa : SLF001 + + port = docker_services.port_for("lambda-api", 8080) + base_url = URL(f"http://{docker_ip}:{port}") + + # The RIE expects this path for invocations + health_url = base_url / "2015-03-31/functions/function/invocations" + + docker_services.wait_until_responsive( + timeout=60.0, + pause=2, + check=lambda: is_responsive(health_url), + ) + return base_url + + +@pytest.fixture(scope="session") +def lambda_client(boto3_session: Session, lambda_runtime_url: URL) -> BaseClient: + """Return a boto3 Lambda client pointing at the simulated lambda runtime.""" + return boto3_session.client("lambda", endpoint_url=str(lambda_runtime_url)) + + +def get_lambda_logs(docker_services) -> list[str]: + """ + Fetch logs from the lambda-api container using the internal pytest-docker executor. + This replaces manual subprocess calls and path resolution. + """ + try: + result = docker_services._docker_compose.execute("logs --no-color lambda-api") # noqa: SLF001 + + output = result.decode("utf-8") if isinstance(result, bytes) else str(result) + + return [line.split("|", 1)[-1].strip() for line in output.splitlines()] + except Exception as e: # noqa: BLE001 + return [f"Error fetching logs: {e!s}"] + + +@pytest.fixture +def lambda_logs(docker_services: Services) -> Callable[[], list[str]]: + """Fixture to provide access to container logs.""" + + def _get_messages() -> list[str]: + return get_lambda_logs(docker_services) + + return _get_messages + + +def build_api_gateway_v2_event(path: str, headers: dict | None = None, params: dict | None = None): + """ + Simulates the HTTP payload sent by a Lambda Function URL or API Gateway V2. + """ + query_string = urllib.parse.urlencode(params) if params else "" + return { + "version": "2.0", + "routeKey": "$default", + "rawPath": path, + "rawQueryString": query_string, + "headers": {"accept": "*/*", "content-type": "application/json", **(headers or {})}, + "queryStringParameters": params or {}, + "requestContext": { + "http": { + "method": "GET", + "path": path, + "protocol": "HTTP/1.1", + "sourceIp": "127.0.0.1", + }, + }, + "isBase64Encoded": False, + } + + +def unwrap_lambda_response(rie_response: httpx.Response) -> httpx.Response: + """ + Unpacks a Lambda Proxy (RIE) response into a standard httpx.Response. + """ + data = rie_response.json() + + inner_body = data.get("body", "{}") + inner_status = data.get("statusCode", 200) + inner_headers = data.get("headers", {}) + + unwrapped_response = httpx.Response( + status_code=inner_status, + content=inner_body.encode("utf-8"), + headers=inner_headers, + request=rie_response.request, + ) + + # Transfers timing metadata from the RIE call to the unwrapped response. + # This prevents a RuntimeError when matchers access the .elapsed property during is_response() assertion. + unwrapped_response._elapsed = rie_response.elapsed # noqa: SLF001 + + return unwrapped_response + + +@pytest.fixture +def invoke_with_mock_apigw_request(lambda_runtime_url: URL) -> Callable[..., httpx.Response]: + """ + Fixture that returns a function to invoke the Lambda via RIE + and returns a clean, unwrapped httpx.Response. + """ + invocation_url = str(lambda_runtime_url / "2015-03-31/functions/function/invocations") + + def _invoke(path: str, headers: dict | None = None, params: dict | None = None) -> httpx.Response: + payload = build_api_gateway_v2_event(path=path, headers=headers, params=params) + + rie_response = httpx.post(invocation_url, json=payload, timeout=10) + + if rie_response.status_code != HTTPStatus.OK: + pytest.fail(f"RIE failed with {rie_response.status_code}: {rie_response.text}") + + return unwrap_lambda_response(rie_response) + + return _invoke diff --git a/tests/integration/lambda/test_app_running_as_lambda.py b/tests/integration/lambda/test_app_running_as_lambda.py index 51abcfe29..e823aed4d 100644 --- a/tests/integration/lambda/test_app_running_as_lambda.py +++ b/tests/integration/lambda/test_app_running_as_lambda.py @@ -1,12 +1,9 @@ -import base64 import json import logging +from collections.abc import Callable from http import HTTPStatus -import httpx -import stamina from botocore.client import BaseClient -from botocore.exceptions import ClientError from brunns.matchers.data import json_matching as is_json_that from brunns.matchers.response import is_response from freezegun import freeze_time @@ -21,7 +18,6 @@ has_key, is_not, ) -from yarl import URL from eligibility_signposting_api.model.campaign_config import CampaignConfig from eligibility_signposting_api.model.consumer_mapping import ConsumerId, ConsumerMapping @@ -32,16 +28,16 @@ logger = logging.getLogger(__name__) -def test_install_and_call_lambda_flask( +def test_install_and_call_lambda_flask( # noqa: PLR0913 lambda_client: BaseClient, - flask_function: str, persisted_person: NHSNumber, consumer_to_active_rsv_campaign_mapping: ConsumerMapping, # noqa: ARG001 consumer_id: ConsumerId, + secretsmanager_client: BaseClient, # noqa :ARG001 + lambda_logs: Callable[[], list[str]], ): """Given lambda installed into localstack, run it via boto3 lambda client""" # Given - # When request_payload = { "version": "2.0", @@ -68,12 +64,10 @@ def test_install_and_call_lambda_flask( "isBase64Encoded": False, } response = lambda_client.invoke( - FunctionName=flask_function, + FunctionName="function", InvocationType="RequestResponse", Payload=json.dumps(request_payload), - LogType="Tail", ) - log_output = base64.b64decode(response["LogResult"]).decode("utf-8") # Then assert_that(response, has_entries(StatusCode=HTTPStatus.OK)) @@ -84,24 +78,26 @@ def test_install_and_call_lambda_flask( has_entries(statusCode=HTTPStatus.OK, body=is_json_that(has_key("processedSuggestions"))), ) - assert_that(log_output, contains_string("checking nhs_number")) + # assert logs from lambda container + messages = lambda_logs() + assert_that(messages, has_item(contains_string("checking nhs_number"))) def test_install_and_call_flask_lambda_over_http( persisted_person: NHSNumber, consumer_to_active_rsv_campaign_mapping: ConsumerMapping, # noqa: ARG001 consumer_id: ConsumerId, - api_gateway_endpoint: URL, + secretsmanager_client: BaseClient, # noqa:ARG001 + invoke_with_mock_apigw_request, ): """Given api-gateway and lambda installed into localstack, run it via http""" # Given # When - invoke_url = f"{api_gateway_endpoint}/patient-check/{persisted_person}" - response = httpx.get( - invoke_url, - headers={"nhs-login-nhs-number": str(persisted_person), UNIQUE_CONSUMER_HEADER: consumer_id}, - timeout=10, - ) + headers = {"nhs-login-nhs-number": str(persisted_person), UNIQUE_CONSUMER_HEADER: consumer_id} + + invoke_path = f"/patient-check/{persisted_person}" + + response = invoke_with_mock_apigw_request(invoke_path, headers) # Then assert_that( @@ -111,25 +107,21 @@ def test_install_and_call_flask_lambda_over_http( def test_install_and_call_flask_lambda_with_unknown_nhs_number( # noqa: PLR0913 - flask_function: str, persisted_person: NHSNumber, consumer_to_active_rsv_campaign_mapping: ConsumerMapping, # noqa: ARG001 consumer_id: ConsumerId, - logs_client: BaseClient, - api_gateway_endpoint: URL, + invoke_with_mock_apigw_request, secretsmanager_client: BaseClient, # noqa: ARG001 + lambda_logs: Callable[[], list[str]], ): """Given lambda installed into localstack, run it via http, with a nonexistent NHS number specified""" # Given nhs_number = f"123{persisted_person}" # When - invoke_url = f"{api_gateway_endpoint}/patient-check/{nhs_number}" - response = httpx.get( - invoke_url, - headers={"nhs-login-nhs-number": str(nhs_number), UNIQUE_CONSUMER_HEADER: consumer_id}, - timeout=10, - ) + invoke_path = f"/patient-check/{nhs_number}" + headers = {"nhs-login-nhs-number": str(nhs_number), UNIQUE_CONSUMER_HEADER: consumer_id} + response = invoke_with_mock_apigw_request(invoke_path, headers) assert_that( response, @@ -164,27 +156,13 @@ def test_install_and_call_flask_lambda_with_unknown_nhs_number( # noqa: PLR0913 ), ) - messages = get_log_messages(flask_function, logs_client) + messages = lambda_logs() assert_that( messages, has_item(contains_string(f"NHS Number '{nhs_number}' was not recognised by the Eligibility Signposting API")), ) -def get_log_messages(flask_function: str, logs_client: BaseClient) -> list[str]: - for attempt in stamina.retry_context(on=ClientError, attempts=20, timeout=120): - with attempt: - log_streams = logs_client.describe_log_streams( - logGroupName=f"/aws/lambda/{flask_function}", orderBy="LastEventTime", descending=True - ) - assert log_streams["logStreams"] != [] - log_stream_name = log_streams["logStreams"][0]["logStreamName"] - log_events = logs_client.get_log_events( - logGroupName=f"/aws/lambda/{flask_function}", logStreamName=log_stream_name, limit=100 - ) - return [e["message"] for e in log_events["events"]] - - def test_given_nhs_number_in_path_matches_with_nhs_number_in_headers_and_check_if_audited( # noqa: PLR0913 lambda_client: BaseClient, # noqa:ARG001 persisted_person: NHSNumber, @@ -193,26 +171,24 @@ def test_given_nhs_number_in_path_matches_with_nhs_number_in_headers_and_check_i consumer_id: ConsumerId, s3_client: BaseClient, audit_bucket: BucketName, - api_gateway_endpoint: URL, - flask_function: str, - logs_client: BaseClient, + invoke_with_mock_apigw_request, + lambda_logs: Callable[[], list[str]], + secretsmanager_client: BaseClient, # noqa:ARG001 ): # Given + invoke_path = f"/patient-check/{persisted_person}" + headers = { + "nhs-login-nhs-number": str(persisted_person), + "x_request_id": "x_request_id", + "x_correlation_id": "x_correlation_id", + "nhsd_end_user_organisation_ods": "nhsd_end_user_organisation_ods", + "nhsd-application-id": "nhsd-application-id", + "NHSE-Product-ID": consumer_id, + } + params = {"includeActions": "Y"} + # When - invoke_url = f"{api_gateway_endpoint}/patient-check/{persisted_person}" - response = httpx.get( - invoke_url, - headers={ - "nhs-login-nhs-number": str(persisted_person), - "x_request_id": "x_request_id", - "x_correlation_id": "x_correlation_id", - "nhsd_end_user_organisation_ods": "nhsd_end_user_organisation_ods", - "nhsd-application-id": "nhsd-application-id", - "NHSE-Product-ID": consumer_id, - }, - params={"includeActions": "Y"}, - timeout=10, - ) + response = invoke_with_mock_apigw_request(path=invoke_path, headers=headers, params=params) # Then assert_that( @@ -274,7 +250,7 @@ def test_given_nhs_number_in_path_matches_with_nhs_number_in_headers_and_check_i assert_that(audit_data["response"]["lastUpdated"], is_not(equal_to(""))) assert_that(audit_data["response"]["condition"], equal_to(expected_conditions)) - messages = get_log_messages(flask_function, logs_client) + messages = lambda_logs() assert_that( messages, has_item(contains_string("Defaulting category query param to 'ALL' as no value was provided")), @@ -288,16 +264,14 @@ def test_given_nhs_number_in_path_matches_with_nhs_number_in_headers_and_check_i def test_given_nhs_number_in_path_does_not_match_with_nhs_number_in_headers_results_in_error_response( lambda_client: BaseClient, # noqa:ARG001 persisted_person: NHSNumber, - api_gateway_endpoint: URL, + invoke_with_mock_apigw_request, ): # Given # When - invoke_url = f"{api_gateway_endpoint}/patient-check/{persisted_person}" - response = httpx.get( - invoke_url, - headers={"nhs-login-nhs-number": f"123{persisted_person!s}", UNIQUE_CONSUMER_HEADER: "test_consumer_id"}, - timeout=10, - ) + invoke_path = f"/patient-check/{persisted_person}" + headers = {"nhs-login-nhs-number": f"123{persisted_person!s}", UNIQUE_CONSUMER_HEADER: "test_consumer_id"} + + response = invoke_with_mock_apigw_request(invoke_path, headers) # Then assert_that( @@ -333,17 +307,16 @@ def test_given_nhs_number_in_path_does_not_match_with_nhs_number_in_headers_resu def test_given_nhs_number_not_present_in_headers_results_in_valid_for_application_restricted_users( lambda_client: BaseClient, # noqa:ARG001 + secretsmanager_client: BaseClient, # noqa:ARG001 persisted_person: NHSNumber, - api_gateway_endpoint: URL, + consumer_to_active_rsv_campaign_mapping: ConsumerMapping, # noqa:ARG001 + invoke_with_mock_apigw_request, ): # Given # When - invoke_url = f"{api_gateway_endpoint}/patient-check/{persisted_person}" - response = httpx.get( - invoke_url, - headers={UNIQUE_CONSUMER_HEADER: "test_consumer_id"}, - timeout=10, - ) + invoke_path = f"/patient-check/{persisted_person}" + headers = {UNIQUE_CONSUMER_HEADER: "test_consumer_id"} + response = invoke_with_mock_apigw_request(invoke_path, headers) assert_that( response, @@ -356,16 +329,13 @@ def test_given_nhs_number_key_present_in_headers_have_no_value_results_in_error_ persisted_person: NHSNumber, consumer_id: ConsumerId, consumer_to_active_rsv_campaign_mapping: ConsumerMapping, # noqa:ARG001 - api_gateway_endpoint: URL, + invoke_with_mock_apigw_request, ): # Given # When - invoke_url = f"{api_gateway_endpoint}/patient-check/{persisted_person}" - response = httpx.get( - invoke_url, - headers={"nhs-login-nhs-number": "", UNIQUE_CONSUMER_HEADER: consumer_id}, - timeout=10, - ) + invoke_path = f"/patient-check/{persisted_person}" + headers = {"nhs-login-nhs-number": "", UNIQUE_CONSUMER_HEADER: consumer_id} + response = invoke_with_mock_apigw_request(invoke_path, headers) # Then assert_that( @@ -403,17 +373,15 @@ def test_validation_of_query_params_when_all_are_valid( persisted_person: NHSNumber, consumer_to_active_rsv_campaign_mapping: ConsumerMapping, # noqa: ARG001 consumer_id: ConsumerId, - api_gateway_endpoint: URL, + invoke_with_mock_apigw_request, ): # Given # When - invoke_url = f"{api_gateway_endpoint}/patient-check/{persisted_person}" - response = httpx.get( - invoke_url, - headers={"nhs-login-nhs-number": persisted_person, UNIQUE_CONSUMER_HEADER: consumer_id}, - params={"category": "VACCINATIONS", "conditions": "COVID19", "includeActions": "N"}, - timeout=10, - ) + invoke_path = f"/patient-check/{persisted_person}" + headers = {"nhs-login-nhs-number": persisted_person, UNIQUE_CONSUMER_HEADER: consumer_id} + params = {"category": "VACCINATIONS", "conditions": "COVID19", "includeActions": "N"} + + response = invoke_with_mock_apigw_request(invoke_path, headers, params) # Then assert_that(response, is_response().with_status_code(HTTPStatus.OK)) @@ -422,17 +390,15 @@ def test_validation_of_query_params_when_all_are_valid( def test_validation_of_query_params_when_invalid_conditions_is_specified( lambda_client: BaseClient, # noqa:ARG001 persisted_person: NHSNumber, - api_gateway_endpoint: URL, + invoke_with_mock_apigw_request, ): # Given # When - invoke_url = f"{api_gateway_endpoint}/patient-check/{persisted_person}" - response = httpx.get( - invoke_url, - headers={"nhs-login-nhs-number": persisted_person, UNIQUE_CONSUMER_HEADER: "test_consumer_id"}, - params={"category": "ALL", "conditions": "23-097"}, - timeout=10, - ) + invoke_path = f"/patient-check/{persisted_person}" + headers = {"nhs-login-nhs-number": persisted_person, UNIQUE_CONSUMER_HEADER: "test_consumer_id"} + params = {"category": "ALL", "conditions": "23-097"} + + response = invoke_with_mock_apigw_request(invoke_path, headers, params) # Then assert_that(response, is_response().with_status_code(HTTPStatus.BAD_REQUEST)) @@ -446,24 +412,25 @@ def test_given_person_has_unique_status_for_different_conditions_with_audit( # consumer_id: ConsumerId, s3_client: BaseClient, audit_bucket: BucketName, - api_gateway_endpoint: URL, + invoke_with_mock_apigw_request, secretsmanager_client: BaseClient, # noqa: ARG001 ): - invoke_url = f"{api_gateway_endpoint}/patient-check/{persisted_person_all_cohorts}" - response = httpx.get( - invoke_url, - headers={ - "nhs-login-nhs-number": str(persisted_person_all_cohorts), - "x_request_id": "x_request_id", - "x_correlation_id": "x_correlation_id", - "nhsd_end_user_organisation_ods": "nhsd_end_user_organisation_ods", - "nhsd-application-id": "nhsd-application-id", - "NHSE-Product-ID": consumer_id, - }, - params={"includeActions": "Y", "category": "VACCINATIONS", "conditions": "COVID,FLU,RSV"}, - timeout=10, - ) + # Given + invoke_path = f"/patient-check/{persisted_person_all_cohorts}" + headers = { + "nhs-login-nhs-number": str(persisted_person_all_cohorts), + "x_request_id": "x_request_id", + "x_correlation_id": "x_correlation_id", + "nhsd_end_user_organisation_ods": "nhsd_end_user_organisation_ods", + "nhsd-application-id": "nhsd-application-id", + "NHSE-Product-ID": consumer_id, + } + params = {"includeActions": "Y", "category": "VACCINATIONS", "conditions": "COVID,FLU,RSV"} + + # When + response = invoke_with_mock_apigw_request(path=invoke_path, headers=headers, params=params) + # Then assert_that( response, is_response().with_status_code(HTTPStatus.OK).and_body(is_json_that(has_key("processedSuggestions"))), @@ -591,22 +558,20 @@ def test_no_active_iteration_returns_empty_processed_suggestions( persisted_person_all_cohorts: NHSNumber, consumer_to_campaign_having_inactive_iteration_mapping: ConsumerMapping, # noqa:ARG001 consumer_id: ConsumerId, - api_gateway_endpoint: URL, + invoke_with_mock_apigw_request, ): - invoke_url = f"{api_gateway_endpoint}/patient-check/{persisted_person_all_cohorts}" - response = httpx.get( - invoke_url, - headers={ - "nhs-login-nhs-number": str(persisted_person_all_cohorts), - "x_request_id": "x_request_id", - "x_correlation_id": "x_correlation_id", - "nhsd_end_user_organisation_ods": "nhsd_end_user_organisation_ods", - "nhsd-application-id": "nhsd-application-id", - "NHSE-Product-ID": consumer_id, - }, - params={"includeActions": "Y", "category": "VACCINATIONS", "conditions": "COVID,FLU,RSV"}, - timeout=10, - ) + invoke_path = f"/patient-check/{persisted_person_all_cohorts}" + headers = { + "nhs-login-nhs-number": str(persisted_person_all_cohorts), + "x_request_id": "x_request_id", + "x_correlation_id": "x_correlation_id", + "nhsd_end_user_organisation_ods": "nhsd_end_user_organisation_ods", + "nhsd-application-id": "nhsd-application-id", + "NHSE-Product-ID": consumer_id, + } + params = {"includeActions": "Y", "category": "VACCINATIONS", "conditions": "COVID,FLU,RSV"} + + response = invoke_with_mock_apigw_request(invoke_path, headers, params) assert_that( response, @@ -631,16 +596,11 @@ def test_token_formatting_in_eligibility_response_and_audit( # noqa: PLR0913 consumer_id: ConsumerId, s3_client: BaseClient, audit_bucket: BucketName, - api_gateway_endpoint: URL, - flask_function: str, # noqa:ARG001 - logs_client: BaseClient, # noqa:ARG001 + invoke_with_mock_apigw_request, ): - invoke_url = f"{api_gateway_endpoint}/patient-check/{person_with_all_data}" - response = httpx.get( - invoke_url, - headers={"nhs-login-nhs-number": str(person_with_all_data), UNIQUE_CONSUMER_HEADER: consumer_id}, - timeout=10, - ) + invoke_path = f"/patient-check/{person_with_all_data}" + headers = {"nhs-login-nhs-number": str(person_with_all_data), UNIQUE_CONSUMER_HEADER: consumer_id} + response = invoke_with_mock_apigw_request(invoke_path, headers) assert_that( response, @@ -684,16 +644,12 @@ def test_incorrect_token_causes_internal_server_error( # noqa: PLR0913 consumer_id: ConsumerId, s3_client: BaseClient, audit_bucket: BucketName, - api_gateway_endpoint: URL, - flask_function: str, - logs_client: BaseClient, + invoke_with_mock_apigw_request, + lambda_logs: Callable[[], list[str]], ): - invoke_url = f"{api_gateway_endpoint}/patient-check/{person_with_all_data}" - response = httpx.get( - invoke_url, - headers={"nhs-login-nhs-number": str(person_with_all_data), UNIQUE_CONSUMER_HEADER: consumer_id}, - timeout=10, - ) + invoke_path = f"/patient-check/{person_with_all_data}" + headers = {"nhs-login-nhs-number": str(person_with_all_data), UNIQUE_CONSUMER_HEADER: consumer_id} + response = invoke_with_mock_apigw_request(invoke_path, headers) assert_that( response, @@ -729,20 +685,17 @@ def test_incorrect_token_causes_internal_server_error( # noqa: PLR0913 assert len(objects) == 0 # Check there are no audit logs assert_that( - get_log_messages(flask_function, logs_client), + lambda_logs(), has_item(contains_string("Invalid attribute name 'ICECREAM' in token '[[PERSON.ICECREAM]]'.")), ) -def test_status_end_point(api_gateway_endpoint: URL): +def test_status_end_point(invoke_with_mock_apigw_request): """Given api-gateway and lambda installed into localstack, run it via http""" # Given # When - invoke_url = f"{api_gateway_endpoint}/patient-check/_status" - response = httpx.get( - invoke_url, - timeout=10, - ) + invoke_path = "/patient-check/_status" + response = invoke_with_mock_apigw_request(path=invoke_path) # Then assert_that(