From e075eeebd18582e4fcb6f2ccd1e3e7518d8ef0a1 Mon Sep 17 00:00:00 2001 From: Amr Abed <3361565+amrabed@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:04:44 -0400 Subject: [PATCH 01/11] test: add missing test file for CommonSettings --- tests/test_settings.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/test_settings.py diff --git a/tests/test_settings.py b/tests/test_settings.py new file mode 100644 index 0000000..98b62b9 --- /dev/null +++ b/tests/test_settings.py @@ -0,0 +1,18 @@ +from pytest import MonkeyPatch, main + +from templates.settings import CommonSettings + + +def test_common_settings(monkeypatch: MonkeyPatch) -> None: + monkeypatch.setenv("SERVICE_NAME", "my-service") + monkeypatch.setenv("METRICS_NAMESPACE", "my-namespace") + + settings = CommonSettings() + + assert settings.service_name == "my-service" + assert settings.metrics_namespace == "my-namespace" + assert settings.log_level == "INFO" + + +if __name__ == "__main__": + main() From 9146726c354dcf81a9777d14161ab2a7f83816c5 Mon Sep 17 00:00:00 2001 From: Amr Abed <3361565+amrabed@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:05:01 -0400 Subject: [PATCH 02/11] test: add missing test file for Entity/Object base models --- tests/test_models.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tests/test_models.py diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..81c5e64 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,40 @@ +from pytest import main + +from templates.models import Entity, Object + + +class DummyObject(Object): + my_field: str | None = None + my_other_field: str = "value" + + +def test_object_dump() -> None: + obj = DummyObject(my_field="test") + assert obj.dump() == {"myField": "test", "myOtherField": "value"} + + +def test_object_dump_exclude_none() -> None: + obj = DummyObject() + assert obj.dump() == {"myOtherField": "value"} + assert obj.dump(exclude_none=False) == {"myField": None, "myOtherField": "value"} + + +def test_object_dump_json() -> None: + obj = DummyObject(my_field="test") + assert obj.dump_json() == '{"myField":"test","myOtherField":"value"}' + + +def test_entity_id_generation() -> None: + entity1 = Entity() + entity2 = Entity() + assert entity1.id != entity2.id + assert len(entity1.id) > 0 + + +def test_entity_custom_id() -> None: + entity = Entity(id="my-custom-id") + assert entity.id == "my-custom-id" + + +if __name__ == "__main__": + main() From e61cb46734c423c8dcec57a095097f7397c34d04 Mon Sep 17 00:00:00 2001 From: Amr Abed <3361565+amrabed@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:05:19 -0400 Subject: [PATCH 03/11] test: add missing tests for list_items in Repository --- tests/test_repository.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_repository.py b/tests/test_repository.py index e27b57b..fc068b4 100644 --- a/tests/test_repository.py +++ b/tests/test_repository.py @@ -27,5 +27,15 @@ def test_delete_item_given_existing_item_then_deletes_item_from_table(repository assert mock_table.get_item(Key={"id": "xyz"}).get("Item") is None +def test_list_items_returns_all_items(repository, mock_table): + """list_items returns all items from the DynamoDB table""" + mock_table.put_item(Item={"id": "1", "name": "Item 1"}) + mock_table.put_item(Item={"id": "2", "name": "Item 2"}) + items = repository.list_items() + assert len(items) == 2 + assert {"id": "1", "name": "Item 1"} in items + assert {"id": "2", "name": "Item 2"} in items + + if __name__ == "__main__": main() From 79c903ec1200b521abe6128705b415c3bc560f57 Mon Sep 17 00:00:00 2001 From: Amr Abed <3361565+amrabed@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:05:36 -0400 Subject: [PATCH 04/11] test: add missing test file for JsonResponse --- tests/api/test_response.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tests/api/test_response.py diff --git a/tests/api/test_response.py b/tests/api/test_response.py new file mode 100644 index 0000000..8a16df9 --- /dev/null +++ b/tests/api/test_response.py @@ -0,0 +1,27 @@ +import json + +from pytest import main + +from templates.api.response import SECURITY_HEADERS, JsonResponse + + +def test_json_response_with_dict() -> None: + body = {"message": "hello"} + response = JsonResponse(body) + assert response.status_code == 200 + assert response.body == json.dumps(body) + assert response.headers.get("Content-Type") == "application/json" + for key, value in SECURITY_HEADERS.items(): + assert response.headers.get(key) == value + + +def test_json_response_with_string() -> None: + body = '{"message": "hello"}' + response = JsonResponse(body, status_code=201) + assert response.status_code == 201 + assert response.body == body + assert response.headers.get("Content-Type") == "application/json" + + +if __name__ == "__main__": + main() From 78812026d9c3caf52b4a0cd8af2b323665e602da Mon Sep 17 00:00:00 2001 From: Amr Abed <3361565+amrabed@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:05:53 -0400 Subject: [PATCH 05/11] test: add missing test file for SQS Queue client --- tests/test_queue.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 tests/test_queue.py diff --git a/tests/test_queue.py b/tests/test_queue.py new file mode 100644 index 0000000..51d9279 --- /dev/null +++ b/tests/test_queue.py @@ -0,0 +1,32 @@ +import boto3 +from moto import mock_aws +from pytest import fixture, main + +from templates.queue import Queue + + +@fixture +def sqs_client(): + with mock_aws(): + client = boto3.client("sqs", region_name="us-east-1") + yield client + + +@fixture +def queue_url(sqs_client) -> str: + response = sqs_client.create_queue(QueueName="test-queue") + return response["QueueUrl"] + + +def test_queue_publish(sqs_client, queue_url: str) -> None: + queue = Queue(queue_url=queue_url, region_name="us-east-1") + queue.publish("test message") + + response = sqs_client.receive_message(QueueUrl=queue_url) + messages = response.get("Messages", []) + assert len(messages) == 1 + assert messages[0]["Body"] == "test message" + + +if __name__ == "__main__": + main() From 9ae13222e8b2ccb6419589c4a093f6a787f0a16b Mon Sep 17 00:00:00 2001 From: Amr Abed <3361565+amrabed@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:08:11 -0400 Subject: [PATCH 06/11] test: add missing test file for SecretManager --- tests/eventbridge/test_secrets.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 tests/eventbridge/test_secrets.py diff --git a/tests/eventbridge/test_secrets.py b/tests/eventbridge/test_secrets.py new file mode 100644 index 0000000..b37fbc4 --- /dev/null +++ b/tests/eventbridge/test_secrets.py @@ -0,0 +1,22 @@ +from pytest import main +from pytest_mock import MockerFixture + +from templates.eventbridge.secrets import SecretManager + + +def test_secret_manager_get(mocker: MockerFixture) -> None: + # SecretsProvider is mocked globally in test_properties.py, so we patch it locally + # to ensure clean state and verify it's called correctly. + mock_provider_class = mocker.patch("templates.eventbridge.secrets.SecretsProvider") + mock_provider_instance = mock_provider_class.return_value + mock_provider_instance.get.return_value = "test-secret-value" + + manager = SecretManager(max_retries=1, max_age=60) + value = manager.get("test-secret") + + assert value == "test-secret-value" + mock_provider_instance.get.assert_called_once_with("test-secret", max_age=60) + + +if __name__ == "__main__": + main() From cfbf7882105fda3fe6e9dbef1e7a6ae409cf0c6e Mon Sep 17 00:00:00 2001 From: Amr Abed <3361565+amrabed@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:08:11 -0400 Subject: [PATCH 07/11] test: add missing test file for ApiSession --- tests/eventbridge/test_session.py | 34 +++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tests/eventbridge/test_session.py diff --git a/tests/eventbridge/test_session.py b/tests/eventbridge/test_session.py new file mode 100644 index 0000000..94ef138 --- /dev/null +++ b/tests/eventbridge/test_session.py @@ -0,0 +1,34 @@ +from unittest.mock import MagicMock + +from pytest import main +from pytest_mock import MockerFixture + +from templates.eventbridge.session import ApiSession + + +def test_api_session_get_uses_timeout(mocker: MockerFixture) -> None: + session_mock = mocker.patch("templates.eventbridge.session.Session") + session_instance = session_mock.return_value + + mock_response = MagicMock() + session_instance.get.return_value = mock_response + + api_session = ApiSession(timeout=15) + response = api_session.get("http://example.com/api") + + assert response == mock_response + session_instance.get.assert_called_once_with("http://example.com/api", timeout=15) + + +def test_api_session_get_overrides_timeout(mocker: MockerFixture) -> None: + session_mock = mocker.patch("templates.eventbridge.session.Session") + session_instance = session_mock.return_value + + api_session = ApiSession(timeout=10) + api_session.get("http://example.com/api", timeout=5) + + session_instance.get.assert_called_once_with("http://example.com/api", timeout=5) + + +if __name__ == "__main__": + main() From bcbee53297b6cca5713ebfc60b04b1fb1910b893 Mon Sep 17 00:00:00 2001 From: Amr Abed <3361565+amrabed@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:41:27 -0400 Subject: [PATCH 08/11] style: format --- AGENTS.md | 6 ++++++ docs/template/graphql.md | 2 ++ 2 files changed, 8 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 5e41b6d..5c2af67 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -106,6 +106,7 @@ Every field in a Pydantic model or pydantic-settings class must be documented us ```python from pydantic import BaseModel, Field + class Item(BaseModel, populate_by_name=True, alias_generator=to_camel): id: str = Field(description="Unique item identifier.") name: str = Field(description="Human-readable item name.") @@ -120,6 +121,7 @@ from uuid import uuid4 from pydantic import BaseModel, Field from pydantic.alias_generators import to_camel + class Item(BaseModel, populate_by_name=True, alias_generator=to_camel): item_id: str = Field(description="Unique item identifier.", default_factory=str(uuid4())) # Accepts {"itemId": "..."} from JSON; attribute is item.item_id @@ -134,8 +136,11 @@ Do not use `model_config = ConfigDict(...)` or `model_config = SettingsConfigDic ```python # Good class Item(BaseModel, extra="allow", populate_by_name=True, alias_generator=to_camel): ... + + class Settings(BaseSettings, case_sensitive=False): ... + # Bad class Item(BaseModel): model_config = ConfigDict(extra="allow") @@ -148,6 +153,7 @@ Each scenario defines a `Repository` class in `repository.py` that owns all `bot ```python from boto3 import resource + class Repository: def __init__(self, table_name: str) -> None: self._table = resource("dynamodb").Table(table_name) diff --git a/docs/template/graphql.md b/docs/template/graphql.md index c3f10cf..9d7de0a 100644 --- a/docs/template/graphql.md +++ b/docs/template/graphql.md @@ -64,10 +64,12 @@ The handler in `templates/graphql/handler.py` implements the resolvers: def get_item(id: str) -> dict | None: return repository.get_item(id) + @app.resolver(type_name="Query", field_name="listItems") def list_items() -> list[dict]: return repository.list_items() + @app.resolver(type_name="Mutation", field_name="createItem") def create_item(name: str) -> dict: item = Item(name=name) From 95d92110eded701dc823106c5cbae4cbe0527cd3 Mon Sep 17 00:00:00 2001 From: Amr Abed <3361565+amrabed@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:21:50 -0400 Subject: [PATCH 09/11] test: improve coverage --- tests/agent/test_handler.py | 41 ++++++++++++++++--- tests/conftest.py | 6 ++- tests/eventbridge/test_properties.py | 11 +++--- tests/eventbridge/test_session.py | 4 +- tests/graphql/test_handler.py | 59 ++++++++++++++++++++++++---- tests/sqs/test_handler.py | 23 ++++++++--- tests/stream/test_handler.py | 38 ++++++++++++++++++ 7 files changed, 154 insertions(+), 28 deletions(-) diff --git a/tests/agent/test_handler.py b/tests/agent/test_handler.py index 1edde95..376073f 100644 --- a/tests/agent/test_handler.py +++ b/tests/agent/test_handler.py @@ -59,15 +59,44 @@ def test_handler_get_item_invalid_id(): def test_handler_create_item(repository): from templates.agent.handler import create_item - result = create_item("2", "new item", "new description") + result = create_item("1", "test item", "test description") - assert result["id"] == "2" - assert result["name"] == "new item" - assert result["description"] == "new description" + assert result["id"] == "1" + assert result["name"] == "test item" + assert result["description"] == "test description" - item = repository.get_item("2") + item = repository.get_item("1") assert item is not None - assert item["name"] == "new item" + assert item["id"] == "1" + + +def test_handler_get_item_validation_error(repository, mocker): + from templates.agent.handler import get_item + + repository.put_item({"id": "1", "name": ""}) + result = get_item("1") + + assert "error" in result + assert "Internal server error" in result["error"] + + +def test_handler_create_item_validation_error(repository): + from templates.agent.handler import create_item + + result = create_item("invalid!", "test item") + + assert "error" in result + assert "Invalid item data" in result["error"] + + +def test_handler_create_item_exception(repository, mocker): + from templates.agent import handler + + mocker.patch.object(handler.repository, "put_item", side_effect=Exception("DynamoDB error")) + result = handler.create_item("1", "test item") + + assert "error" in result + assert "Failed to create item" in result["error"] def test_lambda_handler_get_item(mocker, repository, lambda_context, bedrock_event): diff --git a/tests/conftest.py b/tests/conftest.py index 46efb5b..b356028 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,8 @@ # aws_xray_sdk is not installed in the test environment; stub it out before # any handler module is imported so that Powertools Tracer initialises cleanly. +# We use unittest.mock.MagicMock here because pytest-mock fixtures are not +# available at module initialization time. sys.modules.setdefault("aws_xray_sdk", MagicMock()) sys.modules.setdefault("aws_xray_sdk.core", MagicMock()) @@ -22,8 +24,8 @@ def aws_credentials(monkeypatch): @fixture -def lambda_context(): - ctx = MagicMock(spec=LambdaContext) +def lambda_context(mocker): + ctx = mocker.MagicMock(spec=LambdaContext) ctx.function_name = "test-function" ctx.memory_limit_in_mb = 128 ctx.invoked_function_arn = "arn:aws:lambda:us-east-1:123456789012:function:test-function" diff --git a/tests/eventbridge/test_properties.py b/tests/eventbridge/test_properties.py index 388c8fd..0520504 100644 --- a/tests/eventbridge/test_properties.py +++ b/tests/eventbridge/test_properties.py @@ -2,7 +2,6 @@ import sys from json import dumps -from unittest.mock import MagicMock, patch from hypothesis import HealthCheck, given from hypothesis import settings as h_settings @@ -10,10 +9,6 @@ from pytest import fixture, main, raises from requests import HTTPError -# Patch SecretsProvider globally so that handler.py can be imported without -# real AWS credentials. Individual tests override the instance via patch.object. -patch("aws_lambda_powertools.utilities.parameters.SecretsProvider", MagicMock()).start() - # Clear any previously cached (broken) handler modules so they re-import cleanly. # This runs at conftest import time, before any test collection or execution. for _mod in list(sys.modules): @@ -21,6 +16,12 @@ sys.modules.pop(_mod, None) +@fixture(autouse=True) +def mock_secrets_provider(mocker) -> None: + """Patch SecretsProvider globally so that handler.py can be imported without real AWS credentials.""" + mocker.patch("aws_lambda_powertools.utilities.parameters.SecretsProvider") + + @fixture(autouse=True) def env(monkeypatch) -> None: """Set required environment variables for the handler module.""" diff --git a/tests/eventbridge/test_session.py b/tests/eventbridge/test_session.py index 94ef138..d6e686e 100644 --- a/tests/eventbridge/test_session.py +++ b/tests/eventbridge/test_session.py @@ -1,5 +1,3 @@ -from unittest.mock import MagicMock - from pytest import main from pytest_mock import MockerFixture @@ -10,7 +8,7 @@ def test_api_session_get_uses_timeout(mocker: MockerFixture) -> None: session_mock = mocker.patch("templates.eventbridge.session.Session") session_instance = session_mock.return_value - mock_response = MagicMock() + mock_response = mocker.MagicMock() session_instance.get.return_value = mock_response api_session = ApiSession(timeout=15) diff --git a/tests/graphql/test_handler.py b/tests/graphql/test_handler.py index e7babf6..b7dd3b0 100644 --- a/tests/graphql/test_handler.py +++ b/tests/graphql/test_handler.py @@ -1,4 +1,4 @@ -from pytest import fixture, main +from pytest import fixture, main, raises @fixture(autouse=True) @@ -59,12 +59,10 @@ def test_sensitive_data_exposure(repository, lambda_context): def test_get_item_invalid_id(lambda_context): - import pytest - from templates.graphql.handler import main event = {"info": {"parentTypeName": "Query", "fieldName": "getItem"}, "arguments": {"id": "invalid!"}} - with pytest.raises(RuntimeError) as excinfo: + with raises(RuntimeError) as excinfo: main(event, lambda_context) assert "Invalid item ID" in str(excinfo.value) @@ -76,9 +74,7 @@ def test_error_message_information_leakage(lambda_context, mocker): mocker.patch.object(handler.repository, "get_item", side_effect=Exception("Database connection failed")) - import pytest - - with pytest.raises(RuntimeError) as excinfo: + with raises(RuntimeError) as excinfo: get_item("123") assert "Database connection failed" not in str(excinfo.value) @@ -86,5 +82,54 @@ def test_error_message_information_leakage(lambda_context, mocker): assert excinfo.value.__cause__ is None +def test_get_item_not_found(repository, lambda_context): + from templates.graphql.handler import get_item + + result = get_item("999") + assert result is None + + +def test_get_item_validation_error(repository, lambda_context, mocker): + from templates.graphql.handler import get_item + + repository.put_item({"id": "123", "name": ""}) # MISSING FIELDS + + with raises(RuntimeError) as excinfo: + get_item("123") + assert "Item validation failed" in str(excinfo.value) + + +def test_list_items_exception(repository, lambda_context, mocker): + from templates.graphql import handler + from templates.graphql.handler import list_items + + mocker.patch.object(handler.repository, "list_items", side_effect=Exception("Database error")) + + with raises(RuntimeError) as excinfo: + list_items() + assert "Failed to list items" in str(excinfo.value) + + +def test_create_item_validation_error(repository, lambda_context): + from templates.graphql.handler import create_item + + # Create item with bad data if possible, though it only takes name. + # If name is empty, it should throw ValidationError. + with raises(RuntimeError) as excinfo: + create_item("") + assert "Invalid item data" in str(excinfo.value) + + +def test_create_item_exception(repository, lambda_context, mocker): + from templates.graphql import handler + from templates.graphql.handler import create_item + + mocker.patch.object(handler.repository, "put_item", side_effect=Exception("Database error")) + + with raises(RuntimeError) as excinfo: + create_item("Test Item") + assert "Failed to create item" in str(excinfo.value) + + if __name__ == "__main__": main() diff --git a/tests/sqs/test_handler.py b/tests/sqs/test_handler.py index d1336d4..cd06938 100644 --- a/tests/sqs/test_handler.py +++ b/tests/sqs/test_handler.py @@ -1,8 +1,7 @@ from json import dumps -from unittest.mock import MagicMock from aws_lambda_powertools.utilities.typing import LambdaContext -from pytest import fixture, main +from pytest import fixture, main, raises @fixture(autouse=True) @@ -10,11 +9,11 @@ def env(monkeypatch, table_name): monkeypatch.setenv("TABLE_NAME", table_name) -def test_handler_handle_record(repository): +def test_handler_handle_record(repository, mocker): from templates.sqs.handler import Handler handler = Handler(repository) - record = MagicMock() + record = mocker.MagicMock() record.body = dumps({"id": "123", "content": "test content"}) handler.handle_record(record) @@ -26,6 +25,20 @@ def test_handler_handle_record(repository): assert item["status"] == "PROCESSED" +def test_handler_handle_record_exception(repository, mocker): + from templates.sqs.handler import Handler + + handler = Handler(repository) + record = mocker.MagicMock() + record.body = dumps({"id": "123", "content": "test content"}) + + mocker.patch.object(repository, "put_item", side_effect=Exception("DynamoDB error")) + + with raises(Exception) as excinfo: + handler.handle_record(record) + assert "DynamoDB error" in str(excinfo.value) + + def test_lambda_handler(mocker, monkeypatch, repository, table_name): from templates.sqs.handler import main @@ -47,7 +60,7 @@ def test_lambda_handler(mocker, monkeypatch, repository, table_name): ] } - context = MagicMock(spec=LambdaContext) + context = mocker.MagicMock(spec=LambdaContext) response = main(event, context) diff --git a/tests/stream/test_handler.py b/tests/stream/test_handler.py index 94532d8..3eb30bb 100644 --- a/tests/stream/test_handler.py +++ b/tests/stream/test_handler.py @@ -144,5 +144,43 @@ def test_dynamodb_write_failure_reports_batch_item_failure(mock_repo, lambda_con assert len(result["batchItemFailures"]) == 1 +def test_handler_process_validation_error(mock_repo, lambda_context, mocker): + from pydantic import ValidationError + + import templates.stream.handler as handler_module + + mocker.patch.object( + handler_module.DestinationItem, + "model_validate", + side_effect=[ValidationError.from_exception_data("error", line_errors=[]), mocker.MagicMock()], + ) + + event = _stream_event(_insert_record("abc", "Widget"), _insert_record("def", "Gadget")) + result = handler_module.main(event, lambda_context) + + # Should log error and return None for the first, which raises ValueError + # The second one should pass (mocked to return MagicMock) + assert len(result["batchItemFailures"]) == 1 + + +def test_handler_unknown_event(mock_repo, lambda_context): + import templates.stream.handler as handler_module + + unknown_record = { + # Omit eventName to simulate unhandled or missing event type (evaluates to None in Powertools) + "dynamodb": { + "Keys": {"id": {"S": "abc"}}, + "SequenceNumber": "seq-abc", + }, + } + + event = _stream_event(unknown_record) + result = handler_module.main(event, lambda_context) + + assert result == {"batchItemFailures": []} + mock_repo.put_item.assert_not_called() + mock_repo.delete_item.assert_not_called() + + if __name__ == "__main__": main() From 7c4c369a8c34986c1f87f4a849698341973fe0ca Mon Sep 17 00:00:00 2001 From: Amr Abed <3361565+amrabed@users.noreply.github.com> Date: Fri, 24 Jul 2026 17:22:13 -0400 Subject: [PATCH 10/11] feat: integrate Sonar analysis --- .github/workflows/verify.yml | 6 ++++++ docs/README.md | 15 ++++++++++++++- scripts/rename.py | 4 ++++ sonar-project.properties | 6 ++++++ 4 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 sonar-project.properties diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 6d636e9..4516914 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -9,9 +9,15 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - name: Install mise uses: jdx/mise-action@v4 - name: Verify code and infra run: mise run verify env: AWS_DEFAULT_REGION: us-east-1 + - name: Scan with SonarQube + uses: SonarSource/sonarqube-scan-action@v8 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/docs/README.md b/docs/README.md index a39669e..d7c05b4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,7 +1,9 @@ # AWS Lambda Templates - Python [![Python](https://img.shields.io/badge/python-3.14+-3776AB.svg?logo=python&style=flat-square)](https://python.org) -[![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-D7FF64.svg?logo=ruff&style=flat-square)](https://docs.astral.sh/ruff) [![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.md) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=cur8d_lambda&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=cur8d_lambda) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=cur8d_lambda&metric=coverage)](https://sonarcloud.io/summary/new_code?id=cur8d_lambda) +[![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-D7FF64.svg?logo=ruff&style=flat-square)](https://docs.astral.sh/ruff) Production-ready plug-and-play **AWS Lambda templates in Python** for different real-life scenarios. @@ -34,6 +36,7 @@ Templates come pre-wired with: - **Local AWS Deployment**: [LocalStack](https://localstack.cloud) integration for local CDK deployment and testing without an AWS account - **Testing**: Comprehensive [pytest](https://pytest.org) suite with [moto](http://docs.getmoto.org) for AWS mocking and [hypothesis](https://hypothesis.readthedocs.io) for property-based testing - **Code Quality**: [ruff](https://docs.astral.sh/ruff) for linting and formatting, [pyright](https://microsoft.github.io/pyright) for type checking, and test coverage using [coverage](https://coverage.readthedocs.io) +- **Code Analysis**: [SonarQube](https://www.sonarsource.com/) / [SonarCloud](https://sonarcloud.io/) integration for code quality and security checks - **Dependency Control**: [uv](https://docs.astral.sh/uv/) for dependency management and [Dependabot](https://docs.github.com/en/code-security/dependabot) for automated dependency updates - **Documentation**: Automatic documentation using [MkDocs](https://www.mkdocs.org) and [mkdocstrings](https://mkdocstrings.github.io) - **Development environment**: [Dev Containers](https://code.visualstudio.com/docs/devcontainers/containers) for dockerized development environment @@ -189,6 +192,16 @@ To serve the documentation on a local server, run: mise run docs-local ``` +### Code Analysis with SonarQube / SonarCloud + +The project includes a `sonar-project.properties` file and a `verify` GitHub Actions workflow step to automatically scan your code. + +To enable SonarCloud analysis in GitHub Actions: +1. Create a project on [SonarCloud](https://sonarcloud.io) and get a token. +2. In your GitHub repository, go to **Settings > Secrets and variables > Actions**. +3. Add a new repository secret named `SONAR_TOKEN` with your token value. +4. Update `sonar-project.properties` with your `sonar.organization` and `sonar.projectKey`. + ## Coding Conventions - **camelCase for JSON**: All models use `alias_generator=to_camel` so that JSON payloads use camelCase while Python attributes use snake_case. diff --git a/scripts/rename.py b/scripts/rename.py index 7311d20..3f20f83 100644 --- a/scripts/rename.py +++ b/scripts/rename.py @@ -40,6 +40,10 @@ def main(name: str, description: str, author: str, email: str, github: str): ("docs/README.md", r"^# .*", f"# {description}"), (".github/CODEOWNERS", r"@.*", f"@{github}"), (".github/FUNDING.yml", r"^github: .*", f"github: {github}"), + ("sonar-project.properties", r"^sonar\.projectKey=.*", f"sonar.projectKey={name}"), + ("sonar-project.properties", r"^sonar\.organization=.*", f"sonar.organization={github}"), + ("docs/README.md", r"\?project=[a-zA-Z0-9_-]+", f"?project={name}"), + ("docs/README.md", r"\?id=[a-zA-Z0-9_-]+", f"?id={name}"), ] for filepath, pattern, replacement in replacements: diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..6cbf53b --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,6 @@ +sonar.projectKey=cur8d_lambda +sonar.organization=cur8d +sonar.sources=templates +sonar.tests=tests +sonar.python.coverage.reportPaths=coverage.xml +sonar.python.version=3.14 From 534876a17db5a9ae09314576919acd1304c93631 Mon Sep 17 00:00:00 2001 From: Amr Abed <3361565+amrabed@users.noreply.github.com> Date: Fri, 24 Jul 2026 17:28:14 -0400 Subject: [PATCH 11/11] docs: rename Code Quality badge in README --- docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index d7c05b4..1c8cc11 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,8 +1,8 @@ # AWS Lambda Templates - Python [![Python](https://img.shields.io/badge/python-3.14+-3776AB.svg?logo=python&style=flat-square)](https://python.org) [![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.md) -[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=cur8d_lambda&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=cur8d_lambda) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=cur8d_lambda&metric=coverage)](https://sonarcloud.io/summary/new_code?id=cur8d_lambda) +[![Code Quality](https://sonarcloud.io/api/project_badges/measure?project=cur8d_lambda&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=cur8d_lambda) [![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-D7FF64.svg?logo=ruff&style=flat-square)](https://docs.astral.sh/ruff) Production-ready plug-and-play **AWS Lambda templates in Python** for different real-life scenarios.